Trap focus when connected in `<details open>`
When the `<details-dialog>` element is connected to the document as a child of a `<details>` 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]`.
This commit is contained in:
Родитель
3539591c5a
Коммит
56499c5cd0
35
src/index.ts
35
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 {
|
function toggle(event: Event): void {
|
||||||
const details = event.currentTarget
|
const details = event.currentTarget
|
||||||
if (!(details instanceof Element)) return
|
if (!(details instanceof Element)) return
|
||||||
|
@ -102,20 +121,9 @@ function toggle(event: Event): void {
|
||||||
if (!(dialog instanceof DetailsDialogElement)) return
|
if (!(dialog instanceof DetailsDialogElement)) return
|
||||||
|
|
||||||
if (details.hasAttribute('open')) {
|
if (details.hasAttribute('open')) {
|
||||||
const root = 'getRootNode' in dialog ? (dialog.getRootNode() as Document | ShadowRoot) : document
|
trapFocus(dialog, details)
|
||||||
if (root.activeElement instanceof HTMLElement) {
|
|
||||||
initialized.set(dialog, {details, activeElement: root.activeElement})
|
|
||||||
}
|
|
||||||
|
|
||||||
autofocus(dialog)
|
|
||||||
;(details as HTMLElement).addEventListener('keydown', keydown)
|
|
||||||
} else {
|
} else {
|
||||||
for (const form of dialog.querySelectorAll('form')) {
|
releaseFocus(dialog, details)
|
||||||
form.reset()
|
|
||||||
}
|
|
||||||
const focusElement = findFocusElement(details, dialog)
|
|
||||||
if (focusElement) focusElement.focus()
|
|
||||||
;(details as HTMLElement).removeEventListener('keydown', keydown)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,6 +257,7 @@ class DetailsDialogElement extends HTMLElement {
|
||||||
|
|
||||||
details.addEventListener('toggle', toggle)
|
details.addEventListener('toggle', toggle)
|
||||||
state.details = details
|
state.details = details
|
||||||
|
if (details.hasAttribute('open')) trapFocus(this, details)
|
||||||
|
|
||||||
updateIncludeFragmentEventListeners(details, this.src, this.preload)
|
updateIncludeFragmentEventListeners(details, this.src, this.preload)
|
||||||
}
|
}
|
||||||
|
|
42
test/test.js
42
test/test.js
|
@ -289,6 +289,48 @@ describe('details-dialog-element', function() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('connected as a child of an already [open] <details> element', function () {
|
||||||
|
let details
|
||||||
|
let dialog
|
||||||
|
let summary
|
||||||
|
let close
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.innerHTML = `
|
||||||
|
<details open>
|
||||||
|
<summary>Click</summary>
|
||||||
|
<details-dialog>
|
||||||
|
<button data-button>Button</button>
|
||||||
|
<button ${CLOSE_ATTR}>Goodbye</button>
|
||||||
|
</details-dialog>
|
||||||
|
</details>
|
||||||
|
`
|
||||||
|
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() {
|
describe('shadow DOM context', function() {
|
||||||
let shadowRoot, details, summary, dialog
|
let shadowRoot, details, summary, dialog
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче