feat(overview): rework filter builder with editable chips and native date ranges
Editable filter chips, uniformly-sized value controls, working field-picker search, and native date-range inputs with relative presets in place of the vendored calendar — native inputs give year-jump, manual entry and keyboard accessibility for free.
This commit is contained in:
parent
c0d1550401
commit
8303a39041
23 changed files with 1905 additions and 565 deletions
|
|
@ -941,6 +941,21 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Payment-badge tooltip. A native Popover-API element (opened by the
|
||||
* HoverPopover hook), so it lives in the browser TOP LAYER and is not clipped by
|
||||
* the members table's overflow. Placement is pure CSS anchor positioning: the
|
||||
* badge carries `anchor-name`, this popover carries `position-anchor`, and
|
||||
* `position-area` drops it just below the badge, left-aligned. A `position-try`
|
||||
* fallback flips it above the badge when there is no room below. No JS
|
||||
* coordinate math is involved (unlike the older SortTooltip).
|
||||
*/
|
||||
.payment-tip {
|
||||
margin: 0;
|
||||
position-area: bottom span-right;
|
||||
position-try-fallbacks: flip-block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Vertically center the row checkbox in the sticky first column using the
|
||||
* table-native vertical-align: middle. This keeps display: table-cell intact so
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import {Socket} from "phoenix"
|
|||
import {LiveSocket} from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
import Sortable from "../vendor/sortable"
|
||||
import FilterComboboxSearch from "./hooks/filter_combobox_search"
|
||||
import HoverPopover from "./hooks/hover_popover"
|
||||
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
|
||||
|
|
@ -36,6 +38,11 @@ function getBrowserTimezone() {
|
|||
// Hooks for LiveView components
|
||||
let Hooks = {}
|
||||
|
||||
// Add-filter builder hooks (member overview field-picker search).
|
||||
Hooks.FilterComboboxSearch = FilterComboboxSearch
|
||||
// Top-layer hover/focus popover (member overview payment badge tooltip).
|
||||
Hooks.HoverPopover = HoverPopover
|
||||
|
||||
// IndeterminateCheckbox: the `indeterminate` state of a checkbox is a JS
|
||||
// property, not an HTML attribute, so it cannot be set from the server render.
|
||||
// Mirror it from the data-indeterminate attribute on mount and every update
|
||||
|
|
|
|||
33
assets/js/hooks/filter_combobox_search.js
Normal file
33
assets/js/hooks/filter_combobox_search.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// FilterComboboxSearch hook: the type-ahead search input of the add-filter
|
||||
// field picker (§1.21).
|
||||
//
|
||||
// Responsibilities:
|
||||
// * Autofocus the input when the picker opens, so the user can type
|
||||
// immediately (the cursor lands in the search field on mount). Programmatic
|
||||
// .focus() does NOT trigger the browser's :focus-visible state, so no focus
|
||||
// ring is drawn on open — the ring appears only during keyboard navigation,
|
||||
// which is exactly the desired behaviour (focus-visible on keyboard only).
|
||||
// * Guard Enter: while the listbox is open, Enter must not submit the
|
||||
// surrounding form (this replaces the old standalone ComboBox hook).
|
||||
//
|
||||
// The actual field-list filtering is done server-side (phx-change pushes the
|
||||
// query; the component narrows the descriptor list), so this hook only manages
|
||||
// focus and the Enter guard.
|
||||
const FilterComboboxSearch = {
|
||||
mounted() {
|
||||
// Defer to the next frame so focus lands after the dropdown is laid out.
|
||||
requestAnimationFrame(() => this.el.focus())
|
||||
|
||||
this.handleKeyDown = (e) => {
|
||||
const open = this.el.getAttribute("aria-expanded") === "true"
|
||||
if (e.key === "Enter" && open) e.preventDefault()
|
||||
}
|
||||
this.el.addEventListener("keydown", this.handleKeyDown)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.el.removeEventListener("keydown", this.handleKeyDown)
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterComboboxSearch
|
||||
76
assets/js/hooks/hover_popover.js
Normal file
76
assets/js/hooks/hover_popover.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// HoverPopover hook: opens a native Popover-API element on hover/keyboard focus
|
||||
// of its trigger, so the popover renders in the browser TOP LAYER and escapes
|
||||
// any ancestor `overflow` clipping (the members table forces overflow-y:auto,
|
||||
// which clips an absolutely-positioned daisyUI tooltip/dropdown-content). Unlike
|
||||
// the older SortTooltip, placement is handed to CSS anchor positioning
|
||||
// (`anchor-name` on the trigger, `position-anchor`/`position-area` on the
|
||||
// popover) — this hook does no coordinate math, it only toggles visibility.
|
||||
//
|
||||
// WCAG 2.2 AA / SC 1.4.13 (Content on Hover or Focus): the popover is shown on
|
||||
// both hover and focus, is dismissible via Escape, and is hoverable/persistent —
|
||||
// a short close delay plus the popover's own hover keep it open while the pointer
|
||||
// travels from the trigger onto the popover content.
|
||||
//
|
||||
// Expected DOM:
|
||||
// <a phx-hook="HoverPopover" data-popover-target="tip-id" style="anchor-name:--x">…</a>
|
||||
// <div id="tip-id" popover style="position-anchor:--x">…</div>
|
||||
const HoverPopover = {
|
||||
mounted() {
|
||||
this.tip = document.getElementById(this.el.dataset.popoverTarget)
|
||||
// Feature-detect: without Popover API support fall back to no visual popover
|
||||
// (the trigger's aria-label still conveys the summary to assistive tech).
|
||||
if (!this.tip || typeof this.tip.showPopover !== "function") return
|
||||
|
||||
this.open = () => {
|
||||
try {
|
||||
if (!this.tip.matches(":popover-open")) this.tip.showPopover()
|
||||
} catch (_e) {}
|
||||
}
|
||||
this.close = () => {
|
||||
try {
|
||||
if (this.tip.matches(":popover-open")) this.tip.hidePopover()
|
||||
} catch (_e) {}
|
||||
}
|
||||
this.cancelClose = () => {
|
||||
if (this.closeTimer) {
|
||||
clearTimeout(this.closeTimer)
|
||||
this.closeTimer = null
|
||||
}
|
||||
}
|
||||
this.show = () => {
|
||||
this.cancelClose()
|
||||
this.open()
|
||||
}
|
||||
this.scheduleClose = () => {
|
||||
this.cancelClose()
|
||||
this.closeTimer = setTimeout(() => this.close(), 120)
|
||||
}
|
||||
this.onKey = (e) => {
|
||||
if (e.key === "Escape") this.close()
|
||||
}
|
||||
|
||||
this.el.addEventListener("mouseenter", this.show)
|
||||
this.el.addEventListener("focus", this.show)
|
||||
this.el.addEventListener("mouseleave", this.scheduleClose)
|
||||
this.el.addEventListener("blur", this.scheduleClose)
|
||||
this.el.addEventListener("keydown", this.onKey)
|
||||
// Keep it open while the pointer is over the popover itself (hoverable).
|
||||
this.tip.addEventListener("mouseenter", this.show)
|
||||
this.tip.addEventListener("mouseleave", this.scheduleClose)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
if (!this.tip) return
|
||||
this.cancelClose()
|
||||
this.close()
|
||||
this.el.removeEventListener("mouseenter", this.show)
|
||||
this.el.removeEventListener("focus", this.show)
|
||||
this.el.removeEventListener("mouseleave", this.scheduleClose)
|
||||
this.el.removeEventListener("blur", this.scheduleClose)
|
||||
this.el.removeEventListener("keydown", this.onKey)
|
||||
this.tip.removeEventListener("mouseenter", this.show)
|
||||
this.tip.removeEventListener("mouseleave", this.scheduleClose)
|
||||
}
|
||||
}
|
||||
|
||||
export default HoverPopover
|
||||
Loading…
Add table
Add a link
Reference in a new issue