Skip to content

Commit

Permalink
Merge pull request #706 from KittyGiraudel/nested-dialogs-and-shadow-dom
Browse files Browse the repository at this point in the history
Account for Shadow DOM when dealing with nested dialogs
  • Loading branch information
KittyGiraudel authored Aug 16, 2024
2 parents 1a23903 + cf555c8 commit cad9bb5
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 5 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# [A11y Dialog](https://a11y-dialog.netlify.app)

This is a lightweight (1.5Kb) yet flexible script to create accessible dialog windows.
This is a lightweight (1.7Kb) yet flexible script to create accessible dialog windows.

- [Documentation ↗](https://a11y-dialog.netlify.app)
- [Demo on CodeSandbox ↗](https://codesandbox.io/s/a11y-dialog-v8-5gqfz8)
Expand Down
5 changes: 4 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"attributePosition": "auto"
},
"organizeImports": { "enabled": true },
"linter": { "enabled": true, "rules": { "recommended": true } },
"linter": {
"enabled": true,
"rules": { "recommended": true, "style": { "noParameterAssign": "off" } }
},
"javascript": {
"formatter": {
"quoteProperties": "asNeeded",
Expand Down
12 changes: 9 additions & 3 deletions src/a11y-dialog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { getActiveElement, moveFocusToDialog, trapTabKey } from './dom-utils'
import {
closest,
getActiveElement,
moveFocusToDialog,
trapTabKey,
} from './dom-utils'

export type A11yDialogEvent = 'show' | 'hide' | 'destroy'
export type A11yDialogInstance = InstanceType<typeof A11yDialog>
Expand Down Expand Up @@ -192,8 +197,9 @@ export default class A11yDialog {
*/
private bindKeypress(event: KeyboardEvent) {
// This is an escape hatch in case there are nested open dialogs, so that
// only the top most dialog gets interacted with
if (document.activeElement?.closest('[aria-modal="true"]') !== this.$el) {
// only the top most dialog gets interacted with (`closest` is basically
// `Element.prototype.closest()` accounting for Shadow DOM subtrees)
if (closest('[aria-modal="true"]', getActiveElement()) !== this.$el) {
return
}

Expand Down
19 changes: 19 additions & 0 deletions src/dom-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,22 @@ export function trapTabKey(el: HTMLElement, event: KeyboardEvent) {
event.preventDefault()
}
}

/**
* Find the closest element to the given element matching the given selector,
* accounting for Shadow DOM subtrees.
* @author Louis St-Amour
* @see: https://stackoverflow.com/a/56105394
*/
export function closest(selector: string, base: Element | null) {
function from(el: Element | Window | Document | null): Element | null {
if (!el || el === document || el === window) return null
if ((el as Slottable).assignedSlot) el = (el as Slottable).assignedSlot
return (
(el as Element).closest(selector) ||
from(((el as Element).getRootNode() as ShadowRoot).host)
)
}

return from(base)
}

0 comments on commit cad9bb5

Please sign in to comment.