Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #14010, #13162, #13768, #13082, #10472 #15263

Merged
merged 2 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/app/components/autofocus/autofocus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@ export class AutoFocus {
* When present, it specifies that the component should automatically get focus on load.
* @group Props
*/
@Input({ transform: booleanAttribute }) autofocus: boolean | undefined;
@Input({ transform: booleanAttribute }) autofocus: boolean = false;

focused: boolean = false;

ngAfterContentChecked() {
// This sets the `attr.autofocus` which is different than the Input `autofocus` attribute.
if (this.autofocus === false) {
this.host.nativeElement.removeAttribute('autofocus');
} else {
this.host.nativeElement.setAttribute('autofocus', true);
}

if (!this.focused) {
if (this.autofocus) {
setTimeout(() => {
Expand Down
10 changes: 9 additions & 1 deletion src/app/components/button/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { DomHandler } from 'primeng/dom';
import { SpinnerIcon } from 'primeng/icons/spinner';
import { RippleModule } from 'primeng/ripple';
import { ObjectUtils } from 'primeng/utils';
import { AutoFocusModule } from 'primeng/autofocus';

type ButtonIconPosition = 'left' | 'right' | 'top' | 'bottom';

Expand Down Expand Up @@ -342,6 +343,8 @@ export class ButtonDirective implements AfterViewInit, OnDestroy {
[attr.data-pc-name]="'button'"
[attr.data-pc-section]="'root'"
[attr.tabindex]="tabindex"
pAutoFocus
[autofocus]="autofocus"
>
<ng-content></ng-content>
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
Expand Down Expand Up @@ -477,6 +480,11 @@ export class Button implements AfterContentInit {
* @group Props
*/
@Input() ariaLabel: string | undefined;
/**
* When present, it specifies that the component should automatically get focus on load.
* @group Props
*/
@Input({ transform: booleanAttribute }) autofocus: boolean | undefined;
/**
* Callback to execute when button is clicked.
* This event is intended to be used with the <p-button> component. Using a regular <button> element, use (click).
Expand Down Expand Up @@ -577,7 +585,7 @@ export class Button implements AfterContentInit {
}

@NgModule({
imports: [CommonModule, RippleModule, SharedModule, SpinnerIcon],
imports: [CommonModule, RippleModule, SharedModule, AutoFocusModule, SpinnerIcon],
exports: [ButtonDirective, Button, SharedModule],
declarations: [ButtonDirective, Button]
})
Expand Down
10 changes: 9 additions & 1 deletion src/app/components/calendar/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { TimesIcon } from 'primeng/icons/times';
import { CalendarIcon } from 'primeng/icons/calendar';
import { Nullable, VoidListener } from 'primeng/ts-helpers';
import { NavigationState, CalendarResponsiveOptions, CalendarTypeView, LocaleSettings, Month, CalendarMonthChangeEvent, CalendarYearChangeEvent } from './calendar.interface';
import { AutoFocusModule } from 'primeng/autofocus';

export const CALENDAR_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
Expand Down Expand Up @@ -94,6 +95,8 @@ export const CALENDAR_VALUE_ACCESSOR: any = {
[attr.inputmode]="touchUI ? 'off' : null"
[ngClass]="'p-inputtext p-component'"
autocomplete="off"
pAutoFocus
[autofocus]="autofocus"
/>
<ng-container *ngIf="showClear && !disabled && value != null">
<TimesIcon *ngIf="!clearIconTemplate" [styleClass]="'p-calendar-clear-icon'" (click)="clear()" />
Expand Down Expand Up @@ -676,6 +679,11 @@ export class Calendar implements OnInit, OnDestroy, ControlValueAccessor {
* @group Props
*/
@Input() clearButtonStyleClass: string = 'p-button-text';
/**
* When present, it specifies that the component should automatically get focus on load.
* @group Props
*/
@Input({ transform: booleanAttribute }) autofocus: boolean | undefined;
/**
* Whether to automatically manage layering.
* @group Props
Expand Down Expand Up @@ -3530,7 +3538,7 @@ export class Calendar implements OnInit, OnDestroy, ControlValueAccessor {
}

@NgModule({
imports: [CommonModule, ButtonModule, SharedModule, RippleModule, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, ChevronDownIcon, TimesIcon, CalendarIcon],
imports: [CommonModule, ButtonModule, SharedModule, RippleModule, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, ChevronDownIcon, TimesIcon, CalendarIcon, AutoFocusModule],
exports: [Calendar, ButtonModule, SharedModule],
declarations: [Calendar]
})
Expand Down
12 changes: 10 additions & 2 deletions src/app/components/cascadeselect/cascadeselect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { OverlayOptions, OverlayService, PrimeNGConfig, PrimeTemplate, SharedModule, TranslationKeys } from 'primeng/api';
import { DomHandler } from 'primeng/dom';
import { AngleRightIcon } from 'primeng/icons/angleright';
import { AutoFocusModule } from 'primeng/autofocus';

import { ChevronDownIcon } from 'primeng/icons/chevrondown';
import { TimesIcon } from 'primeng/icons/times';
import { Overlay, OverlayModule } from 'primeng/overlay';
Expand Down Expand Up @@ -252,6 +254,8 @@ export class CascadeSelectSub implements OnInit {
(focus)="onInputFocus($event)"
(blur)="onInputBlur($event)"
(keydown)="onInputKeyDown($event)"
pAutoFocus
[autofocus]="autofocus"
/>
</div>
<span [ngClass]="labelClass" [attr.data-pc-section]="'label'">
Expand Down Expand Up @@ -496,6 +500,11 @@ export class CascadeSelect implements OnInit, AfterContentInit {
* @group Props
*/
@Input() overlayOptions: OverlayOptions | undefined;
/**
* When present, it specifies that the component should automatically get focus on load.
* @group Props
*/
@Input({ transform: booleanAttribute }) autofocus: boolean | undefined;
/**
* Transition options of the show animation.
* @group Props
Expand Down Expand Up @@ -636,7 +645,6 @@ export class CascadeSelect implements OnInit, AfterContentInit {
get labelClass() {
return {
'p-cascadeselect-label': true,
'p-inputtext': true,
'p-placeholder': this.label() === this.placeholder,
'p-cascadeselect-label-empty': !this.value && (this.label() === 'p-emptylabel' || this.label().length === 0)
};
Expand Down Expand Up @@ -1335,7 +1343,7 @@ export class CascadeSelect implements OnInit, AfterContentInit {
}

@NgModule({
imports: [CommonModule, OverlayModule, SharedModule, RippleModule, ChevronDownIcon, AngleRightIcon, TimesIcon],
imports: [CommonModule, OverlayModule, SharedModule, RippleModule, AutoFocusModule, ChevronDownIcon, AngleRightIcon, TimesIcon],
exports: [CascadeSelect, OverlayModule, CascadeSelectSub, SharedModule],
declarations: [CascadeSelect, CascadeSelectSub]
})
Expand Down
10 changes: 9 additions & 1 deletion src/app/components/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { PrimeTemplate, SharedModule } from 'primeng/api';
import { AutoFocusModule } from 'primeng/autofocus';
import { CheckIcon } from 'primeng/icons/check';
import { Nullable } from 'primeng/ts-helpers';
import { ObjectUtils } from 'primeng/utils';
Expand Down Expand Up @@ -62,6 +63,8 @@ export const CHECKBOX_VALUE_ACCESSOR: any = {
(focus)="onInputFocus($event)"
(blur)="onInputBlur($event)"
[attr.data-pc-section]="'hiddenInput'"
pAutoFocus
[autofocus]="autofocus"
/>
</div>
<div
Expand Down Expand Up @@ -184,6 +187,11 @@ export class Checkbox implements ControlValueAccessor {
* @group Props
*/
@Input({ transform: booleanAttribute }) required: boolean | undefined;
/**
* When present, it specifies that the component should automatically get focus on load.
* @group Props
*/
@Input({ transform: booleanAttribute }) autofocus: boolean | undefined;
/**
* Value in checked state.
* @group Props
Expand Down Expand Up @@ -322,7 +330,7 @@ export class Checkbox implements ControlValueAccessor {
}

@NgModule({
imports: [CommonModule, CheckIcon],
imports: [CommonModule, AutoFocusModule, CheckIcon],
exports: [Checkbox, SharedModule],
declarations: [Checkbox]
})
Expand Down
10 changes: 9 additions & 1 deletion src/app/components/chips/chips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { PrimeTemplate, SharedModule } from 'primeng/api';
import { AutoFocusModule } from 'primeng/autofocus';
import { TimesIcon } from 'primeng/icons/times';
import { TimesCircleIcon } from 'primeng/icons/timescircle';
import { InputTextModule } from 'primeng/inputtext';
Expand Down Expand Up @@ -107,6 +108,8 @@ export const CHIPS_VALUE_ACCESSOR: any = {
[disabled]="disabled || isMaxedOut"
[ngStyle]="inputStyle"
[class]="inputStyleClass"
pAutoFocus
[autofocus]="autofocus"
/>
</li>
<li *ngIf="value != null && filled && !disabled && showClear">
Expand Down Expand Up @@ -225,6 +228,11 @@ export class Chips implements AfterContentInit, ControlValueAccessor {
* @group Props
*/
@Input({ transform: booleanAttribute }) showClear: boolean = false;
/**
* When present, it specifies that the component should automatically get focus on load.
* @group Props
*/
@Input({ transform: booleanAttribute }) autofocus: boolean | undefined;
/**
* Callback to invoke on chip add.
* @param {ChipsAddEvent} event - Custom chip add event.
Expand Down Expand Up @@ -592,7 +600,7 @@ export class Chips implements AfterContentInit, ControlValueAccessor {
}

@NgModule({
imports: [CommonModule, InputTextModule, SharedModule, TimesCircleIcon, TimesIcon],
imports: [CommonModule, InputTextModule, SharedModule, AutoFocusModule, TimesCircleIcon, TimesIcon],
exports: [Chips, InputTextModule, SharedModule],
declarations: [Chips]
})
Expand Down
10 changes: 9 additions & 1 deletion src/app/components/colorpicker/colorpicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { OverlayService, PrimeNGConfig, TranslationKeys } from 'primeng/api';
import { ConnectedOverlayScrollHandler, DomHandler } from 'primeng/dom';
import { AutoFocusModule } from 'primeng/autofocus';
import { Nullable, VoidListener } from 'primeng/ts-helpers';
import { ZIndexUtils } from 'primeng/utils';
import { ColorPickerChangeEvent } from './colorpicker.interface';
Expand Down Expand Up @@ -63,6 +64,8 @@ export const COLORPICKER_VALUE_ACCESSOR: any = {
[style.backgroundColor]="inputBgColor"
[attr.data-pc-section]="'input'"
[attr.aria-label]="ariaLabel"
pAutoFocus
[autofocus]="autofocus"
/>
<div
*ngIf="inline || overlayVisible"
Expand Down Expand Up @@ -157,6 +160,11 @@ export class ColorPicker implements ControlValueAccessor, OnDestroy {
* @group Props
*/
@Input() hideTransitionOptions: string = '.1s linear';
/**
* When present, it specifies that the component should automatically get focus on load.
* @group Props
*/
@Input({ transform: booleanAttribute }) autofocus: boolean | undefined;
/**
* Callback to invoke on value change.
* @param {ColorPickerChangeEvent} event - Custom value change event.
Expand Down Expand Up @@ -809,7 +817,7 @@ export class ColorPicker implements ControlValueAccessor, OnDestroy {
}

@NgModule({
imports: [CommonModule],
imports: [CommonModule, AutoFocusModule],
exports: [ColorPicker],
declarations: [ColorPicker]
})
Expand Down
45 changes: 14 additions & 31 deletions src/app/components/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const hideAnimation = animation([animate('{{transition}}', style({ transform: '{
*ngIf="maskVisible"
[class]="maskStyleClass"
[style]="maskStyle"
(focus)="containerFocus($event)"
[ngClass]="{
'p-dialog-mask': true,
'p-component-overlay p-component-overlay-enter': this.modal,
Expand Down Expand Up @@ -311,7 +310,7 @@ export class Dialog implements AfterContentInit, OnInit, OnDestroy {
*/
@Input({ transform: numberAttribute }) minY: number = 0;
/**
* When enabled, first button receives focus on show.
* When enabled, first focusable element receives focus on show.
* @group Props
*/
@Input({ transform: booleanAttribute }) focusOnShow: boolean = true;
Expand Down Expand Up @@ -599,12 +598,22 @@ export class Dialog implements AfterContentInit, OnInit, OnDestroy {
return this.header !== null ? UniqueComponentId() + '_header' : null;
}

focus() {
let focusable = DomHandler.findSingle(this.container, '[autofocus]');
focus(focusParentElement = this.contentViewChild.nativeElement) {
let focusable = DomHandler.getFocusableElement(focusParentElement, '[autofocus]');
if (focusable) {
this.zone.runOutsideAngular(() => {
setTimeout(() => focusable.focus(), 5);
});
return;
}
const focusableElement = DomHandler.getFocusableElement(focusParentElement);
if (focusableElement) {
this.zone.runOutsideAngular(() => {
setTimeout(() => focusableElement.focus(), 5);
});
} else if (this.footerViewChild) {
// If the content section is empty try to focus on footer
this.focus(this.footerViewChild.nativeElement);
}
}

Expand Down Expand Up @@ -711,32 +720,6 @@ export class Dialog implements AfterContentInit, OnInit, OnDestroy {
}
}

onKeydown(event: KeyboardEvent) {
if (this.focusTrap) {
if (event.which === 9) {
event.preventDefault();

let focusableElements = DomHandler.getFocusableElements(this.container as HTMLDivElement);

if (focusableElements && focusableElements.length > 0) {
if (!focusableElements[0].ownerDocument.activeElement) {
focusableElements[0].focus();
} else {
let focusedIndex = focusableElements.indexOf(focusableElements[0].ownerDocument.activeElement);

if (event.shiftKey) {
if (focusedIndex == -1 || focusedIndex === 0) focusableElements[focusableElements.length - 1].focus();
else focusableElements[focusedIndex - 1].focus();
} else {
if (focusedIndex == -1 || focusedIndex === focusableElements.length - 1) focusableElements[0].focus();
else focusableElements[focusedIndex + 1].focus();
}
}
}
}
}
}

onDrag(event: MouseEvent) {
if (this.dragging) {
const containerWidth = DomHandler.getOuterWidth(this.container);
Expand Down Expand Up @@ -929,7 +912,7 @@ export class Dialog implements AfterContentInit, OnInit, OnDestroy {
const documentTarget: any = this.el ? this.el.nativeElement.ownerDocument : 'document';

this.documentEscapeListener = this.renderer.listen(documentTarget, 'keydown', (event) => {
if (event.which == 27) {
if (event.key == 'Escape') {
this.close(event);
}
});
Expand Down
36 changes: 25 additions & 11 deletions src/app/components/dom/domhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,17 +615,20 @@ export class DomHandler {
element && document.activeElement !== element && element.focus(options);
}

public static getFocusableElements(element, selector = '') {
let focusableElements = this.find(
element,
`button:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
[href][clientHeight][clientWidth]:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
input:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
select:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
textarea:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
[tabIndex]:not([tabIndex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
[contenteditable]:not([tabIndex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector}`
);
public static getFocusableSelectorString(selector = ''): string {
return `button:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
[href][clientHeight][clientWidth]:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
input:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
select:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
textarea:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
[tabIndex]:not([tabIndex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
[contenteditable]:not([tabIndex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
.p-inputtext:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector},
.p-button:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${selector}`;
}

public static getFocusableElements(element, selector = ''): any[] {
let focusableElements = this.find(element, this.getFocusableSelectorString(selector));

let visibleFocusableElements = [];

Expand All @@ -637,6 +640,17 @@ export class DomHandler {
return visibleFocusableElements;
}

public static getFocusableElement(element, selector = ''): any | null {
let focusableElement = this.findSingle(element, this.getFocusableSelectorString(selector));

if (focusableElement) {
const computedStyle = getComputedStyle(focusableElement);
if (this.isVisible(focusableElement) && computedStyle.display != 'none' && computedStyle.visibility != 'hidden') return focusableElement;
}

return null;
}

public static getFirstFocusableElement(element, selector) {
const focusableElements = this.getFocusableElements(element, selector);

Expand Down
Loading