From 56499c5cd0eea8ba09a5b5d19b467fd316d8ec50 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Tue, 9 Aug 2022 15:20:53 -0400 Subject: [PATCH] Trap focus when connected in `
` When the `` element is connected to the document as a child of a `
` element rendered with `[open]`, trap focus immediately. In support of this change, extract the `trapFocus` and `releaseFocus` functions, then call `trapFocus` within the `connectedCallback` if the ancestor detail is `[open]`. --- src/index.ts | 35 ++++++++++++++++++++++------------- test/test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index 55eb9b7..8bea9ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,6 +95,25 @@ function onSummaryClick(event: Event): void { } } +function trapFocus(dialog: DetailsDialogElement, details: Element): void { + const root = 'getRootNode' in dialog ? (dialog.getRootNode() as Document | ShadowRoot) : document + if (root.activeElement instanceof HTMLElement) { + initialized.set(dialog, {details, activeElement: root.activeElement}) + } + + autofocus(dialog) + ;(details as HTMLElement).addEventListener('keydown', keydown) +} + +function releaseFocus(dialog: DetailsDialogElement, details: Element): void { + for (const form of dialog.querySelectorAll('form')) { + form.reset() + } + const focusElement = findFocusElement(details, dialog) + if (focusElement) focusElement.focus() + ;(details as HTMLElement).removeEventListener('keydown', keydown) +} + function toggle(event: Event): void { const details = event.currentTarget if (!(details instanceof Element)) return @@ -102,20 +121,9 @@ function toggle(event: Event): void { if (!(dialog instanceof DetailsDialogElement)) return if (details.hasAttribute('open')) { - const root = 'getRootNode' in dialog ? (dialog.getRootNode() as Document | ShadowRoot) : document - if (root.activeElement instanceof HTMLElement) { - initialized.set(dialog, {details, activeElement: root.activeElement}) - } - - autofocus(dialog) - ;(details as HTMLElement).addEventListener('keydown', keydown) + trapFocus(dialog, details) } else { - for (const form of dialog.querySelectorAll('form')) { - form.reset() - } - const focusElement = findFocusElement(details, dialog) - if (focusElement) focusElement.focus() - ;(details as HTMLElement).removeEventListener('keydown', keydown) + releaseFocus(dialog, details) } } @@ -249,6 +257,7 @@ class DetailsDialogElement extends HTMLElement { details.addEventListener('toggle', toggle) state.details = details + if (details.hasAttribute('open')) trapFocus(this, details) updateIncludeFragmentEventListeners(details, this.src, this.preload) } diff --git a/test/test.js b/test/test.js index c90db77..9cfc6d5 100644 --- a/test/test.js +++ b/test/test.js @@ -289,6 +289,48 @@ describe('details-dialog-element', function() { }) }) + describe('connected as a child of an already [open]
element', function () { + let details + let dialog + let summary + let close + + beforeEach(function() { + const container = document.createElement('div') + container.innerHTML = ` +
+ Click + + + + +
+ ` + document.body.append(container) + + details = document.querySelector('details') + dialog = details.querySelector('details-dialog') + summary = details.querySelector('summary') + close = dialog.querySelector(CLOSE_SELECTOR) + }) + + afterEach(function() { + document.body.innerHTML = '' + }) + + it('manages focus', async function() { + assert.equal(document.activeElement, dialog) + triggerKeydownEvent(document.activeElement, 'Tab', true) + assert.equal(document.activeElement, document.querySelector(`[${CLOSE_ATTR}]`)) + triggerKeydownEvent(document.activeElement, 'Tab') + assert.equal(document.activeElement, document.querySelector(`[data-button]`)) + triggerKeydownEvent(document.activeElement, 'Tab') + assert.equal(document.activeElement, document.querySelector(`[${CLOSE_ATTR}]`)) + triggerKeydownEvent(document.activeElement, 'Tab') + assert.equal(document.activeElement, document.querySelector(`[data-button]`)) + }) + }) + describe('shadow DOM context', function() { let shadowRoot, details, summary, dialog beforeEach(function() {