Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion assets/js/hooks/dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ export default {
},

handleEnterOrSpace(e) {
const focusedItem = this.el.querySelector(SELECTORS.FOCUSED_MENUITEM)
// Only trust a focused item while the menu is actually open - data-focus can linger
// on an item after Escape closes the menu, since clearing it happens in the async
// phx:hide-end handler, not synchronously in hideMenu(). Without this guard, a fast
// Escape followed by Enter/Space can "click" a stale focused item instead of
// reopening the menu.
const focusedItem = this.isMenuVisible() ? this.el.querySelector(SELECTORS.FOCUSED_MENUITEM) : null

if (focusedItem && focusedItem.getAttribute('aria-disabled') !== 'true') {
// A menu item is focused - click it
Expand Down Expand Up @@ -232,16 +237,35 @@ export default {
}
},

// phx:show-start/phx:hide-end are dispatched asynchronously by LiveView's transition
// machinery, and the actual display mutation on the inner menu happens as part of that
// same internal, multi-step async completion - not synchronously with these events. A
// rapid close-then-reopen (or reopen-then-close) can call execJS(show) and execJS(hide)
// back-to-back before the previous call's internal steps finish, so their completions
// interleave and whichever happens to run last wins, regardless of which was issued
// most recently. Both handlers defensively re-assert the inner menu's display against
// our own synchronous source of truth (the wrapper, which showMenuAndFocusFirst/Last
// and hideMenu/toggleMenu control directly) so a stale completion corrects itself
// instead of leaving the inner element in the wrong state.
handleShowStart() {
const shouldBeOpen = this.isMenuVisible()
this.refs.menu.style.display = shouldBeOpen ? '' : 'none'
if (!shouldBeOpen) return

this.refs.button.setAttribute('aria-expanded', 'true')

// Setup autoUpdate to reposition on scroll/resize
this.cleanupAutoUpdate()
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.menuWrapper, () => {
this.positionMenu()
})
},

handleHideEnd() {
const shouldBeOpen = this.isMenuVisible()
this.refs.menu.style.display = shouldBeOpen ? '' : 'none'
if (shouldBeOpen) return

this.clearFocus()
this.refs.menu.removeAttribute('aria-activedescendant')
this.refs.button.setAttribute('aria-expanded', 'false')
Expand Down
Loading
Loading