Skip to content

Commit

Permalink
Focus trap doesn't release focus when the element is removed from the…
Browse files Browse the repository at this point in the history
… dom (#2682)

* add failing test

* Release focus when trap is removed from the DOM
  • Loading branch information
SimoTod committed Mar 1, 2022
1 parent 1b70485 commit 4b7e912
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 22 deletions.
46 changes: 26 additions & 20 deletions packages/focus/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { focusable, tabbable, isFocusable } from 'tabbable'

export default function (Alpine) {
let lastFocused
let currentFocused
let currentFocused

window.addEventListener('focusin', () => {
lastFocused = currentFocused
Expand All @@ -12,10 +12,10 @@ export default function (Alpine) {

Alpine.magic('focus', el => {
let within = el

return {
__noscroll: false,
__wrapAround: false,
__noscroll: false,
__wrapAround: false,
within(el) { within = el; return this },
withoutScrolling() { this.__noscroll = true; return this },
noscroll() { this.__noscroll = true; return this },
Expand All @@ -34,19 +34,19 @@ export default function (Alpine) {
return currentFocused
},
focusables() {
if (Array.isArray(within)) return within
if (Array.isArray(within)) return within

return focusable(within, { displayCheck: 'none' })
},
all() { return this.focusables() },
isFirst(el) {
let els = this.all()
let els = this.all()

return els[0] && els[0].isSameNode(el)
},
isLast(el) {
let els = this.all()
let els = this.all()

return els.length && els.slice(-1)[0].isSameNode(el)
},
getFirst() { return this.all()[0] },
Expand Down Expand Up @@ -85,7 +85,7 @@ export default function (Alpine) {
previous() { this.focus(this.getPrevious()) },
prev() { return this.previous() },
focus(el) {
if (! el) return
if (! el) return

setTimeout(() => {
if (! el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0')
Expand All @@ -97,7 +97,7 @@ export default function (Alpine) {
})

Alpine.directive('trap', Alpine.skipDuringClone(
(el, { expression, modifiers }, { effect, evaluateLater }) => {
(el, { expression, modifiers }, { effect, evaluateLater, cleanup }) => {
let evaluator = evaluateLater(expression)

let oldValue = false
Expand All @@ -111,6 +111,18 @@ export default function (Alpine) {
let undoInert = () => {}
let undoDisableScrolling = () => {}

const releaseFocus = () => {
undoInert()
undoInert = () => {}

undoDisableScrolling()
undoDisableScrolling = () => {}

trap.deactivate({
returnFocus: !modifiers.includes('noreturn')
})
}

effect(() => evaluator(value => {
if (oldValue === value) return

Expand All @@ -126,19 +138,13 @@ export default function (Alpine) {

// Stop trapping.
if (! value && oldValue) {
undoInert()
undoInert = () => {}

undoDisableScrolling()
undoDisableScrolling = () => {}

trap.deactivate({
returnFocus: !modifiers.includes('noreturn')
})
releaseFocus()
}

oldValue = !! value
}))

cleanup(releaseFocus)
},
// When cloning, we only want to add aria-hidden attributes to the
// DOM and not try to actually trap, as trapping can mess with the
Expand Down
22 changes: 20 additions & 2 deletions tests/cypress/integration/plugins/focus.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,25 @@ test('works with clone',
}
)

test('releases focus when x-if is destroyed',
[html`
<div x-data="{ open: false }">
<button id="1" @click="open = true">open</button>
<template x-if="open">
<div x-trap="open">
<button @click="open = false" id="2">close</button>
</div>
</template>
</div>
`],
({ get }, reload) => {
get('#1').click()
get('#2').should(haveFocus())
get('#2').click()
get('#1').should(haveFocus())
},
)

test('can trap focus with inert',
[html`
<div x-data="{ open: false }">
Expand Down Expand Up @@ -104,8 +123,7 @@ test('can trap focus with noscroll',
test('can trap focus with noreturn',
[html`
<div x-data="{ open: false }" x-trap.noreturn="open">
<input id="input" @focus="open = true" />
<input id="input" @focus="open = true">
<div x-show="open">
<button @click="open = false" id="close">close</button>
</div>
Expand Down

0 comments on commit 4b7e912

Please sign in to comment.