Skip to content

Commit

Permalink
Fixed #15482 - Rework and update focustrap
Browse files Browse the repository at this point in the history
  • Loading branch information
cetincakiroglu committed May 10, 2024
1 parent d04749e commit d10cb09
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 13 deletions.
75 changes: 74 additions & 1 deletion src/app/components/dom/domhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ export class DomHandler {
return null;
}

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

return focusableElements.length > 0 ? focusableElements[0] : null;
Expand Down Expand Up @@ -765,4 +765,77 @@ export class DomHandler {
document.body.style.removeProperty('--scrollbar-width');
this.removeClass(document.body, className);
}

public static createElement(type, attributes = {}, ...children) {
if (type) {
const element = document.createElement(type);

this.setAttributes(element, attributes);
element.append(...children);

return element;
}

return undefined;
}

public static setAttribute(element, attribute = '', value) {
if (this.isElement(element) && value !== null && value !== undefined) {
element.setAttribute(attribute, value);
}
}

public static setAttributes(element, attributes = {}) {
if (this.isElement(element)) {
const computedStyles = (rule, value) => {
const styles = element?.$attrs?.[rule] ? [element?.$attrs?.[rule]] : [];

return [value].flat().reduce((cv, v) => {
if (v !== null && v !== undefined) {
const type = typeof v;

if (type === 'string' || type === 'number') {
cv.push(v);
} else if (type === 'object') {
const _cv = Array.isArray(v)
? computedStyles(rule, v)
: Object.entries(v).map(([_k, _v]) => (rule === 'style' && (!!_v || _v === 0) ? `${_k.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}:${_v}` : !!_v ? _k : undefined));

cv = _cv.length ? cv.concat(_cv.filter((c) => !!c)) : cv;
}
}

return cv;
}, styles);
};

Object.entries(attributes).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
const matchedEvent = key.match(/^on(.+)/);

if (matchedEvent) {
element.addEventListener(matchedEvent[1].toLowerCase(), value);
} else if (key === 'pBind') {
this.setAttributes(element, value);
} else {
value = key === 'class' ? [...new Set(computedStyles('class', value))].join(' ').trim() : key === 'style' ? computedStyles('style', value).join(';').trim() : value;
(element.$attrs = element.$attrs || {}) && (element.$attrs[key] = value);
element.setAttribute(key, value);
}
}
});
}
}

public static isFocusableElement(element, selector = '') {
return this.isElement(element)
? element.matches(`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}`)
: false;
}
}
72 changes: 60 additions & 12 deletions src/app/components/focustrap/focustrap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DomHandler } from 'primeng/dom';
import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common';
import { Directive, ElementRef, Input, NgModule, inject, booleanAttribute, PLATFORM_ID } from '@angular/core';

import { CommonModule } from '@angular/common';
import { Directive, ElementRef, HostListener, Input, NgModule, inject, booleanAttribute } from '@angular/core';
/**
* Focus Trap keeps focus within a certain DOM element while tabbing.
* @group Components
Expand All @@ -19,20 +19,68 @@ export class FocusTrap {
*/
@Input({ transform: booleanAttribute }) pFocusTrapDisabled: boolean = false;

platformId = inject(PLATFORM_ID);

host: ElementRef = inject(ElementRef);

@HostListener('keydown.tab', ['$event'])
@HostListener('keydown.shift.tab', ['$event'])
onkeydown(e: KeyboardEvent) {
if (this.pFocusTrapDisabled !== true) {
e.preventDefault();
const focusableElement = DomHandler.getNextFocusableElement(this.host.nativeElement, e.shiftKey);
if (focusableElement) {
focusableElement.focus();
focusableElement.select?.();
}
document: Document = inject(DOCUMENT);

firstHiddenFocusableElement!: HTMLElement;

lastHiddenFocusableElement!: HTMLElement;

ngOnInit() {
if (isPlatformBrowser(this.platformId) && !this.pFocusTrapDisabled) {
this.createHiddenFocusableElements();
}
}

getComputedSelector(selector) {
return `:not(.p-hidden-focusable):not([data-p-hidden-focusable="true"])${selector ?? ''}`;
}

createHiddenFocusableElements() {
const tabindex = '0';

const createFocusableElement = (onFocus) => {
return DomHandler.createElement('span', {
class: 'p-hidden-accessible p-hidden-focusable',
tabindex,
role: 'presentation',
'aria-hidden': true,
'data-p-hidden-accessible': true,
'data-p-hidden-focusable': true,
onFocus: onFocus?.bind(this)
});
};

this.firstHiddenFocusableElement = createFocusableElement(this.onFirstHiddenElementFocus);
this.lastHiddenFocusableElement = createFocusableElement(this.onLastHiddenElementFocus);

this.firstHiddenFocusableElement.setAttribute('data-pc-section', 'firstfocusableelement');
this.lastHiddenFocusableElement.setAttribute('data-pc-section', 'lastfocusableelement');

this.host.nativeElement.prepend(this.firstHiddenFocusableElement);
this.host.nativeElement.append(this.lastHiddenFocusableElement);
}

onFirstHiddenElementFocus(event) {
const { currentTarget, relatedTarget } = event;
const focusableElement =
relatedTarget === this.lastHiddenFocusableElement || !this.host.nativeElement?.contains(relatedTarget) ? DomHandler.getFirstFocusableElement(currentTarget.parentElement, ':not(.p-hidden-focusable)') : this.lastHiddenFocusableElement;

DomHandler.focus(focusableElement);
}

onLastHiddenElementFocus(event) {
const { currentTarget, relatedTarget } = event;
const focusableElement =
relatedTarget === this.firstHiddenFocusableElement || !this.host.nativeElement?.contains(relatedTarget) ? DomHandler.getLastFocusableElement(currentTarget.parentElement, ':not(.p-hidden-focusable)') : this.firstHiddenFocusableElement;

DomHandler.focus(focusableElement);
}

ngOnDestroy() {}
}

@NgModule({
Expand Down

0 comments on commit d10cb09

Please sign in to comment.