-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
♿ a11y(bal-modal, bal-popup): Overlay background accessible (#1475)
* Create PR for #1423 * feat(a11y): keep focus in modal when tab * feat(a11y): run format * fix(a11y): add changeset * fix(a11y): remove debugger * fix(ally): removed todos --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Marinkov, Magdalena <[email protected]> Co-authored-by: Marco Zirkenbach <[email protected]>
- Loading branch information
1 parent
5f3e38c
commit 4290704
Showing
5 changed files
with
353 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@baloise/ds-core': patch | ||
--- | ||
|
||
**core**: modal: keep focus within modal when navigating with keyboard |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/** | ||
* This query string selects elements that | ||
* are eligible to receive focus. We select | ||
* interactive elements that meet the following | ||
* criteria: | ||
* 1. Element does not have a negative tabindex | ||
* 2. Element does not have `hidden` | ||
* 3. Element does not have `disabled` for non-Ionic components. | ||
* 4. Element does not have `disabled` or `disabled="true"` for Ionic components. | ||
* Note: We need this distinction because `disabled="false"` is | ||
* valid usage for the disabled property on bal-button. | ||
*/ | ||
export const focusableQueryString = | ||
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), .bal-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .bal-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])' | ||
|
||
/** | ||
* Focuses the first descendant in a context | ||
* that can receive focus. If none exists, | ||
* a fallback element will be focused. | ||
* This fallback is typically an ancestor | ||
* container such as a menu or overlay so focus does not | ||
* leave the container we are trying to trap focus in. | ||
* | ||
* If no fallback is specified then we focus the container itself. | ||
*/ | ||
export const focusFirstDescendant = <R extends HTMLElement, T extends HTMLElement>(ref: R, fallbackElement?: T) => { | ||
const firstInput = ref.querySelector<HTMLElement>(focusableQueryString) | ||
focusElementInContext(firstInput, fallbackElement ?? ref) | ||
} | ||
|
||
/** | ||
* Focuses the last descendant in a context | ||
* that can receive focus. If none exists, | ||
* a fallback element will be focused. | ||
* This fallback is typically an ancestor | ||
* container such as a menu or overlay so focus does not | ||
* leave the container we are trying to trap focus in. | ||
* | ||
* If no fallback is specified then we focus the container itself. | ||
*/ | ||
export const focusLastDescendant = <R extends HTMLElement, T extends HTMLElement>(ref: R, fallbackElement?: T) => { | ||
const inputs = Array.from(ref.querySelectorAll<HTMLElement>(focusableQueryString)) | ||
const lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null | ||
|
||
focusElementInContext(lastInput, fallbackElement ?? ref) | ||
} | ||
|
||
/** | ||
* Focuses a particular element in a context. If the element | ||
* doesn't have anything focusable associated with it then | ||
* a fallback element will be focused. | ||
* | ||
* This fallback is typically an ancestor | ||
* container such as a menu or overlay so focus does not | ||
* leave the container we are trying to trap focus in. | ||
* This should be used instead of the focus() method | ||
* on most elements because the focusable element | ||
* may not be the host element. | ||
* | ||
* For example, if an bal-button should be focused | ||
* then we should actually focus the native <button> | ||
* element inside of bal-button's shadow root, not | ||
* the host element itself. | ||
*/ | ||
const focusElementInContext = <T extends HTMLElement>( | ||
hostToFocus: HTMLElement | null | undefined, | ||
fallbackElement: T, | ||
) => { | ||
let elementToFocus = hostToFocus | ||
|
||
const shadowRoot = hostToFocus?.shadowRoot | ||
if (shadowRoot) { | ||
// If there are no inner focusable elements, just focus the host element. | ||
elementToFocus = shadowRoot.querySelector<HTMLElement>(focusableQueryString) || hostToFocus | ||
} | ||
|
||
if (elementToFocus) { | ||
focusVisibleElement(elementToFocus) | ||
} else { | ||
// Focus fallback element instead of letting focus escape | ||
fallbackElement.focus() | ||
} | ||
} | ||
|
||
export const focusVisibleElement = (el: HTMLElement) => { | ||
el.focus() | ||
|
||
/** | ||
* When programmatically focusing an element, | ||
* the focus-visible utility will not run because | ||
* it is expecting a keyboard event to have triggered this; | ||
* however, there are times when we need to manually control | ||
* this behavior so we call the `setFocus` method on bal-app | ||
* which will let us explicitly set the elements to focus. | ||
*/ | ||
if (el.classList.contains('bal-focusable')) { | ||
const app = el.closest('bal-app') | ||
if (app) { | ||
app.setFocus([el]) | ||
} | ||
} | ||
} |
Oops, something went wrong.