Context
Follow-up to #28. While removing the inline-handler/window.* contract, the
filter-bar and YearPicker work surfaced that the "anchored layer that opens,
closes on Escape / outside-click" pattern is reimplemented several times with
three different mechanisms.
Current popup/dismiss implementations
- Hand-rolled (vanilla TS):
ts/elements/dropdown.ts — toggle + outside-click (click, no Escape)
ts/elements/search-select.ts — outside-click (click) + Escape (in keydown)
ts/elements/date-range-picker.ts — Escape + outside-click (mousedown)
ts/elements/year-picker.ts — Escape + outside-click via bindPopupDismiss
(popup is appended to document.body, so it passes the popup as an extra
"inside" root)
- Flowbite-driven:
Popover (data-popover-target / data-popover) and the
navbar dropdowns — separate engine, separate dismiss behaviour.
#28 already took the first incremental step: extracted Escape + outside-click into
bindPopupDismiss(options) in ts/utils.ts, adopted by date-range-picker.ts and
year-picker.ts. dropdown.ts and search-select.ts were left alone there because
they use click (not mousedown) and have differing Escape handling — migrating
them is behavioural and was out of #28's scope.
Goal
Decide whether a single popup primitive should own:
- open/close state,
- dismiss (Escape + outside-click), and
- optional anchored positioning,
and have all of the above adopt it — including reconsidering whether the two
Flowbite-driven cases (Popover, navbar dropdowns) fold in or stay on Flowbite.
Tasks (proposed)
Notes / non-goals
- This is a quality/DRY refactor, not a behaviour change for end users.
- Vendored UMD globals (
htmx, Alpine, Flowbite, Datepicker) stay classic
by design.
Context
Follow-up to #28. While removing the inline-handler/
window.*contract, thefilter-bar and YearPicker work surfaced that the "anchored layer that opens,
closes on Escape / outside-click" pattern is reimplemented several times with
three different mechanisms.
Current popup/dismiss implementations
ts/elements/dropdown.ts— toggle + outside-click (click, no Escape)ts/elements/search-select.ts— outside-click (click) + Escape (in keydown)ts/elements/date-range-picker.ts— Escape + outside-click (mousedown)ts/elements/year-picker.ts— Escape + outside-click viabindPopupDismiss(popup is appended to
document.body, so it passes the popup as an extra"inside" root)
Popover(data-popover-target/data-popover) and thenavbar dropdowns — separate engine, separate dismiss behaviour.
#28 already took the first incremental step: extracted Escape + outside-click into
bindPopupDismiss(options)ints/utils.ts, adopted bydate-range-picker.tsandyear-picker.ts.dropdown.tsandsearch-select.tswere left alone there becausethey use
click(notmousedown) and have differing Escape handling — migratingthem is behavioural and was out of #28's scope.
Goal
Decide whether a single popup primitive should own:
and have all of the above adopt it — including reconsidering whether the two
Flowbite-driven cases (
Popover, navbar dropdowns) fold in or stay on Flowbite.Tasks (proposed)
dropdown.tsandsearch-select.tsontobindPopupDismiss(normalising
clickvsmousedownand Escape behaviour).sibling helper) so hand-rolled popups stop duplicating placement math.
Popover+ navbar dropdowns: unify or keep.Notes / non-goals
htmx,Alpine,Flowbite,Datepicker) stay classicby design.