diff --git a/README.md b/README.md index 679a4ca..584b164 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,18 @@ A modal dialog opened with a <details> button. +## DEPRECATION WARNING + +This web component has been deprecated. There are a number of accessibility concerns with this approach and so we no longer recommend using this component. + +### Accessibility and Usability Concerns + +* Semantically, using a details-summary pattern for a dialog solution can be confusing for screen reader users. +* If the user performs a "find in page" operation on a website using details-dialog elements, the content on those elements will appear when they shouldn't. +* Opening the dialog does not disable scrolling on the underlying page. + +GitHub are moving towards using [a dialog Primer View Component](https://primer.style/view-components/components/alpha/dialog) which enforces certain aspects of the design (such as always having a close button and a title). + ## Installation Available on [npm](https://www.npmjs.com/) as [**@github/details-dialog-element**](https://www.npmjs.com/package/@github/details-dialog-element). ``` @@ -87,4 +99,4 @@ Browsers without native [custom element support][support] require a [polyfill][] ## License -Distributed under the MIT license. See LICENSE for details. \ No newline at end of file +Distributed under the MIT license. See LICENSE for details. diff --git a/package.json b/package.json index adc52db..10fd56a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ ], "license": "MIT", "files": [ - "dist" + "dist", + "custom-elements.json", + "vscode.html-custom-data.json" ], "prettier": "@github/prettier-config", "devDependencies": { diff --git a/src/index.ts b/src/index.ts index 58fc246..d7084c6 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) } } @@ -300,6 +308,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() {