diff --git a/components/lib/focustrap/FocusTrap.d.ts b/components/lib/focustrap/FocusTrap.d.ts index 41bc8b7ac..68ccb7b82 100644 --- a/components/lib/focustrap/FocusTrap.d.ts +++ b/components/lib/focustrap/FocusTrap.d.ts @@ -8,6 +8,89 @@ */ import { DirectiveBinding, ObjectDirective } from 'vue'; +/** + * Custom passthrough(pt) hooks options. + */ +export interface FocusTrapPassThroughHooksOptions { + /** + * Called before bound element's attributes or event listeners are applied. + */ + created?: DirectiveBinding; + /** + * Called right before the element is inserted into the DOM. + */ + beforeMount?: DirectiveBinding; + /** + * Called when the bound element's parent component and all its children are mounted. + */ + mounted?: DirectiveBinding; + /** + * Called before the parent component is updated. + */ + beforeUpdate?: DirectiveBinding; + /** + * Called after the parent component and all of its children have updated all of its children have updated. + */ + updated?: DirectiveBinding; + /** + * Called before the parent component is unmounted. + */ + beforeUnmount?: DirectiveBinding; + /** + * Called when the parent component is unmounted. + */ + unmounted?: DirectiveBinding; +} + +/** + * Custom passthrough(pt) css options. + */ +export interface FocusTrapPassThroughCSSOptions { + /** + * Style class of the element. + */ + class?: any; + /** + * Inline style of the element. + */ + style?: any; +} + +export interface FocusTrapPassThroughDirectiveOptions { + /** + * Uses to pass attributes to the life cycle hooks. + * @see {@link FocusTrapPassThroughHooksOptions} + */ + hooks?: FocusTrapPassThroughHooksOptions; + /** + * Uses to pass attributes to the styles. + * @see {@link FocusTrapPassThroughCSSOptions} + */ + css?: FocusTrapPassThroughCSSOptions; +} + +/** + * Custom passthrough(pt) options. + * @see {@link FocusTrapOptions.pt} + */ +export interface FocusTrapPassThroughOptions { + /** + * Uses to pass attributes to the root's DOM element. + * @see {@link FocusTrapPassThroughDirectiveOptions} + */ + root?: FocusTrapPassThroughDirectiveOptions; + /** + * Uses to pass attributes to the first focusable element's DOM element. + * @see {@link FocusTrapPassThroughDirectiveOptions} + */ + firstFocusableElement?: FocusTrapPassThroughDirectiveOptions; + /** + * Uses to pass attributes to the last focusable element's DOM element. + * @see {@link FocusTrapPassThroughDirectiveOptions} + */ + lastFocusableElement?: FocusTrapPassThroughDirectiveOptions; +} + /** * Defines options of FocusTrap. */ @@ -17,6 +100,16 @@ export interface FocusTrapOptions { * @defaultValue false */ disabled?: boolean | undefined; + /** + * When When disabled, focustrap will not focus by default. + * @defaultValue true + */ + autoFocus?: boolean | undefined; + /** + * Uses to pass attributes to DOM elements inside the component. + * @type {FocusTrapPassThroughOptions} + */ + pt?: FocusTrapPassThroughOptions; } /** diff --git a/components/lib/focustrap/FocusTrap.js b/components/lib/focustrap/FocusTrap.js index 1123e8693..2eba62cd4 100644 --- a/components/lib/focustrap/FocusTrap.js +++ b/components/lib/focustrap/FocusTrap.js @@ -1,3 +1,4 @@ +import { BaseDirective } from 'primevue/basedirective'; import { DomHandler, ObjectUtils } from 'primevue/utils'; function bind(el, binding) { @@ -37,9 +38,9 @@ function unbind(el) { function autoFocus(el, binding) { const { autoFocusSelector = '', firstFocusableSelector = '', autoFocus = false } = binding.value || {}; - let focusableElement = DomHandler.getFirstFocusableElement(el, `[autofocus]:not(.p-hidden-focusable)${autoFocusSelector}`); + let focusableElement = DomHandler.getFirstFocusableElement(el, `[autofocus]:not([data-p-hidden-focusable="true"])${autoFocusSelector}`); - autoFocus && !focusableElement && (focusableElement = DomHandler.getFirstFocusableElement(el, `:not(.p-hidden-focusable)${firstFocusableSelector}`)); + autoFocus && !focusableElement && (focusableElement = DomHandler.getFirstFocusableElement(el, `:not([data-p-hidden-focusable="true"])${firstFocusableSelector}`)); DomHandler.focus(focusableElement); } @@ -47,7 +48,7 @@ function onFirstHiddenElementFocus(event) { const { currentTarget, relatedTarget } = event; const focusableElement = relatedTarget === currentTarget.$_pfocustrap_lasthiddenfocusableelement - ? DomHandler.getFirstFocusableElement(currentTarget.parentElement, `:not(.p-hidden-focusable)${currentTarget.$_pfocustrap_focusableselector}`) + ? DomHandler.getFirstFocusableElement(currentTarget.parentElement, `:not([data-p-hidden-focusable="true"])${currentTarget.$_pfocustrap_focusableselector}`) : currentTarget.$_pfocustrap_lasthiddenfocusableelement; DomHandler.focus(focusableElement); @@ -57,7 +58,7 @@ function onLastHiddenElementFocus(event) { const { currentTarget, relatedTarget } = event; const focusableElement = relatedTarget === currentTarget.$_pfocustrap_firsthiddenfocusableelement - ? DomHandler.getLastFocusableElement(currentTarget.parentElement, `:not(.p-hidden-focusable)${currentTarget.$_pfocustrap_focusableselector}`) + ? DomHandler.getLastFocusableElement(currentTarget.parentElement, `:not([data-p-hidden-focusable="true"])${currentTarget.$_pfocustrap_focusableselector}`) : currentTarget.$_pfocustrap_firsthiddenfocusableelement; DomHandler.focus(focusableElement); @@ -69,10 +70,24 @@ function createHiddenFocusableElements(el, binding) { const createFocusableElement = (onFocus) => { const element = document.createElement('span'); - element.classList.value = 'p-hidden-accessible p-hidden-focusable'; + if (binding.instance.$primevue && binding.instance.$primevue.config && binding.instance.$primevue.config.unstyled) { + element.style.border = '0'; + element.style.clip = 'rect(0 0 0 0)'; + element.style.height = '1px'; + element.style.margin = '-1px'; + element.style.overflow = 'hidden'; + element.style.padding = '0'; + element.style.position = 'absolute'; + element.style.width = '1px'; + } else { + element.classList = 'p-hidden-accessible p-hidden-focusable'; + } + element.tabIndex = tabIndex; element.setAttribute('aria-hidden', 'true'); element.setAttribute('role', 'presentation'); + element.setAttribute('data-p-hidden-accessible', true); + element.setAttribute('data-p-hidden-focusable', true); element.addEventListener('focus', onFocus); return element; @@ -83,15 +98,17 @@ function createHiddenFocusableElements(el, binding) { firstFocusableElement.$_pfocustrap_lasthiddenfocusableelement = lastFocusableElement; firstFocusableElement.$_pfocustrap_focusableselector = firstFocusableSelector; + firstFocusableElement.setAttribute('data-pc-section', 'firstfocusableelement'); lastFocusableElement.$_pfocustrap_firsthiddenfocusableelement = firstFocusableElement; lastFocusableElement.$_pfocustrap_focusableselector = lastFocusableSelector; + lastFocusableElement.setAttribute('data-pc-section', 'lastfocusableelement'); el.prepend(firstFocusableElement); el.append(lastFocusableElement); } -const FocusTrap = { +const FocusTrap = BaseDirective.extend('focustrap', { mounted(el, binding) { const { disabled } = binding.value || {}; @@ -100,7 +117,14 @@ const FocusTrap = { bind(el, binding); autoFocus(el, binding); } + + el.setAttribute('data-pc-section', 'root'); + el.setAttribute('data-pc-name', 'focustrap'); + + BaseDirective.directiveElement = el; + BaseDirective.handleCSS('focustrap', el, binding); }, + updated(el, binding) { const { disabled } = binding.value || {}; @@ -109,6 +133,6 @@ const FocusTrap = { unmounted(el) { unbind(el); } -}; +}); export default FocusTrap;