Skip to content

Commit

Permalink
Merge pull request #15263 from BGBRWR/issue-14010
Browse files Browse the repository at this point in the history
  • Loading branch information
cetincakiroglu authored Apr 18, 2024
2 parents 222a146 + 4522279 commit c4163b7
Show file tree
Hide file tree
Showing 28 changed files with 453 additions and 133 deletions.
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

0 comments on commit c4163b7

Please sign in to comment.