Fix dropdown closing on native picker interaction in Brave and Edge#1975
Merged
Conversation
daisyUI's CSS-only dropdown relies on `:focus-within` to stay open. That
mechanism breaks when the dropdown contains a native form control like a
`<select>`: in older Chromium-based browsers (notably Brave and Edge), opening
the native picker moves focus to `<body>`, so `:focus-within` flips false and
the dropdown collapses mid-interaction. Chromium also sometimes fires a
synthesized "light-dismiss" click on the page underneath after the picker
closes, which a naive outside-click handler would treat as a real outside
click.
Introduce a `BackpexDropdown` LiveView hook that manages the open state via
the `dropdown-open` class instead of relying on `:focus-within`. The hook:
* Toggles on `mousedown` anywhere outside the menu — needed because daisyUI
applies `pointer-events: none` to the trigger while the dropdown is open,
which re-targets pointer events to the dropdown root.
* Tracks where the most recent `mousedown` landed so the document `click`
handler can distinguish real outside clicks from synthesized light-dismiss
clicks (their `mousedown` is inside the dropdown).
* Restores the open class in `updated()` so morphdom doesn't strip it on a
LiveView re-render (e.g. after `phx-change` on a filter `<select>`).
* Closes on Escape and returns focus to the trigger.
Wired up via `phx-hook="BackpexDropdown"` on the shared `.dropdown` component
so all Backpex dropdowns (filters, theme, user menu, column toggle,
multi-select) benefit uniformly.
When a user picks a value in a filter `<select>`, `phx-change` triggers a LiveView patch and morphdom updates the surrounding form. Focus on the select gets dropped during that update — even though the select node itself isn't replaced. Track the focused element inside the dropdown in `beforeUpdate()` and restore focus in `updated()` if it ended up outside the dropdown afterwards. Also add an `id` to the select filter form so morphdom has a stable identifier to match against.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses a Chromium (Brave/Edge) quirk where interacting with native form controls (e.g. <select>) inside a daisyUI dropdown causes the dropdown to close mid-interaction. It does so by moving dropdown “open” state management from CSS :focus-within to an explicit dropdown-open class controlled by a LiveView hook.
Changes:
- Added a new LiveView hook (
BackpexDropdown) that toggles/maintainsdropdown-open, handles outside-click, Escape-to-close, and attempts to preserve focus across LiveView morphdom updates. - Wired the hook into the shared dropdown core component so all Backpex dropdowns benefit.
- Added an explicit
idto the select filter<select>(supports focus restoration) and updated hook exports + built JS artifacts.
Reviewed changes
Copilot reviewed 6 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
assets/js/hooks/_dropdown.js |
New hook implementing JS-managed dropdown open/close + focus restoration. |
assets/js/hooks/index.js |
Exports the new BackpexDropdown hook. |
lib/backpex/html/core_components.ex |
Attaches phx-hook="BackpexDropdown" to the shared dropdown component root. |
lib/backpex/filters/select.ex |
Adds an explicit id to the rendered <select> to support focus restoration. |
priv/static/js/backpex.esm.js |
Bundled ESM output updated to include the new hook. |
priv/static/js/backpex.cjs.js |
Bundled CJS output updated to include the new hook. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
* Support keyboard activation: pressing Enter or Space on the trigger now
toggles the dropdown via a `keydown` listener, matching WAI-ARIA button
semantics.
* Attach document-level listeners (`mousedown`, `click`, `keydown`) only
while the dropdown is open. With multiple dropdowns on a page, closed
instances no longer add to the event-handler tax.
* Symmetric cleanup: `destroyed()` now removes the root `mousedown` and
trigger `keydown` listeners alongside the document-level ones.
Also pre-set `mousedownInside = true` in `handleRootMousedown` because the
document-level mousedown listener is attached during `open()` and therefore
doesn't catch the mousedown that triggered the open itself.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In older Chromium-based browsers (notably Brave and Microsoft Edge), interacting with a native form control inside a Backpex dropdown — most visibly the Category filter
<select>— closes the dropdown mid-interaction. Chrome stable doesn't reproduce, but anyone on a lagging Chromium fork hits it.Root cause
daisyUI's CSS-only dropdown stays open via
:focus-within. Two Chromium quirks break that mechanism:<select>inside the dropdown, the picker opens anddocument.activeElementbecomes<body>.:focus-withinflips false, daisyUI's CSS animates the dropdown closed.clickon the page underneath the picker — even though the user only interacted with the native widget. A naive outside-click handler treats this as a real outside click.There's also a daisyUI-specific subtlety that bit the toggle behavior: while the dropdown is open, daisyUI applies
pointer-events: noneto the trigger, so pointer events at the trigger's position re-target to the dropdown root. A listener attached directly to the trigger wouldn't fire for the close-on-second-click.Affects any daisyUI dropdown that contains a native picker (
<select>,<input type=\"date\">, etc.) — not just nested ones.Fix
A small LiveView hook (
BackpexDropdown) that manages the open state via thedropdown-openclass instead of relying on:focus-within:mousedownanywhere outside the menu (handles both the trigger and the re-targeted root).mousedownlanded so the documentclickhandler can distinguish real outside clicks from synthesized light-dismiss clicks (theirmousedownis inside the dropdown).updated()so morphdom doesn't strip it during a LiveView re-render (e.g. afterphx-changeon a filter<select>).Wired via
phx-hook=\"BackpexDropdown\"on the shared.dropdowncomponent, so all dropdowns (filters, theme, user menu, column toggle, multi-select) benefit uniformly.Test plan