@eifi1/ui-kit 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -1
- package/dist/components/amount-input.d.ts +7 -0
- package/dist/components/autocomplete.d.ts +101 -0
- package/dist/components/autocomplete.js +260 -0
- package/dist/components/autocomplete.js.map +1 -0
- package/dist/components/calculator.d.ts +7 -0
- package/dist/components/chip.d.ts +22 -7
- package/dist/components/chip.js +14 -1
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +100 -0
- package/dist/components/choice-card.js +170 -0
- package/dist/components/choice-card.js.map +1 -0
- package/dist/components/combobox-core.d.ts +76 -6
- package/dist/components/combobox-core.js +119 -49
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +12 -2
- package/dist/components/combobox.js +42 -17
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +10 -2
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +91 -0
- package/dist/components/danger-confirm.js +181 -0
- package/dist/components/danger-confirm.js.map +1 -0
- package/dist/components/date-picker.js +19 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +84 -0
- package/dist/components/dialog-frame.js +86 -0
- package/dist/components/dialog-frame.js.map +1 -0
- package/dist/components/disclosure.d.ts +108 -0
- package/dist/components/disclosure.js +127 -0
- package/dist/components/disclosure.js.map +1 -0
- package/dist/components/entity-combobox.d.ts +17 -3
- package/dist/components/entity-combobox.js +25 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +161 -0
- package/dist/components/file-button.js +225 -0
- package/dist/components/file-button.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +72 -23
- package/dist/components/file-dropzone.js +219 -94
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/icon-picker.d.ts +72 -0
- package/dist/components/icon-picker.js +104 -0
- package/dist/components/icon-picker.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +3 -0
- package/dist/components/mini-calendar.js +4 -3
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +8 -1
- package/dist/components/modal.js +4 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.js +16 -5
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +16 -3
- package/dist/components/multi-entity-combobox.js +25 -5
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +41 -1
- package/dist/components/number-field.js +42 -10
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +35 -2
- package/dist/components/number-input.js +35 -4
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +7 -0
- package/dist/components/popover.js +4 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/search-field.d.ts +16 -0
- package/dist/components/search-field.js +29 -7
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/signature-pad.d.ts +43 -1
- package/dist/components/signature-pad.js +74 -2
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +69 -0
- package/dist/components/swatch-picker.js +75 -0
- package/dist/components/swatch-picker.js.map +1 -0
- package/dist/components/tile-radio.d.ts +50 -0
- package/dist/components/tile-radio.js +140 -0
- package/dist/components/tile-radio.js.map +1 -0
- package/dist/components/toggle-group.d.ts +27 -5
- package/dist/components/toggle-group.js +22 -15
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +18 -0
- package/dist/components/trigger-aria.js +25 -0
- package/dist/components/trigger-aria.js.map +1 -0
- package/dist/components/ui.d.ts +164 -18
- package/dist/components/ui.js +207 -34
- package/dist/components/ui.js.map +1 -1
- package/dist/i18n/defaults.d.ts +7 -0
- package/dist/i18n/defaults.js +13 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +38 -5
- package/dist/i18n/kit-labels.js +12 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/index.d.ts +16 -7
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/table-text.d.ts +127 -0
- package/dist/lib/table-text.js +82 -0
- package/dist/lib/table-text.js.map +1 -0
- package/dist/rhf/form.d.ts +79 -0
- package/dist/rhf/form.js +143 -0
- package/dist/rhf/form.js.map +1 -0
- package/dist/rhf.d.ts +4 -0
- package/dist/rhf.js +3 -0
- package/dist/rhf.js.map +1 -0
- package/dist/shell/app-shell.js +3 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/table-text.d.ts +1 -0
- package/dist/table-text.js +3 -0
- package/dist/table-text.js.map +1 -0
- package/package.json +14 -1
- package/src/components/autocomplete.tsx +425 -0
- package/src/components/chip.tsx +43 -7
- package/src/components/choice-card.tsx +305 -0
- package/src/components/combobox-core.tsx +228 -58
- package/src/components/combobox.tsx +58 -21
- package/src/components/currency-select.tsx +16 -2
- package/src/components/danger-confirm.tsx +286 -0
- package/src/components/date-picker.tsx +31 -6
- package/src/components/dialog-frame.tsx +179 -0
- package/src/components/disclosure.tsx +259 -0
- package/src/components/entity-combobox.tsx +41 -6
- package/src/components/file-button.tsx +431 -0
- package/src/components/file-dropzone.tsx +323 -117
- package/src/components/icon-picker.tsx +181 -0
- package/src/components/mini-calendar.tsx +7 -3
- package/src/components/modal.tsx +10 -2
- package/src/components/month-picker.tsx +22 -4
- package/src/components/multi-entity-combobox.tsx +40 -6
- package/src/components/number-field.tsx +86 -10
- package/src/components/number-input.tsx +79 -2
- package/src/components/popover.tsx +4 -1
- package/src/components/search-field.tsx +49 -6
- package/src/components/signature-pad.tsx +112 -0
- package/src/components/swatch-picker.tsx +141 -0
- package/src/components/tile-radio.tsx +228 -0
- package/src/components/toggle-group.tsx +54 -18
- package/src/components/trigger-aria.ts +42 -0
- package/src/components/ui.tsx +427 -40
- package/src/i18n/defaults.ts +12 -1
- package/src/i18n/kit-labels.tsx +45 -5
- package/src/index.ts +19 -0
- package/src/lib/table-text.ts +265 -0
- package/src/rhf/form.tsx +300 -0
- package/src/rhf.ts +9 -0
- package/src/shell/app-shell.tsx +3 -1
- package/src/table-text.ts +8 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/combobox.tsx"],"sourcesContent":["import { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { ChevronDown, X } from \"lucide-react\";\nimport { FieldLabel, FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport { useActiveOptionScroll, type ComboOption } from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n// One look for both combobox flavors below — the suggestion list and its rows\n// must stay pixel-identical between the free-text and the id-keyed variant.\nconst LIST_CLASS =\n \"max-h-64 overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--bg-surface)] py-1 shadow-lg\";\n\n/**\n * \"This focus came from a mouse button that is not the left one\" — for the two\n * fields below, which open their list ON FOCUS.\n *\n * Keksdose live #309 rework: *\"When clicking the mouse back button now while hovering\n * one of the selects it opens the select as long as I am holding the button down and\n * closes select after releasing.\"* A mouse's BACK button focuses whatever it is\n * pressed over, exactly as the left one does — the browser only reserves the\n * NAVIGATION for itself — so a field that opens on focus opened a list for a gesture\n * that means \"go back\", and the release then navigated out from under it. Measured on\n * his own budget: `mousePressed button=back` over the transfer form's account field\n * left `aria-expanded=true`, and the release took the whole add card with it.\n *\n * The press is recorded and read by the focus that the SAME press causes: focus is\n * mousedown's default action, dispatched inside the same task, so the flag is always\n * read before the `setTimeout` below can drop it. And it is always dropped — a press\n * that focuses nothing (the pointer was over a disabled field, the button was\n * released elsewhere) must not leave a latch that swallows the next Tab.\n *\n * Only the OPEN is suppressed, never the navigation: Back still goes back, which is\n * the whole of what the button was pressed for.\n */\nfunction usePrimaryPressOnly() {\n const auxPress = useRef(false);\n return {\n onMouseDown: (e: { button: number }) => {\n if (e.button === 0) return;\n auxPress.current = true;\n setTimeout(() => {\n auxPress.current = false;\n }, 0);\n },\n /** True while handling the focus a non-primary press just caused. */\n fromAuxButton: () => auxPress.current,\n };\n}\n\n/**\n * The desktop suggestion list, PORTALLED and anchored to the field.\n *\n * It used to be an `absolute` `<ul>` inside the field's own wrapper, and that is\n * Keksdose live #295: *\"Category select inside the list is not readable. Some sort\n * of z indexes issue?\"*. It was not z-index — a stacking context can be out-ranked,\n * but `overflow` cannot be argued with. The receipt's line table sits in a card\n * carrying `overflow-clip`, so a list opened from the last visible row was cut off\n * at the card's edge: measured at 256px tall with 150px of it painted.\n *\n * Every other panel in this package already learned this — `DropdownPanel`'s\n * `anchorRef` form, the calculator popover, the tooltip — so this reuses the same\n * hook rather than inventing a second placement. What that buys beyond the clip:\n * the list flips above the field when there is no room below, and it caps its own\n * height against the VISIBLE viewport, which on a phone means the on-screen\n * keyboard (see {@link useAnchoredPanel}).\n *\n * `panelRef` is not optional plumbing. Portalled, the list is no longer a\n * descendant of the wrapper, so {@link useDropdown}'s outside-click test answers\n * \"outside\" for a click on the list itself — the first option a user picked would\n * close the dropdown having picked nothing.\n */\nfunction SuggestionList({\n id,\n anchorRef,\n panelRef,\n children,\n}: {\n /** What the field's `aria-controls` names. */\n id: string;\n anchorRef: RefObject<HTMLElement | null>;\n panelRef: RefObject<HTMLDivElement | null>;\n children: ReactNode;\n}) {\n const { rect, top, maxHeight } = useAnchoredPanel(anchorRef, true, { preferredHeight: 256 });\n if (!rect) return null;\n return createPortal(\n <div\n ref={panelRef}\n // z-50, not the old z-30: the list is a child of <body> now, so it is\n // competing with the app's own overlays rather than with its own siblings.\n className=\"fixed z-50\"\n style={{ top, left: rect.left, width: rect.width }}\n >\n <ul id={id} role=\"listbox\" className={LIST_CLASS} style={{ maxHeight }}>\n {children}\n </ul>\n </div>,\n document.body,\n );\n}\n\nconst rowClass = (isActive: boolean) =>\n cn(\n \"block w-full truncate px-3 py-1.5 text-left text-sm text-[var(--text-primary)]\",\n isActive ? \"bg-[var(--bg-active)]\" : \"hover:bg-[var(--bg-hover)]\",\n );\n\n/**\n * Four of the div's own attributes are omitted because this component already owns\n * the name, with a different meaning: `id` is the INPUT's (a caller labels or\n * automates the field, not the box around it), and `onChange`/`onBlur`/`onSubmit`\n * are the field's — a value, a departure, and \"I mean what I typed\" — rather than\n * the DOM events of the wrapper. Everything else a wrapper needs reaches the root.\n */\nexport interface ComboboxProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\" | \"onBlur\" | \"onSubmit\"> {\n value: string;\n onChange: (v: string) => void;\n /** Suggestion pool (e.g. existing payee names). */\n options: string[];\n label?: string;\n id?: string;\n placeholder?: string;\n className?: string;\n /** Required and unanswered — see {@link Input}'s `invalid`. Set on the `<input>`\n * itself rather than on the wrapper, which is what let Keksdose's account picker\n * delete the `[&_input]:…` copy of {@link FIELD_INVALID} it had been carrying\n * because this component had no `invalid` of its own. */\n invalid?: boolean;\n /** Heading an option belongs under. Supplying it makes this list read exactly\n * like {@link InlineEntityCombobox}'s — one heading per group with its rows\n * indented beneath — instead of a flat list (feedback #136 rework: the payee\n * field sat next to the newly-grouped category field and no longer matched).\n * Omit for a plain list. */\n groupBy?: (option: string) => string;\n /**\n * Something to show at the far end of an option's row — a badge saying what the\n * option IS, as distinct from what it is called.\n *\n * Keksdose's category-name field is the case: its proposals carry per-locale\n * names, so picking one stores a category that follows the UI language, and\n * picking a look-alike custom name does not. That difference is invisible in the\n * label and decides what the row DOES, so the row has to show it. Returning\n * `null` for an option renders nothing and costs no layout.\n *\n * Not rendered in the create row, which by definition names nothing in the pool.\n */\n optionAdornment?: (option: string) => ReactNode;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`, open\n * the list with it. A click-the-value inline editor needs it: the cell the user\n * clicked names the field the caret should land in. {@link InlineEntityCombobox}\n * has had it since live #218. */\n autoFocus?: boolean;\n /** Focus genuinely LEFT the field. Not fired by picking a row — the rows suppress\n * `mousedown`, so the input never blurs — which is what makes it usable as a\n * \"close the inline editor\" signal. */\n onBlur?: () => void;\n /** Enter pressed with no row highlighted, i.e. \"I mean what I typed\". The list\n * closes either way; this is for a caller whose Enter also submits a row editor. */\n onSubmit?: () => void;\n /** Rows to offer when the field is empty. 8 on a dropdown, where that is all\n * that fits; the phone sheet asks for more because it has a screen. */\n maxSuggestions?: number;\n searchPlaceholder?: string;\n closeLabel?: string;\n /**\n * Label for the row that COMMITS a value the list does not contain — e.g.\n * `(v) => \\`Add \"${v}\" as payee\\``. Supplying it turns the free text into\n * something you confirm rather than something you leave behind.\n *\n * Keksdose live #212: *\"For the payee input have an apply button, when input is\n * placed like 'add xxx as payee' to confirm. Now it can be put in and closed with\n * the right top X, which seems not intuitive.\"* On the phone sheet the search box\n * IS the value, so typing a brand-new name and dismissing the sheet did commit it —\n * but the only control on offer was the close X, which reads as \"discard\". A named\n * affordance says what the typing did.\n *\n * Omit and the row is not rendered, which is the right default for a list whose\n * values are all supposed to already exist.\n */\n createLabel?: (value: string) => string;\n}\n\n/**\n * Free-text combobox that looks like the shared {@link Select} (same FIELD_BASE\n * styling, chevron and static floating label) but lets the user type a value\n * that isn't in the list — e.g. naming a brand-new payee. Picking from the\n * filtered suggestion list fills the value; typing keeps whatever was entered.\n *\n * Use this instead of an `<Input list=\"…\">` + `<datalist>` so the control is\n * visually and behaviourally consistent with the other dropdowns (feedback\n * #230 — the payee field looked/behaved differently from every other select).\n */\nexport function Combobox({\n value,\n onChange,\n options,\n label,\n id,\n placeholder,\n className,\n groupBy,\n maxSuggestions,\n searchPlaceholder,\n closeLabel,\n createLabel,\n invalid,\n optionAdornment,\n autoFocus,\n onBlur,\n onSubmit,\n \"aria-label\": ariaLabel,\n ...rest\n}: ComboboxProps) {\n const generated = useId();\n const fieldId = id ?? generated;\n // Derived from the GENERATED id, never from `id`: a caller's id is theirs to\n // collide with, and `aria-controls` has to resolve.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n // `backCloses` is the desktop half of live #309: the phone's list IS a\n // {@link PickerSheet}, which registers its own history entry, so registering a\n // second one here would cost two Back presses to dismiss one sheet.\n const isPhone = useMediaQuery(PHONE_QUERY, false);\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone });\n const primaryOnly = usePrimaryPressOnly();\n const [active, setActive] = useState(-1);\n // A phone opens the list as a full-screen sheet with its own input, the way a\n // native <select> does — live #200: \"Paid as full screen dialog with input.\n // Similar to the account select that already appears as full screen.\" The\n // anchored list stays for pointer devices, where it is the better shape.\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — the inner wrapper, which hugs the input,\n // not the outer one (a grid item that can be taller than the field).\n const fieldRef = useRef<HTMLDivElement>(null);\n\n /**\n * Is the text in the field a QUERY, or just what was picked last time?\n *\n * Keksdose dev#549, filed against the category-name field: *\"Currently it acts kind\n * of a filter and clicking again does not open anything since the previous text is\n * still there likely filtering all other options out.\"* A combobox whose filter is\n * its own value can only ever re-offer the answer it already has — after picking\n * \"REWE\" the list reopens holding one row, the one you are looking at, and changing\n * your mind means clearing the field by hand first.\n *\n * So the text filters only while it is being TYPED; opening the list, by focus or\n * by clicking the field again, puts the whole pool back. {@link InlineEntityCombobox}\n * below has had this since it was written (see its `typedQuery`) and the app's\n * category-name field grew its own copy to get it — this one, the payee field, was\n * the one left with the bug.\n *\n * Not derived from \"does the text exactly match an option\": a custom value that\n * happens to collide with one would then behave differently from every other, and\n * what is being tracked is what the user just did, which is not a property of the\n * string.\n */\n const [typing, setTyping] = useState(false);\n\n const query = typing ? value.trim().toLowerCase() : \"\";\n const matches = useMemo(() => {\n const seen = new Set<string>();\n const uniq = options.filter((o) => o && !seen.has(o) && seen.add(o));\n let ranked: string[];\n const cap = maxSuggestions ?? (isPhone ? 50 : 8);\n if (!query) {\n ranked = uniq.slice(0, cap);\n } else {\n // Prefix matches first, then substring matches — most relevant on top.\n const starts = uniq.filter((o) => o.toLowerCase().startsWith(query));\n const contains = uniq.filter(\n (o) => !o.toLowerCase().startsWith(query) && o.toLowerCase().includes(query),\n );\n ranked = [...starts, ...contains].slice(0, cap);\n }\n if (!groupBy) return ranked;\n // Keep each group contiguous so a heading appears once instead of every time\n // the ranking interleaves two groups — while preserving \"best match first\":\n // a Map keeps insertion order, so groups come out ordered by their best-ranked\n // member and members keep their rank order inside the group.\n const byGroup = new Map<string, string[]>();\n for (const o of ranked) {\n const key = groupBy(o);\n if (!byGroup.has(key)) byGroup.set(key, []);\n byGroup.get(key)!.push(o);\n }\n return [...byGroup.values()].flat();\n }, [options, query, groupBy, maxSuggestions, isPhone]);\n\n /**\n * Which rows OPEN a group, and with what heading — computed once for both lists.\n *\n * The desktop list had this as `matches[i - 1]`, which is free; the sheet had it as\n * `matches[matches.indexOf(o) - 1]`, which is a scan of the list per row and another\n * per heading test. That is quadratic in the sheet's own cap of 50, on the branch\n * that runs on the slowest hardware we ship to — and it answers the wrong question\n * besides: `indexOf` finds the FIRST row with that string, so a duplicated option\n * would have compared against a neighbour it is not next to. (`matches` de-duplicates\n * today; that is a property of the memo above, not of the row being rendered.)\n */\n const groupHeadings = useMemo(() => {\n if (!groupBy) return null;\n const heads = new Map<number, string>();\n let prev: string | undefined;\n matches.forEach((o, i) => {\n const group = groupBy(o);\n if (group != null && group !== prev) heads.set(i, group);\n prev = group;\n });\n return heads;\n }, [matches, groupBy]);\n\n const commit = (v: string) => {\n onChange(v);\n setOpen(false);\n setActive(-1);\n // What is in the field is now an ANSWER, not a query — so reopening offers the\n // whole pool again rather than a one-row filter of the value just chosen.\n setTyping(false);\n };\n\n // Offered only when the typed text is genuinely NOT in the pool: an exact\n // (case-insensitive) match is an existing entry the list is already showing, and a\n // second way to pick it would be noise. Compared against `options`, not `matches` —\n // `matches` is capped, so a pool of 300 payees would otherwise offer to \"add\" one\n // that exists but fell off the end of the list.\n // What the field claims the keyboard is on. Only on a pointer device: the phone\n // renders its rows in a {@link PickerSheet} whose own search box owns focus, and a\n // field pointing at an id that is not there is worse than one pointing nowhere.\n const activeId = !isPhone && active >= 0 && active < matches.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const typed = value.trim();\n const createRow =\n createLabel && typed.length > 0 && !options.some((o) => o.toLowerCase() === typed.toLowerCase())\n ? createLabel(typed)\n : null;\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id, a form-level\n // `aria-describedby`. Not the NAME: that belongs on the <input> below, which is\n // the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n {/* The chevron centers against this inner wrapper, which hugs the input.\n The outer div can be taller than the input (as a grid item it\n stretches to the row height, e.g. next to the editor's category cell\n with its split button), which used to drag a top-1/2 chevron down to\n the input's bottom edge (feedback #248). */}\n <div ref={fieldRef} className=\"relative\">\n <input\n id={fieldId}\n value={value}\n placeholder={placeholder}\n // The visual label is a floating <span>, not a <label for>, so fall back to\n // it for the accessible name — otherwise the field announces only what is\n // typed in it (dev#477, applied to all three entity fields of a transaction\n // form at once).\n aria-label={ariaLabel ?? label}\n role=\"combobox\"\n aria-expanded={open}\n // The list this field is the mouth of. Required by the role, and the half\n // that was missing: the field said it was expanded and never said what it\n // had expanded, so a reader had no way from the box to the options\n // (ESLint's `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n autoFocus={autoFocus}\n onBlur={onBlur}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={() => {\n // A back/forward mouse button focuses this field on its way to\n // navigating; it is not a request to open anything (live #309 rework).\n if (primaryOnly.fromAuxButton()) return;\n setOpen(true);\n setTyping(false);\n // The sheet carries its own input, so the field behind it must not also\n // pull up the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n // A CLICK as well as focus — the other half of dev#549. Picking a suggestion\n // closes the list without moving focus (the rows suppress `mousedown` on\n // purpose, so the input never blurred), which means clicking the field again\n // fires no `focus` event at all and the list stayed shut. \"Does not open\n // anything\" was literally true.\n onClick={() => {\n setOpen(true);\n setTyping(false);\n }}\n // `inputMode=\"none\"` rather than readOnly: the field must not look\n // uneditable (FIELD_BASE greys a read-only field since dev#468) and must\n // still take focus — it just has no keyboard of its own, the same trick\n // the amount field uses for the numpad.\n inputMode={isPhone ? \"none\" : undefined}\n onChange={(e) => {\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n setTyping(true);\n }}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => Math.min(i + 1, matches.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n if (open && active >= 0 && active < matches.length) {\n e.preventDefault();\n commit(matches[active]);\n } else {\n // No row highlighted: the typed text is the answer. `preventDefault`\n // only when a caller is taking Enter, so a plain form submit is\n // otherwise left alone.\n if (onSubmit) e.preventDefault();\n setOpen(false);\n onSubmit?.();\n }\n } else if (e.key === \"Escape\") {\n setOpen(false);\n setActive(-1);\n } else if (e.key === \"Tab\") {\n // Closes, and lets the browser take the Tab: focus is in THIS input,\n // which is staying, so there is nothing to catch — unlike the pickers\n // whose focus sits inside a portalled panel.\n setOpen(false);\n setActive(-1);\n }\n // Home/End are deliberately absent. The APG gives them to the list only\n // where the combobox is not editable; here the text IS the value, a\n // payee name is long enough to want the caret moved to its start, and\n // the desktop list is capped at 8 rows — so jumping it would be worth\n // almost nothing and would cost the one gesture that field is used with.\n }}\n className={cn(FIELD_BASE, label !== undefined && FIELD_FLOATING_PAD, \"pr-9\", invalid && FIELD_INVALID)}\n />\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input.\n e.preventDefault();\n setOpen((o) => !o);\n }}\n className=\"absolute right-2.5 top-1/2 size-4 -translate-y-1/2 cursor-pointer text-[var(--text-placeholder)]\"\n />\n </div>\n {isPhone && (\n <PickerSheet\n open={open}\n onClose={() => setOpen(false)}\n title={label}\n // The sheet's input IS the field: this is a free-text control, so what is\n // typed here is the value (a brand-new payee is just a name nothing\n // matches), and the list below narrows as it changes.\n query={value}\n onQueryChange={(v) => {\n onChange(v);\n setTyping(true);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {createRow && (\n // `role=\"option\"` rides the BUTTON, not the <li> around it: an option\n // may not contain a separately focusable control, and the button is\n // what a pointer presses. Same shape `CommandPalette` has always had.\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n onClick={() => commit(typed)}\n className={cn(SHEET_ROW_CLASS, \"font-medium text-[var(--brand)]\")}\n >\n {createRow}\n </button>\n </li>\n )}\n {matches.map((o, i) => {\n const group = groupHeadings?.get(i);\n return (\n <Fragment key={o}>\n {group !== undefined && (\n <li\n role=\"presentation\"\n className=\"px-4 pb-0.5 pt-3 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]\"\n >\n {group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o === value}\n onClick={() => commit(o)}\n className={cn(\n SHEET_ROW_CLASS,\n optionAdornment && \"flex items-center justify-between gap-2\",\n )}\n >\n {optionAdornment ? <span className=\"truncate\">{o}</span> : o}\n {optionAdornment?.(o)}\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n </PickerSheet>\n )}\n {!isPhone && open && (matches.length > 0 || createRow) && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {createRow && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows below: the input's blur would otherwise\n // close the list before the click landed.\n e.preventDefault();\n commit(typed);\n }}\n className={cn(rowClass(false), \"font-medium text-[var(--brand)]\")}\n >\n {createRow}\n </button>\n </li>\n )}\n {matches.map((o, i) => {\n // Heading at each group boundary only — `matches` is group-contiguous, so\n // the boundaries are a property of the list rather than of the branch\n // drawing it. See `groupHeadings`.\n const group = groupHeadings?.get(i);\n return (\n <Fragment key={o}>\n {group !== undefined && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, not highlighted. The row the arrows are on is named by\n // the field's `aria-activedescendant`; saying \"selected\" for it\n // would tell a reader that the row they are passing over is\n // already the answer.\n aria-selected={o === value}\n // Focus stays in the field — that is what\n // `aria-activedescendant` is for — so the rows are not tab stops.\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the blur from the input firing first\n // doesn't close the list before the selection registers.\n e.preventDefault();\n commit(o);\n }}\n onMouseEnter={() => setActive(i)}\n className={cn(\n rowClass(i === active),\n group != null && \"pl-6\",\n // A row with a badge is a flex row so the label truncates and the\n // badge keeps its width; without one it stays the plain block the\n // other lists render, so nothing shifts for callers that pass none.\n optionAdornment && \"flex items-center justify-between gap-2\",\n )}\n >\n {optionAdornment ? <span className=\"truncate\">{o}</span> : o}\n {optionAdornment?.(o)}\n </button>\n </li>\n </Fragment>\n );\n })}\n </SuggestionList>\n )}\n </div>\n );\n}\n\n/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}\n * for why the div's spellings of them are omitted. */\nexport interface InlineEntityComboboxProps<V extends string | number>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\"> {\n /** Selected option id, or null when nothing is selected. */\n value: V | null;\n /** Required and unanswered — see {@link Combobox}'s `invalid`. */\n invalid?: boolean;\n /** A picked/typed option emits its id; emptying the field emits `null`. */\n onChange: (v: V | null) => void;\n options: ComboOption<V>[];\n label?: string;\n id?: string;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`\n * below, open the list with it. A click-the-value editor needs it: the cell\n * the user clicked names the field the editor should land the caret in, and\n * without a passthrough this component's fixed prop list puts the `<input>`\n * out of a caller's reach entirely (Keksdose feedback live #218). */\n autoFocus?: boolean;\n /** Phone sheet only — the anchored list has the field itself to type in. Falls\n * back to `placeholder`, then to `combobox.search` from the {@link UiKitProvider}. */\n searchPlaceholder?: string;\n /** Phone sheet only: a full screen showing nothing has to say why. The anchored\n * list simply does not open. Default: `combobox.noResults` from the provider. */\n emptyLabel?: string;\n closeLabel?: string;\n /** Offer a clear \"×\" in place of the chevron whenever something is selected.\n *\n * Emptying the text already clears (see {@link reconcile}), and on a desktop that\n * is the fast way. It is not a way at all on a PHONE: touching the field opens the\n * full-screen sheet, which covers the very input the text would have been deleted\n * from — so a field that had been answered could not be UNanswered by any gesture\n * the screen offered (Keksdose live #236, filed from a phone). The \"×\" is the one\n * affordance both shells share, and it clears without opening anything. */\n clearable?: boolean;\n clearLabel?: string;\n}\n\n/**\n * The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input\n * with the shared field styling, chevron and inline as-you-type filtering) but\n * the value is an option id, not free text. Built so an entity picker can sit\n * next to a free-text combobox without any visible difference — a button-trigger\n * panel picker ({@link EntityCombobox}) never matches an input field exactly.\n *\n * Text-vs-value reconciliation: the input's text is transient. Picking a row or\n * typing an exact (unique) label commits that option; emptying the text commits\n * a clear; anything else reverts to the selected option's label on blur/Escape.\n *\n * **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}\n * beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto\n * this component and must not lose its sheet). The sheet carries its OWN query state\n * rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a\n * sheet that emptied it to show the full list would read as \"the user cleared the\n * field\" the moment it closed.\n */\nexport function InlineEntityCombobox<V extends string | number>({\n value,\n onChange,\n options,\n label,\n id,\n placeholder,\n className,\n disabled,\n autoFocus,\n searchPlaceholder,\n emptyLabel,\n closeLabel,\n clearable,\n clearLabel,\n invalid,\n \"aria-label\": ariaLabel,\n ...rest\n}: InlineEntityComboboxProps<V>) {\n const generated = useId();\n const fieldId = id ?? generated;\n // See the twin above on why these hang off the generated id.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const isPhone = useMediaQuery(PHONE_QUERY, false);\n // See the twin above: the phone sheet owns its own Back entry (live #309).\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone });\n const primaryOnly = usePrimaryPressOnly();\n const [active, setActive] = useState(-1);\n // null = not editing → the input shows the selected option's label.\n const [text, setText] = useState<string | null>(null);\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — see {@link SuggestionList}.\n const fieldRef = useRef<HTMLDivElement>(null);\n // The sheet's search box. Starts empty on every open, so a field that already\n // holds a value still offers the whole list — the shape a native <select> has\n // on a phone, and what the anchored panel gets from `query` below.\n const [sheetQuery, setSheetQuery] = useState(\"\");\n // Props first, provider second — see `EntityCombobox`. `emptyLabel` used to render\n // nothing when omitted, which left a phone user staring at a blank full-screen\n // sheet; now an omitted one says \"No results\" in the provider's language.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n });\n\n const selected = useMemo(\n () => (value == null ? null : (options.find((o) => o.value === value) ?? null)),\n [options, value],\n );\n const shown = text ?? selected?.label ?? \"\";\n // Nothing selected has nothing to clear, and the chevron comes back — the field\n // keeps exactly one trailing control, so the \"×\" never crowds the value it sits on.\n const showClear = Boolean(clearable && value != null && !disabled);\n\n // Focusing select-alls the current label; filtering only kicks in once the\n // text actually differs from it, so an already-filled field still opens on\n // the FULL list instead of a single-row \"filter\" of its own value.\n const typedQuery =\n text !== null && text !== (selected?.label ?? \"\") ? text.trim().toLowerCase() : \"\";\n const query = isPhone ? sheetQuery.trim().toLowerCase() : typedQuery;\n const matches = useMemo(() => {\n let ranked = options;\n if (query) {\n const hit = (s: string | undefined) => s?.toLowerCase().includes(query) ?? false;\n const starts = options.filter((o) => o.label.toLowerCase().startsWith(query));\n const rest = options.filter(\n (o) =>\n !o.label.toLowerCase().startsWith(query) &&\n (hit(o.label) || hit(o.sublabel) || hit(o.group)),\n );\n ranked = [...starts, ...rest];\n }\n if (!ranked.some((o) => o.group)) return ranked;\n // Keep each group contiguous so the headings below appear once instead of\n // re-appearing every time the ranking interleaves two groups — while preserving\n // \"best match first\": a Map keeps insertion order, so groups come out ordered by\n // their best-ranked member and members keep their rank order inside the group.\n const blocks = new Map<string, ComboOption<V>[]>();\n for (const o of ranked) {\n const key = o.group ?? \"\";\n if (!blocks.has(key)) blocks.set(key, []);\n blocks.get(key)!.push(o);\n }\n return [...blocks.values()].flat();\n }, [options, query]);\n\n // Pointer-device only: the phone's rows live in a {@link PickerSheet} whose own\n // search box holds focus, so the field behind it must not claim to be pointing at\n // one of them.\n const activeId = !isPhone && active >= 0 && active < matches.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n setSheetQuery(\"\");\n };\n const commit = (o: ComboOption<V>) => {\n if (o.value !== value) onChange(o.value);\n setText(null);\n close();\n };\n /** Turn loose text into a decision: empty clears, an exact label match that names\n * ONE entity commits, anything else reverts to the selected label.\n *\n * Unique by VALUE, not by row. An option may deliberately appear twice — Keksdose\n * repeats recently-used categories in a \"Recent\" group at the top (live #203) — and\n * counting rows made every such option uncommittable: two hits, so nothing fired and\n * the field reverted to whatever was selected before. Typing a category you had just\n * used, then tabbing away, silently discarded it, and the more categories you used\n * the more of them stopped working. Two rows naming the same id are not an ambiguity;\n * two ids sharing a label are. */\n const reconcile = () => {\n if (text !== null) {\n const q = text.trim();\n if (!q) {\n if (value != null) onChange(null);\n } else {\n const hits = options.filter((o) => o.label.toLowerCase() === q.toLowerCase());\n const ids = new Set(hits.map((h) => h.value));\n if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);\n }\n setText(null);\n }\n close();\n };\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id, a form-level\n // `aria-describedby`. Not the NAME: that belongs on the <input> below, which is\n // the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n {/* Inner wrapper for chevron centering — same reasoning as Combobox above.\n It is also what the portalled list anchors to. */}\n <div ref={fieldRef} className=\"relative\">\n <input\n id={fieldId}\n value={shown}\n placeholder={placeholder}\n // The visual label is a floating <span>, not a <label for>, so without this\n // the field has NO accessible name — it announces its value and nothing\n // else. Just the label, never \"label: value\" the way a trigger button has\n // to compose it: an input already exposes its value separately.\n aria-label={ariaLabel ?? (typeof label === \"string\" ? label : undefined)}\n role=\"combobox\"\n aria-expanded={open}\n // Required by the role, and the half that was missing: the field said it\n // was expanded and never said what it had expanded (ESLint's\n // `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n disabled={disabled}\n autoFocus={autoFocus}\n // `inputMode=\"none\"` rather than readOnly, for the same reason Combobox\n // above gives: the sheet carries the keyboard, and a readOnly field would\n // take FIELD_BASE's settled look on a field that is perfectly editable.\n inputMode={isPhone ? \"none\" : undefined}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={(e) => {\n // See {@link usePrimaryPressOnly}: a back/forward button lands here on\n // its way to navigating, and neither the list nor the select-all is\n // anything it asked for (live #309 rework).\n if (primaryOnly.fromAuxButton()) return;\n setText(shown);\n e.currentTarget.select();\n setOpen(true);\n // The sheet has its own input; the field behind it must not also pull up\n // the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n onBlur={() => {\n // On a phone the blur is the SHEET taking focus, not the user leaving the\n // field — reconciling there would close the sheet the instant it opened.\n if (!isPhone) reconcile();\n }}\n onChange={(e) => {\n setText(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => Math.min(i + 1, matches.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n if (open && active >= 0 && active < matches.length) {\n e.preventDefault();\n commit(matches[active]);\n } else {\n reconcile();\n }\n } else if (e.key === \"Escape\") {\n setText(null);\n close();\n } else if (e.key === \"Tab\") {\n // Focus is in THIS input and stays there, so the browser's own Tab is\n // left alone; all that is needed is that the list stop covering what\n // the user is tabbing to. `reconcile` rather than `close`, because\n // leaving the field is exactly when loose text has to be judged.\n reconcile();\n }\n // Home/End stay with the caret — see the note in {@link Combobox}: this\n // field is editable, and its text is what `reconcile` judges.\n }}\n className={cn(FIELD_BASE, label !== undefined && FIELD_FLOATING_PAD, \"pr-9\", invalid && FIELD_INVALID)}\n />\n {showClear ? (\n <button\n type=\"button\"\n // Out of the tab order, like the clear on `EntityCombobox`: the keyboard\n // already clears this field by selecting its text and deleting, and a\n // second stop between every picker and the next field is a worse trade\n // than the one gesture it saves.\n tabIndex={-1}\n aria-label={labels.clear}\n // preventDefault, exactly as the chevron does: without it the press\n // focuses the input, which on a phone opens the sheet over the field the\n // press was clearing.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n setText(null);\n close();\n onChange(null);\n }}\n className={cn(\n \"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)]\",\n \"hover:text-[var(--text-secondary)]\",\n )}\n >\n <X aria-hidden className=\"size-4\" />\n </button>\n ) : (\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input.\n e.preventDefault();\n setOpen((o) => !o);\n }}\n className={cn(\n \"absolute right-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\",\n disabled ? \"opacity-50\" : \"cursor-pointer\",\n )}\n />\n )}\n </div>\n {isPhone && (\n <PickerSheet\n open={open}\n // Closing without choosing keeps the value: `text` was never emptied, so\n // reconcile has nothing to undo — it just puts the label back.\n onClose={reconcile}\n title={label}\n query={sheetQuery}\n onQueryChange={(v) => {\n setSheetQuery(v);\n setActive(-1);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder ?? labels.search}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {matches.map((o, i) => (\n // Keyed by group AND value, like combobox-core.tsx: an option may\n // deliberately appear twice (see `reconcile`), and a bare value key\n // would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className=\"px-4 pb-0.5 pt-3 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o.value === value}\n onClick={() => commit(o)}\n className={cn(SHEET_ROW_CLASS, o.value === value && \"font-medium\")}\n >\n {o.label}\n </button>\n </li>\n </Fragment>\n ))}\n {matches.length === 0 && (\n <li className=\"px-4 py-3 text-sm text-[var(--text-muted)]\">{labels.noResults}</li>\n )}\n </ul>\n </PickerSheet>\n )}\n {!isPhone && open && matches.length > 0 && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {matches.map((o, i) => (\n // A group heading is emitted at each group boundary rather than repeating\n // the group on every row (feedback #136). `matches` is group-contiguous,\n // so comparing with the previous row is enough. Fragment key sits here;\n // the heading and the option carry their own list semantics — keyed by\n // group AND value, because an option may deliberately appear twice (see\n // `reconcile`).\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // See {@link Combobox}: the role rides the button, `aria-selected`\n // is the CHOSEN row, and the keyboard's row is the field's\n // `aria-activedescendant`.\n role=\"option\"\n aria-selected={o.value === value}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the input's blur can't close the\n // list before the selection registers.\n e.preventDefault();\n commit(o);\n }}\n onMouseEnter={() => setActive(i)}\n className={cn(\n rowClass(i === active),\n // Indented under its heading, so the hierarchy is readable at a\n // glance instead of inferred from grey trailing text.\n o.group && \"pl-6\",\n o.value === value && \"font-medium\",\n )}\n >\n {o.label}\n {o.sublabel && (\n <span className=\"ml-2 text-xs text-[var(--text-placeholder)]\">\n {o.sublabel}\n </span>\n )}\n </button>\n </li>\n </Fragment>\n ))}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["rest"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/combobox.tsx"],"sourcesContent":["import { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { ChevronDown, X } from \"lucide-react\";\nimport { FieldLabel, FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { useDropdown } from \"./dropdown\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { PickerSheet, SHEET_ROW_CLASS } from \"./picker-sheet\";\nimport {\n SUGGESTION_LIST_CLASS,\n suggestionRowClass,\n useActiveOptionScroll,\n useComboboxFieldError,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n// One look for both combobox flavors below — the suggestion list and its rows\n// must stay pixel-identical between the free-text and the id-keyed variant (and\n// `Autocomplete`, which shares them from the core).\nconst LIST_CLASS = SUGGESTION_LIST_CLASS;\n\n/**\n * \"This focus came from a mouse button that is not the left one\" — for the two\n * fields below, which open their list ON FOCUS.\n *\n * Keksdose live #309 rework: *\"When clicking the mouse back button now while hovering\n * one of the selects it opens the select as long as I am holding the button down and\n * closes select after releasing.\"* A mouse's BACK button focuses whatever it is\n * pressed over, exactly as the left one does — the browser only reserves the\n * NAVIGATION for itself — so a field that opens on focus opened a list for a gesture\n * that means \"go back\", and the release then navigated out from under it. Measured on\n * his own budget: `mousePressed button=back` over the transfer form's account field\n * left `aria-expanded=true`, and the release took the whole add card with it.\n *\n * The press is recorded and read by the focus that the SAME press causes: focus is\n * mousedown's default action, dispatched inside the same task, so the flag is always\n * read before the `setTimeout` below can drop it. And it is always dropped — a press\n * that focuses nothing (the pointer was over a disabled field, the button was\n * released elsewhere) must not leave a latch that swallows the next Tab.\n *\n * Only the OPEN is suppressed, never the navigation: Back still goes back, which is\n * the whole of what the button was pressed for.\n */\nfunction usePrimaryPressOnly() {\n const auxPress = useRef(false);\n return {\n onMouseDown: (e: { button: number }) => {\n if (e.button === 0) return;\n auxPress.current = true;\n setTimeout(() => {\n auxPress.current = false;\n }, 0);\n },\n /** True while handling the focus a non-primary press just caused. */\n fromAuxButton: () => auxPress.current,\n };\n}\n\n/**\n * The desktop suggestion list, PORTALLED and anchored to the field.\n *\n * It used to be an `absolute` `<ul>` inside the field's own wrapper, and that is\n * Keksdose live #295: *\"Category select inside the list is not readable. Some sort\n * of z indexes issue?\"*. It was not z-index — a stacking context can be out-ranked,\n * but `overflow` cannot be argued with. The receipt's line table sits in a card\n * carrying `overflow-clip`, so a list opened from the last visible row was cut off\n * at the card's edge: measured at 256px tall with 150px of it painted.\n *\n * Every other panel in this package already learned this — `DropdownPanel`'s\n * `anchorRef` form, the calculator popover, the tooltip — so this reuses the same\n * hook rather than inventing a second placement. What that buys beyond the clip:\n * the list flips above the field when there is no room below, and it caps its own\n * height against the VISIBLE viewport, which on a phone means the on-screen\n * keyboard (see {@link useAnchoredPanel}).\n *\n * `panelRef` is not optional plumbing. Portalled, the list is no longer a\n * descendant of the wrapper, so {@link useDropdown}'s outside-click test answers\n * \"outside\" for a click on the list itself — the first option a user picked would\n * close the dropdown having picked nothing.\n */\nfunction SuggestionList({\n id,\n anchorRef,\n panelRef,\n children,\n}: {\n /** What the field's `aria-controls` names. */\n id: string;\n anchorRef: RefObject<HTMLElement | null>;\n panelRef: RefObject<HTMLDivElement | null>;\n children: ReactNode;\n}) {\n const { rect, top, maxHeight } = useAnchoredPanel(anchorRef, true, { preferredHeight: 256 });\n if (!rect) return null;\n return createPortal(\n <div\n ref={panelRef}\n // z-50, not the old z-30: the list is a child of <body> now, so it is\n // competing with the app's own overlays rather than with its own siblings.\n className=\"fixed z-50\"\n style={{ top, left: rect.left, width: rect.width }}\n >\n <ul id={id} role=\"listbox\" className={LIST_CLASS} style={{ maxHeight }}>\n {children}\n </ul>\n </div>,\n document.body,\n );\n}\n\nconst rowClass = suggestionRowClass;\n\n/**\n * Four of the div's own attributes are omitted because this component already owns\n * the name, with a different meaning: `id` is the INPUT's (a caller labels or\n * automates the field, not the box around it), and `onChange`/`onBlur`/`onSubmit`\n * are the field's — a value, a departure, and \"I mean what I typed\" — rather than\n * the DOM events of the wrapper. Everything else a wrapper needs reaches the root.\n */\nexport interface ComboboxProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\" | \"onBlur\" | \"onSubmit\"> {\n value: string;\n onChange: (v: string) => void;\n /** Suggestion pool (e.g. existing payee names). */\n options: string[];\n label?: string;\n id?: string;\n placeholder?: string;\n className?: string;\n /** Required and unanswered — see {@link Input}'s `invalid`. Set on the `<input>`\n * itself rather than on the wrapper, which is what let Keksdose's account picker\n * delete the `[&_input]:…` copy of {@link FIELD_INVALID} it had been carrying\n * because this component had no `invalid` of its own. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the `<input>`'s `aria-describedby`, and implies `invalid` (lenkbank's\n * scope fields, which moved here off `<Input list>` + `<datalist>`). */\n error?: ReactNode;\n /** As `<input disabled>`: no focus, no list, the field's settled look. The chevron\n * stops toggling with it — it is a mouse target the input's own `disabled` does\n * not reach. */\n disabled?: boolean;\n /** Heading an option belongs under. Supplying it makes this list read exactly\n * like {@link InlineEntityCombobox}'s — one heading per group with its rows\n * indented beneath — instead of a flat list (feedback #136 rework: the payee\n * field sat next to the newly-grouped category field and no longer matched).\n * Omit for a plain list. */\n groupBy?: (option: string) => string;\n /**\n * Something to show at the far end of an option's row — a badge saying what the\n * option IS, as distinct from what it is called.\n *\n * Keksdose's category-name field is the case: its proposals carry per-locale\n * names, so picking one stores a category that follows the UI language, and\n * picking a look-alike custom name does not. That difference is invisible in the\n * label and decides what the row DOES, so the row has to show it. Returning\n * `null` for an option renders nothing and costs no layout.\n *\n * Not rendered in the create row, which by definition names nothing in the pool.\n */\n optionAdornment?: (option: string) => ReactNode;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`, open\n * the list with it. A click-the-value inline editor needs it: the cell the user\n * clicked names the field the caret should land in. {@link InlineEntityCombobox}\n * has had it since live #218. */\n autoFocus?: boolean;\n /** Focus genuinely LEFT the field. Not fired by picking a row — the rows suppress\n * `mousedown`, so the input never blurs — which is what makes it usable as a\n * \"close the inline editor\" signal. */\n onBlur?: () => void;\n /** Enter pressed with no row highlighted, i.e. \"I mean what I typed\". The list\n * closes either way; this is for a caller whose Enter also submits a row editor. */\n onSubmit?: () => void;\n /** Rows to offer when the field is empty. 8 on a dropdown, where that is all\n * that fits; the phone sheet asks for more because it has a screen. */\n maxSuggestions?: number;\n searchPlaceholder?: string;\n closeLabel?: string;\n /**\n * Label for the row that COMMITS a value the list does not contain — e.g.\n * `(v) => \\`Add \"${v}\" as payee\\``. Supplying it turns the free text into\n * something you confirm rather than something you leave behind.\n *\n * Keksdose live #212: *\"For the payee input have an apply button, when input is\n * placed like 'add xxx as payee' to confirm. Now it can be put in and closed with\n * the right top X, which seems not intuitive.\"* On the phone sheet the search box\n * IS the value, so typing a brand-new name and dismissing the sheet did commit it —\n * but the only control on offer was the close X, which reads as \"discard\". A named\n * affordance says what the typing did.\n *\n * Omit and the row is not rendered, which is the right default for a list whose\n * values are all supposed to already exist.\n */\n createLabel?: (value: string) => string;\n}\n\n/**\n * Free-text combobox that looks like the shared {@link Select} (same FIELD_BASE\n * styling, chevron and static floating label) but lets the user type a value\n * that isn't in the list — e.g. naming a brand-new payee. Picking from the\n * filtered suggestion list fills the value; typing keeps whatever was entered.\n *\n * Use this instead of an `<Input list=\"…\">` + `<datalist>` so the control is\n * visually and behaviourally consistent with the other dropdowns (feedback\n * #230 — the payee field looked/behaved differently from every other select).\n */\nexport function Combobox({\n value,\n onChange,\n options,\n label,\n id,\n placeholder,\n className,\n groupBy,\n maxSuggestions,\n searchPlaceholder,\n closeLabel,\n createLabel,\n invalid,\n error,\n disabled,\n optionAdornment,\n autoFocus,\n onBlur,\n onSubmit,\n \"aria-label\": ariaLabel,\n ...rest\n}: ComboboxProps) {\n const field = useComboboxFieldError(error, invalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // Derived from the GENERATED id, never from `id`: a caller's id is theirs to\n // collide with, and `aria-controls` has to resolve.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n // `backCloses` is the desktop half of live #309: the phone's list IS a\n // {@link PickerSheet}, which registers its own history entry, so registering a\n // second one here would cost two Back presses to dismiss one sheet.\n const isPhone = useMediaQuery(PHONE_QUERY, false);\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone });\n const primaryOnly = usePrimaryPressOnly();\n const [active, setActive] = useState(-1);\n // A phone opens the list as a full-screen sheet with its own input, the way a\n // native <select> does — live #200: \"Paid as full screen dialog with input.\n // Similar to the account select that already appears as full screen.\" The\n // anchored list stays for pointer devices, where it is the better shape.\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — the inner wrapper, which hugs the input,\n // not the outer one (a grid item that can be taller than the field).\n const fieldRef = useRef<HTMLDivElement>(null);\n\n /**\n * Is the text in the field a QUERY, or just what was picked last time?\n *\n * Keksdose dev#549, filed against the category-name field: *\"Currently it acts kind\n * of a filter and clicking again does not open anything since the previous text is\n * still there likely filtering all other options out.\"* A combobox whose filter is\n * its own value can only ever re-offer the answer it already has — after picking\n * \"REWE\" the list reopens holding one row, the one you are looking at, and changing\n * your mind means clearing the field by hand first.\n *\n * So the text filters only while it is being TYPED; opening the list, by focus or\n * by clicking the field again, puts the whole pool back. {@link InlineEntityCombobox}\n * below has had this since it was written (see its `typedQuery`) and the app's\n * category-name field grew its own copy to get it — this one, the payee field, was\n * the one left with the bug.\n *\n * Not derived from \"does the text exactly match an option\": a custom value that\n * happens to collide with one would then behave differently from every other, and\n * what is being tracked is what the user just did, which is not a property of the\n * string.\n */\n const [typing, setTyping] = useState(false);\n\n const query = typing ? value.trim().toLowerCase() : \"\";\n const matches = useMemo(() => {\n const seen = new Set<string>();\n const uniq = options.filter((o) => o && !seen.has(o) && seen.add(o));\n let ranked: string[];\n const cap = maxSuggestions ?? (isPhone ? 50 : 8);\n if (!query) {\n ranked = uniq.slice(0, cap);\n } else {\n // Prefix matches first, then substring matches — most relevant on top.\n const starts = uniq.filter((o) => o.toLowerCase().startsWith(query));\n const contains = uniq.filter(\n (o) => !o.toLowerCase().startsWith(query) && o.toLowerCase().includes(query),\n );\n ranked = [...starts, ...contains].slice(0, cap);\n }\n if (!groupBy) return ranked;\n // Keep each group contiguous so a heading appears once instead of every time\n // the ranking interleaves two groups — while preserving \"best match first\":\n // a Map keeps insertion order, so groups come out ordered by their best-ranked\n // member and members keep their rank order inside the group.\n const byGroup = new Map<string, string[]>();\n for (const o of ranked) {\n const key = groupBy(o);\n if (!byGroup.has(key)) byGroup.set(key, []);\n byGroup.get(key)!.push(o);\n }\n return [...byGroup.values()].flat();\n }, [options, query, groupBy, maxSuggestions, isPhone]);\n\n /**\n * Which rows OPEN a group, and with what heading — computed once for both lists.\n *\n * The desktop list had this as `matches[i - 1]`, which is free; the sheet had it as\n * `matches[matches.indexOf(o) - 1]`, which is a scan of the list per row and another\n * per heading test. That is quadratic in the sheet's own cap of 50, on the branch\n * that runs on the slowest hardware we ship to — and it answers the wrong question\n * besides: `indexOf` finds the FIRST row with that string, so a duplicated option\n * would have compared against a neighbour it is not next to. (`matches` de-duplicates\n * today; that is a property of the memo above, not of the row being rendered.)\n */\n const groupHeadings = useMemo(() => {\n if (!groupBy) return null;\n const heads = new Map<number, string>();\n let prev: string | undefined;\n matches.forEach((o, i) => {\n const group = groupBy(o);\n if (group != null && group !== prev) heads.set(i, group);\n prev = group;\n });\n return heads;\n }, [matches, groupBy]);\n\n const commit = (v: string) => {\n onChange(v);\n setOpen(false);\n setActive(-1);\n // What is in the field is now an ANSWER, not a query — so reopening offers the\n // whole pool again rather than a one-row filter of the value just chosen.\n setTyping(false);\n };\n\n // Offered only when the typed text is genuinely NOT in the pool: an exact\n // (case-insensitive) match is an existing entry the list is already showing, and a\n // second way to pick it would be noise. Compared against `options`, not `matches` —\n // `matches` is capped, so a pool of 300 payees would otherwise offer to \"add\" one\n // that exists but fell off the end of the list.\n // What the field claims the keyboard is on. Only on a pointer device: the phone\n // renders its rows in a {@link PickerSheet} whose own search box owns focus, and a\n // field pointing at an id that is not there is worse than one pointing nowhere.\n const activeId = !isPhone && active >= 0 && active < matches.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const typed = value.trim();\n const createRow =\n createLabel && typed.length > 0 && !options.some((o) => o.toLowerCase() === typed.toLowerCase())\n ? createLabel(typed)\n : null;\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id, a form-level\n // `aria-describedby`. Not the NAME: that belongs on the <input> below, which is\n // the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n {/* The chevron centers against this inner wrapper, which hugs the input.\n The outer div can be taller than the input (as a grid item it\n stretches to the row height, e.g. next to the editor's category cell\n with its split button), which used to drag a top-1/2 chevron down to\n the input's bottom edge (feedback #248). */}\n <div ref={fieldRef} className=\"relative\">\n <input\n id={fieldId}\n value={value}\n placeholder={placeholder}\n // The visual label is a floating <span>, not a <label for>, so fall back to\n // it for the accessible name — otherwise the field announces only what is\n // typed in it (dev#477, applied to all three entity fields of a transaction\n // form at once).\n aria-label={ariaLabel ?? label}\n role=\"combobox\"\n aria-expanded={open}\n // The list this field is the mouth of. Required by the role, and the half\n // that was missing: the field said it was expanded and never said what it\n // had expanded, so a reader had no way from the box to the options\n // (ESLint's `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={field.describedBy}\n autoComplete=\"off\"\n disabled={disabled}\n autoFocus={autoFocus}\n onBlur={onBlur}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={() => {\n // A back/forward mouse button focuses this field on its way to\n // navigating; it is not a request to open anything (live #309 rework).\n if (primaryOnly.fromAuxButton()) return;\n setOpen(true);\n setTyping(false);\n // The sheet carries its own input, so the field behind it must not also\n // pull up the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n // A CLICK as well as focus — the other half of dev#549. Picking a suggestion\n // closes the list without moving focus (the rows suppress `mousedown` on\n // purpose, so the input never blurred), which means clicking the field again\n // fires no `focus` event at all and the list stayed shut. \"Does not open\n // anything\" was literally true.\n onClick={() => {\n setOpen(true);\n setTyping(false);\n }}\n // `inputMode=\"none\"` rather than readOnly: the field must not look\n // uneditable (FIELD_BASE greys a read-only field since dev#468) and must\n // still take focus — it just has no keyboard of its own, the same trick\n // the amount field uses for the numpad.\n inputMode={isPhone ? \"none\" : undefined}\n onChange={(e) => {\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n setTyping(true);\n }}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => Math.min(i + 1, matches.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n if (open && active >= 0 && active < matches.length) {\n e.preventDefault();\n commit(matches[active]);\n } else {\n // No row highlighted: the typed text is the answer. `preventDefault`\n // only when a caller is taking Enter, so a plain form submit is\n // otherwise left alone.\n if (onSubmit) e.preventDefault();\n setOpen(false);\n onSubmit?.();\n }\n } else if (e.key === \"Escape\") {\n setOpen(false);\n setActive(-1);\n } else if (e.key === \"Tab\") {\n // Closes, and lets the browser take the Tab: focus is in THIS input,\n // which is staying, so there is nothing to catch — unlike the pickers\n // whose focus sits inside a portalled panel.\n setOpen(false);\n setActive(-1);\n }\n // Home/End are deliberately absent. The APG gives them to the list only\n // where the combobox is not editable; here the text IS the value, a\n // payee name is long enough to want the caret moved to its start, and\n // the desktop list is capped at 8 rows — so jumping it would be worth\n // almost nothing and would cost the one gesture that field is used with.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pr-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input.\n e.preventDefault();\n if (!disabled) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute right-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\",\n disabled ? \"opacity-50\" : \"cursor-pointer\",\n )}\n />\n </div>\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !disabled}\n onClose={() => setOpen(false)}\n title={label}\n // The sheet's input IS the field: this is a free-text control, so what is\n // typed here is the value (a brand-new payee is just a name nothing\n // matches), and the list below narrows as it changes.\n query={value}\n onQueryChange={(v) => {\n onChange(v);\n setTyping(true);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {createRow && (\n // `role=\"option\"` rides the BUTTON, not the <li> around it: an option\n // may not contain a separately focusable control, and the button is\n // what a pointer presses. Same shape `CommandPalette` has always had.\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n onClick={() => commit(typed)}\n className={cn(SHEET_ROW_CLASS, \"font-medium text-[var(--brand)]\")}\n >\n {createRow}\n </button>\n </li>\n )}\n {matches.map((o, i) => {\n const group = groupHeadings?.get(i);\n return (\n <Fragment key={o}>\n {group !== undefined && (\n <li\n role=\"presentation\"\n className=\"px-4 pb-0.5 pt-3 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]\"\n >\n {group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o === value}\n onClick={() => commit(o)}\n className={cn(\n SHEET_ROW_CLASS,\n optionAdornment && \"flex items-center justify-between gap-2\",\n )}\n >\n {optionAdornment ? <span className=\"truncate\">{o}</span> : o}\n {optionAdornment?.(o)}\n </button>\n </li>\n </Fragment>\n );\n })}\n </ul>\n </PickerSheet>\n )}\n {!isPhone && open && !disabled && (matches.length > 0 || createRow) && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {createRow && (\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={false}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown, like the rows below: the input's blur would otherwise\n // close the list before the click landed.\n e.preventDefault();\n commit(typed);\n }}\n className={cn(rowClass(false), \"font-medium text-[var(--brand)]\")}\n >\n {createRow}\n </button>\n </li>\n )}\n {matches.map((o, i) => {\n // Heading at each group boundary only — `matches` is group-contiguous, so\n // the boundaries are a property of the list rather than of the branch\n // drawing it. See `groupHeadings`.\n const group = groupHeadings?.get(i);\n return (\n <Fragment key={o}>\n {group !== undefined && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, not highlighted. The row the arrows are on is named by\n // the field's `aria-activedescendant`; saying \"selected\" for it\n // would tell a reader that the row they are passing over is\n // already the answer.\n aria-selected={o === value}\n // Focus stays in the field — that is what\n // `aria-activedescendant` is for — so the rows are not tab stops.\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the blur from the input firing first\n // doesn't close the list before the selection registers.\n e.preventDefault();\n commit(o);\n }}\n onMouseEnter={() => setActive(i)}\n className={cn(\n rowClass(i === active),\n group != null && \"pl-6\",\n // A row with a badge is a flex row so the label truncates and the\n // badge keeps its width; without one it stays the plain block the\n // other lists render, so nothing shifts for callers that pass none.\n optionAdornment && \"flex items-center justify-between gap-2\",\n )}\n >\n {optionAdornment ? <span className=\"truncate\">{o}</span> : o}\n {optionAdornment?.(o)}\n </button>\n </li>\n </Fragment>\n );\n })}\n </SuggestionList>\n )}\n </div>\n );\n}\n\n/** `id` is the INPUT's and `onChange` is the field's — see {@link ComboboxProps}\n * for why the div's spellings of them are omitted. */\nexport interface InlineEntityComboboxProps<V extends string | number>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"id\" | \"onChange\"> {\n /** Selected option id, or null when nothing is selected. */\n value: V | null;\n /** Required and unanswered — see {@link Combobox}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value — see {@link Combobox}'s `error`. */\n error?: ReactNode;\n /** A picked/typed option emits its id; emptying the field emits `null`. */\n onChange: (v: V | null) => void;\n options: ComboOption<V>[];\n label?: string;\n id?: string;\n placeholder?: string;\n className?: string;\n disabled?: boolean;\n /** Focus on mount, the way `<input autoFocus>` does — and, through `onFocus`\n * below, open the list with it. A click-the-value editor needs it: the cell\n * the user clicked names the field the editor should land the caret in, and\n * without a passthrough this component's fixed prop list puts the `<input>`\n * out of a caller's reach entirely (Keksdose feedback live #218). */\n autoFocus?: boolean;\n /** Phone sheet only — the anchored list has the field itself to type in. Falls\n * back to `placeholder`, then to `combobox.search` from the {@link UiKitProvider}. */\n searchPlaceholder?: string;\n /** Phone sheet only: a full screen showing nothing has to say why. The anchored\n * list simply does not open. Default: `combobox.noResults` from the provider. */\n emptyLabel?: string;\n closeLabel?: string;\n /** Offer a clear \"×\" in place of the chevron whenever something is selected.\n *\n * Emptying the text already clears (see {@link reconcile}), and on a desktop that\n * is the fast way. It is not a way at all on a PHONE: touching the field opens the\n * full-screen sheet, which covers the very input the text would have been deleted\n * from — so a field that had been answered could not be UNanswered by any gesture\n * the screen offered (Keksdose live #236, filed from a phone). The \"×\" is the one\n * affordance both shells share, and it clears without opening anything. */\n clearable?: boolean;\n clearLabel?: string;\n}\n\n/**\n * The id-keyed sibling of {@link Combobox}: identical anatomy (a real text input\n * with the shared field styling, chevron and inline as-you-type filtering) but\n * the value is an option id, not free text. Built so an entity picker can sit\n * next to a free-text combobox without any visible difference — a button-trigger\n * panel picker ({@link EntityCombobox}) never matches an input field exactly.\n *\n * Text-vs-value reconciliation: the input's text is transient. Picking a row or\n * typing an exact (unique) label commits that option; emptying the text commits\n * a clear; anything else reverts to the selected option's label on blur/Escape.\n *\n * **On a phone the list opens as a full-screen sheet**, exactly like {@link Combobox}\n * beside it (Keksdose live #200, extended by dev#477 — the account picker moved onto\n * this component and must not lose its sheet). The sheet carries its OWN query state\n * rather than reusing `text`: `text` is what {@link reconcile} judges on blur, so a\n * sheet that emptied it to show the full list would read as \"the user cleared the\n * field\" the moment it closed.\n */\nexport function InlineEntityCombobox<V extends string | number>({\n value,\n onChange,\n options,\n label,\n id,\n placeholder,\n className,\n disabled,\n autoFocus,\n searchPlaceholder,\n emptyLabel,\n closeLabel,\n clearable,\n clearLabel,\n invalid,\n error,\n \"aria-label\": ariaLabel,\n ...rest\n}: InlineEntityComboboxProps<V>) {\n const field = useComboboxFieldError(error, invalid);\n const generated = useId();\n const fieldId = id ?? generated;\n // See the twin above on why these hang off the generated id.\n const listboxId = `${generated}-listbox`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const isPhone = useMediaQuery(PHONE_QUERY, false);\n // See the twin above: the phone sheet owns its own Back entry (live #309).\n const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone });\n const primaryOnly = usePrimaryPressOnly();\n const [active, setActive] = useState(-1);\n // null = not editing → the input shows the selected option's label.\n const [text, setText] = useState<string | null>(null);\n const sheetInputRef = useRef<HTMLInputElement | null>(null);\n // The box the portalled list hangs off — see {@link SuggestionList}.\n const fieldRef = useRef<HTMLDivElement>(null);\n // The sheet's search box. Starts empty on every open, so a field that already\n // holds a value still offers the whole list — the shape a native <select> has\n // on a phone, and what the anchored panel gets from `query` below.\n const [sheetQuery, setSheetQuery] = useState(\"\");\n // Props first, provider second — see `EntityCombobox`. `emptyLabel` used to render\n // nothing when omitted, which left a phone user staring at a blank full-screen\n // sheet; now an omitted one says \"No results\" in the provider's language.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n });\n\n const selected = useMemo(\n () => (value == null ? null : (options.find((o) => o.value === value) ?? null)),\n [options, value],\n );\n const shown = text ?? selected?.label ?? \"\";\n // Nothing selected has nothing to clear, and the chevron comes back — the field\n // keeps exactly one trailing control, so the \"×\" never crowds the value it sits on.\n const showClear = Boolean(clearable && value != null && !disabled);\n\n // Focusing select-alls the current label; filtering only kicks in once the\n // text actually differs from it, so an already-filled field still opens on\n // the FULL list instead of a single-row \"filter\" of its own value.\n const typedQuery =\n text !== null && text !== (selected?.label ?? \"\") ? text.trim().toLowerCase() : \"\";\n const query = isPhone ? sheetQuery.trim().toLowerCase() : typedQuery;\n const matches = useMemo(() => {\n let ranked = options;\n if (query) {\n const hit = (s: string | undefined) => s?.toLowerCase().includes(query) ?? false;\n const starts = options.filter((o) => o.label.toLowerCase().startsWith(query));\n const rest = options.filter(\n (o) =>\n !o.label.toLowerCase().startsWith(query) &&\n (hit(o.label) || hit(o.sublabel) || hit(o.group)),\n );\n ranked = [...starts, ...rest];\n }\n if (!ranked.some((o) => o.group)) return ranked;\n // Keep each group contiguous so the headings below appear once instead of\n // re-appearing every time the ranking interleaves two groups — while preserving\n // \"best match first\": a Map keeps insertion order, so groups come out ordered by\n // their best-ranked member and members keep their rank order inside the group.\n const blocks = new Map<string, ComboOption<V>[]>();\n for (const o of ranked) {\n const key = o.group ?? \"\";\n if (!blocks.has(key)) blocks.set(key, []);\n blocks.get(key)!.push(o);\n }\n return [...blocks.values()].flat();\n }, [options, query]);\n\n // Pointer-device only: the phone's rows live in a {@link PickerSheet} whose own\n // search box holds focus, so the field behind it must not claim to be pointing at\n // one of them.\n const activeId = !isPhone && active >= 0 && active < matches.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n setSheetQuery(\"\");\n };\n const commit = (o: ComboOption<V>) => {\n if (o.value !== value) onChange(o.value);\n setText(null);\n close();\n };\n /** Turn loose text into a decision: empty clears, an exact label match that names\n * ONE entity commits, anything else reverts to the selected label.\n *\n * Unique by VALUE, not by row. An option may deliberately appear twice — Keksdose\n * repeats recently-used categories in a \"Recent\" group at the top (live #203) — and\n * counting rows made every such option uncommittable: two hits, so nothing fired and\n * the field reverted to whatever was selected before. Typing a category you had just\n * used, then tabbing away, silently discarded it, and the more categories you used\n * the more of them stopped working. Two rows naming the same id are not an ambiguity;\n * two ids sharing a label are. */\n const reconcile = () => {\n if (text !== null) {\n const q = text.trim();\n if (!q) {\n if (value != null) onChange(null);\n } else {\n const hits = options.filter((o) => o.label.toLowerCase() === q.toLowerCase());\n const ids = new Set(hits.map((h) => h.value));\n if (ids.size === 1 && hits[0].value !== value) onChange(hits[0].value);\n }\n setText(null);\n }\n close();\n };\n\n return (\n // `rest` dresses the outer box — a `data-tour` anchor, a test id, a form-level\n // `aria-describedby`. Not the NAME: that belongs on the <input> below, which is\n // the combobox a reader meets. Spread FIRST, so the field's ARIA and the\n // handlers carrying live #309 and dev#549 cannot be replaced from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n {/* Inner wrapper for chevron centering — same reasoning as Combobox above.\n It is also what the portalled list anchors to. */}\n <div ref={fieldRef} className=\"relative\">\n <input\n id={fieldId}\n value={shown}\n placeholder={placeholder}\n // The visual label is a floating <span>, not a <label for>, so without this\n // the field has NO accessible name — it announces its value and nothing\n // else. Just the label, never \"label: value\" the way a trigger button has\n // to compose it: an input already exposes its value separately.\n aria-label={ariaLabel ?? (typeof label === \"string\" ? label : undefined)}\n role=\"combobox\"\n aria-expanded={open}\n // Required by the role, and the half that was missing: the field said it\n // was expanded and never said what it had expanded (ESLint's\n // `role-has-required-aria-props`, the audit's §a11y).\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-autocomplete=\"list\"\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={field.describedBy}\n autoComplete=\"off\"\n disabled={disabled}\n autoFocus={autoFocus}\n // `inputMode=\"none\"` rather than readOnly, for the same reason Combobox\n // above gives: the sheet carries the keyboard, and a readOnly field would\n // take FIELD_BASE's settled look on a field that is perfectly editable.\n inputMode={isPhone ? \"none\" : undefined}\n onMouseDown={primaryOnly.onMouseDown}\n onFocus={(e) => {\n // See {@link usePrimaryPressOnly}: a back/forward button lands here on\n // its way to navigating, and neither the list nor the select-all is\n // anything it asked for (live #309 rework).\n if (primaryOnly.fromAuxButton()) return;\n setText(shown);\n e.currentTarget.select();\n setOpen(true);\n // The sheet has its own input; the field behind it must not also pull up\n // the keyboard and scroll the page under the dialog.\n if (isPhone) sheetInputRef.current?.focus();\n }}\n onBlur={() => {\n // On a phone the blur is the SHEET taking focus, not the user leaving the\n // field — reconciling there would close the sheet the instant it opened.\n if (!isPhone) reconcile();\n }}\n onChange={(e) => {\n setText(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n setActive((i) => Math.min(i + 1, matches.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n if (open && active >= 0 && active < matches.length) {\n e.preventDefault();\n commit(matches[active]);\n } else {\n reconcile();\n }\n } else if (e.key === \"Escape\") {\n setText(null);\n close();\n } else if (e.key === \"Tab\") {\n // Focus is in THIS input and stays there, so the browser's own Tab is\n // left alone; all that is needed is that the list stop covering what\n // the user is tabbing to. `reconcile` rather than `close`, because\n // leaving the field is exactly when loose text has to be judged.\n reconcile();\n }\n // Home/End stay with the caret — see the note in {@link Combobox}: this\n // field is editable, and its text is what `reconcile` judges.\n }}\n className={cn(\n FIELD_BASE,\n label !== undefined && FIELD_FLOATING_PAD,\n \"pr-9\",\n field.isInvalid && FIELD_INVALID,\n )}\n />\n {showClear ? (\n <button\n type=\"button\"\n // Out of the tab order, like the clear on `EntityCombobox`: the keyboard\n // already clears this field by selecting its text and deleting, and a\n // second stop between every picker and the next field is a worse trade\n // than the one gesture it saves.\n tabIndex={-1}\n aria-label={labels.clear}\n // preventDefault, exactly as the chevron does: without it the press\n // focuses the input, which on a phone opens the sheet over the field the\n // press was clearing.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => {\n setText(null);\n close();\n onChange(null);\n }}\n className={cn(\n \"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)]\",\n \"hover:text-[var(--text-secondary)]\",\n )}\n >\n <X aria-hidden className=\"size-4\" />\n </button>\n ) : (\n <ChevronDown\n aria-hidden\n onMouseDown={(e) => {\n // Toggle on the chevron without stealing focus from the input — and\n // not at all on a disabled field, which the input's own `disabled`\n // does not stop from here.\n e.preventDefault();\n if (!disabled) setOpen((o) => !o);\n }}\n className={cn(\n \"absolute right-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\",\n disabled ? \"opacity-50\" : \"cursor-pointer\",\n )}\n />\n )}\n </div>\n {field.errorEl}\n {isPhone && (\n <PickerSheet\n open={open && !disabled}\n // Closing without choosing keeps the value: `text` was never emptied, so\n // reconcile has nothing to undo — it just puts the label back.\n onClose={reconcile}\n title={label}\n query={sheetQuery}\n onQueryChange={(v) => {\n setSheetQuery(v);\n setActive(-1);\n }}\n searchPlaceholder={searchPlaceholder ?? placeholder ?? labels.search}\n inputRef={sheetInputRef}\n closeLabel={closeLabel}\n >\n <ul id={listboxId} role=\"listbox\">\n {matches.map((o, i) => (\n // Keyed by group AND value, like combobox-core.tsx: an option may\n // deliberately appear twice (see `reconcile`), and a bare value key\n // would collide.\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className=\"px-4 pb-0.5 pt-3 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={o.value === value}\n onClick={() => commit(o)}\n className={cn(SHEET_ROW_CLASS, o.value === value && \"font-medium\")}\n >\n {o.label}\n </button>\n </li>\n </Fragment>\n ))}\n {matches.length === 0 && (\n <li className=\"px-4 py-3 text-sm text-[var(--text-muted)]\">{labels.noResults}</li>\n )}\n </ul>\n </PickerSheet>\n )}\n {!isPhone && open && !disabled && matches.length > 0 && (\n <SuggestionList id={listboxId} anchorRef={fieldRef} panelRef={panelRef}>\n {matches.map((o, i) => (\n // A group heading is emitted at each group boundary rather than repeating\n // the group on every row (feedback #136). `matches` is group-contiguous,\n // so comparing with the previous row is enough. Fragment key sits here;\n // the heading and the option carry their own list semantics — keyed by\n // group AND value, because an option may deliberately appear twice (see\n // `reconcile`).\n <Fragment key={`${o.group ?? \"\"}|${String(o.value)}`}>\n {o.group && o.group !== matches[i - 1]?.group && (\n <li\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)] first:pt-1\"\n >\n {o.group}\n </li>\n )}\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // See {@link Combobox}: the role rides the button, `aria-selected`\n // is the CHOSEN row, and the keyboard's row is the field's\n // `aria-activedescendant`.\n role=\"option\"\n aria-selected={o.value === value}\n tabIndex={-1}\n onMouseDown={(e) => {\n // mousedown (not click) so the input's blur can't close the\n // list before the selection registers.\n e.preventDefault();\n commit(o);\n }}\n onMouseEnter={() => setActive(i)}\n className={cn(\n rowClass(i === active),\n // Indented under its heading, so the hierarchy is readable at a\n // glance instead of inferred from grey trailing text.\n o.group && \"pl-6\",\n o.value === value && \"font-medium\",\n )}\n >\n {o.label}\n {o.sublabel && (\n <span className=\"ml-2 text-xs text-[var(--text-placeholder)]\">\n {o.sublabel}\n </span>\n )}\n </button>\n </li>\n </Fragment>\n ))}\n </SuggestionList>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["rest"]}
|
|
@@ -5,7 +5,12 @@ import { Check } from "lucide-react";
|
|
|
5
5
|
import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from "./ui.js";
|
|
6
6
|
import { cn } from "../lib/cn.js";
|
|
7
7
|
import { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from "./dropdown.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_COMMON_LABELS,
|
|
10
|
+
DEFAULT_CURRENCY_LABELS,
|
|
11
|
+
useKitLabels,
|
|
12
|
+
useKitLocale
|
|
13
|
+
} from "../i18n/kit-labels.js";
|
|
9
14
|
const CURRENCIES = [
|
|
10
15
|
{ code: "EUR", name: "Euro", symbol: "\u20AC", country: "eu" },
|
|
11
16
|
{ code: "USD", name: "US Dollar", symbol: "$", country: "us" },
|
|
@@ -76,6 +81,7 @@ function CurrencySelect({
|
|
|
76
81
|
}) {
|
|
77
82
|
const { open, setOpen, wrapperRef, query, setQuery, inputRef } = useDropdownSearch();
|
|
78
83
|
const text = useKitLabels("currency", DEFAULT_CURRENCY_LABELS, labels);
|
|
84
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
79
85
|
const locale = useKitLocale();
|
|
80
86
|
const selected = getCurrency(value);
|
|
81
87
|
const pool = useMemo(() => {
|
|
@@ -90,6 +96,8 @@ function CurrencySelect({
|
|
|
90
96
|
);
|
|
91
97
|
}, [query, pool, currencyNames, locale]);
|
|
92
98
|
const listboxId = `${useId()}-listbox`;
|
|
99
|
+
const fieldName = typeof label === "string" ? label : text.currency;
|
|
100
|
+
const name = ariaLabel ?? common.fieldValue(fieldName, selected ? selected.code : placeholder ?? text.currency);
|
|
93
101
|
return (
|
|
94
102
|
// The wrapper takes `rest`; the trigger takes the name. This field's label is a
|
|
95
103
|
// floating <span> rather than a <label for>, so a caller who wants the control to
|
|
@@ -100,7 +108,7 @@ function CurrencySelect({
|
|
|
100
108
|
"button",
|
|
101
109
|
{
|
|
102
110
|
type: "button",
|
|
103
|
-
"aria-label":
|
|
111
|
+
"aria-label": name,
|
|
104
112
|
onClick: () => setOpen((v) => !v),
|
|
105
113
|
role: "combobox",
|
|
106
114
|
"aria-haspopup": "listbox",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/currency-select.tsx"],"sourcesContent":["import { useMemo, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from \"./dropdown\";\nimport { DEFAULT_CURRENCY_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\nexport interface CurrencyOption {\n code: string;\n name: string;\n symbol: string;\n /** ISO 3166-1 alpha-2 country code (lowercase) used by flag-icons. */\n country: string;\n}\n\nexport const CURRENCIES: CurrencyOption[] = [\n { code: \"EUR\", name: \"Euro\", symbol: \"€\", country: \"eu\" },\n { code: \"USD\", name: \"US Dollar\", symbol: \"$\", country: \"us\" },\n { code: \"GBP\", name: \"British Pound\", symbol: \"£\", country: \"gb\" },\n { code: \"CHF\", name: \"Swiss Franc\", symbol: \"CHF\", country: \"ch\" },\n { code: \"JPY\", name: \"Japanese Yen\", symbol: \"¥\", country: \"jp\" },\n { code: \"CAD\", name: \"Canadian Dollar\", symbol: \"CA$\", country: \"ca\" },\n { code: \"AUD\", name: \"Australian Dollar\", symbol: \"A$\", country: \"au\" },\n { code: \"NZD\", name: \"New Zealand Dollar\", symbol: \"NZ$\", country: \"nz\" },\n { code: \"SEK\", name: \"Swedish Krona\", symbol: \"kr\", country: \"se\" },\n { code: \"NOK\", name: \"Norwegian Krone\", symbol: \"kr\", country: \"no\" },\n { code: \"DKK\", name: \"Danish Krone\", symbol: \"kr\", country: \"dk\" },\n { code: \"PLN\", name: \"Polish Zloty\", symbol: \"zł\", country: \"pl\" },\n { code: \"CZK\", name: \"Czech Koruna\", symbol: \"Kč\", country: \"cz\" },\n { code: \"HUF\", name: \"Hungarian Forint\", symbol: \"Ft\", country: \"hu\" },\n { code: \"RON\", name: \"Romanian Leu\", symbol: \"lei\", country: \"ro\" },\n { code: \"BGN\", name: \"Bulgarian Lev\", symbol: \"лв\", country: \"bg\" },\n { code: \"TRY\", name: \"Turkish Lira\", symbol: \"₺\", country: \"tr\" },\n { code: \"RUB\", name: \"Russian Ruble\", symbol: \"₽\", country: \"ru\" },\n { code: \"CNY\", name: \"Chinese Yuan\", symbol: \"¥\", country: \"cn\" },\n { code: \"HKD\", name: \"Hong Kong Dollar\", symbol: \"HK$\", country: \"hk\" },\n { code: \"SGD\", name: \"Singapore Dollar\", symbol: \"S$\", country: \"sg\" },\n { code: \"INR\", name: \"Indian Rupee\", symbol: \"₹\", country: \"in\" },\n { code: \"BRL\", name: \"Brazilian Real\", symbol: \"R$\", country: \"br\" },\n { code: \"MXN\", name: \"Mexican Peso\", symbol: \"MX$\", country: \"mx\" },\n { code: \"ZAR\", name: \"South African Rand\", symbol: \"R\", country: \"za\" },\n { code: \"ILS\", name: \"Israeli Shekel\", symbol: \"₪\", country: \"il\" },\n { code: \"AED\", name: \"UAE Dirham\", symbol: \"د.إ\", country: \"ae\" },\n];\n\nexport interface CurrencyFlagProps extends ComponentPropsWithoutRef<\"span\"> {\n /** ISO 3166-1 alpha-2, lowercase — the class flag-icons keys its sprite by. */\n country: string;\n}\n\nexport function CurrencyFlag({ country, className, ...rest }: CurrencyFlagProps) {\n return (\n <span\n {...rest}\n // Stays `aria-hidden` whatever a caller passes: the flag is a picture of the\n // code printed next to it, and a reader that met both would hear the currency\n // twice.\n aria-hidden\n className={cn(\"fi inline-block w-5 h-[15px] rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)] shrink-0\", `fi-${country}`, className)}\n />\n );\n}\n\nconst CURRENCY_BY_CODE = new Map(CURRENCIES.map((c) => [c.code, c]));\n\nexport function getCurrency(code: string | null | undefined): CurrencyOption | undefined {\n return code ? CURRENCY_BY_CODE.get(code.toUpperCase()) : undefined;\n}\n\n/** The name to SHOW for a currency: the caller's translation if they have one for\n * that code; else, when a `locale` is known, the name `Intl.DisplayNames` has for\n * it in that locale; else the English name {@link CURRENCIES} carries. Shared with\n * AmountInput, which renders the same list from the same data.\n *\n * `Intl` only when a locale was actually GIVEN (by prop or `<UiKitProvider>`): an\n * app that never set one keeps the shipped English names it has always shown,\n * rather than whatever language the test runner or the browser happens to be in. */\nexport function currencyName(\n c: CurrencyOption,\n names?: Record<string, string>,\n locale?: string,\n): string {\n const own = names?.[c.code];\n if (own !== undefined) return own;\n if (locale) {\n try {\n const shown = new Intl.DisplayNames([locale], { type: \"currency\" }).of(c.code);\n if (shown && shown !== c.code) return shown;\n } catch {\n // A malformed tag, or a runtime without `DisplayNames`: the shipped name.\n }\n }\n return c.name;\n}\n\n/** `onChange` is the kit's — \"a currency was picked\", carrying the code — rather\n * than the div's form event. */\nexport interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n value: string;\n onChange: (code: string) => void;\n placeholder?: string;\n label?: ReactNode;\n /**\n * Restrict the pickable currencies to these ISO codes (e.g. only the\n * currencies actually present in a budget). Order is preserved. Defaults to\n * the full {@link CURRENCIES} list.\n */\n options?: string[];\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /** Every user-facing string this control owns, shaped like `AmountInput`'s\n * `labels` so the two currency pickers in the kit are configured the same way.\n *\n * `search` exists because `placeholder` used to do BOTH jobs: it named the empty\n * field and, for want of anything else, it was also handed to the search box —\n * so a caller who wrote \"Currency\" got \"Currency\" as the prompt for typing a\n * filter, and a caller who wanted to name the filter had to rename the field.\n * `placeholder` still wins for the trigger, so nothing moves for the callers\n * that set it; what changes is that the search box no longer borrows it. */\n labels?: {\n /** The empty trigger, when `placeholder` is not given. */\n currency?: string;\n /** The filter box inside the open panel. */\n search?: string;\n };\n /**\n * Translated currency names, keyed by ISO code — `{ CHF: \"Schweizer Franken\" }`.\n *\n * {@link CURRENCIES} is data the package ships, so its 27 names were the one\n * string in the row a consumer could not translate: the code and the symbol are\n * language-neutral, and the name beside them stayed English in a German UI. This\n * is a sparse OVERRIDE rather than a replacement list, so an app translates the\n * eight currencies it actually uses and keeps the rest; forking `CURRENCIES` to\n * get there would also fork the flags and symbols, which nobody wants to maintain.\n *\n * The search matches what is SHOWN — type \"Franken\" and you find CHF — because a\n * filter that only matches the hidden English name is worse than no filter.\n */\n currencyNames?: Record<string, string>;\n}\n\nexport function CurrencySelect({\n value,\n onChange,\n placeholder,\n className,\n label,\n options,\n invalid,\n labels,\n currencyNames,\n \"aria-label\": ariaLabel,\n ...rest\n}: CurrencySelectProps) {\n const { open, setOpen, wrapperRef, query, setQuery, inputRef } = useDropdownSearch();\n // `labels` > `currency` from the provider > English.\n const text = useKitLabels(\"currency\", DEFAULT_CURRENCY_LABELS, labels);\n const locale = useKitLocale();\n\n const selected = getCurrency(value);\n const pool = useMemo(() => {\n if (!options) return CURRENCIES;\n // Keep the caller's order; drop codes we don't have a definition for.\n return options.map((code) => getCurrency(code)).filter((c): c is CurrencyOption => !!c);\n }, [options]);\n const filtered = useMemo(() => {\n const q = query.trim().toLocaleLowerCase(locale);\n if (!q) return pool;\n return pool.filter(\n (c) =>\n c.code.toLowerCase().includes(q) ||\n currencyName(c, currencyNames, locale).toLocaleLowerCase(locale).includes(q) ||\n c.symbol.toLowerCase().includes(q),\n );\n }, [query, pool, currencyNames, locale]);\n\n const listboxId = `${useId()}-listbox`;\n\n return (\n // The wrapper takes `rest`; the trigger takes the name. This field's label is a\n // floating <span> rather than a <label for>, so a caller who wants the control to\n // announce more than the code inside it has nowhere else to put it.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n <button\n type=\"button\"\n aria-label={ariaLabel}\n onClick={() => setOpen((v) => !v)}\n // The implicit `button` role supports neither `aria-expanded` nor\n // `aria-invalid`, so this trigger previously wore a red border and announced\n // nothing about being invalid or about there being a list behind it. The role\n // that DESCRIBES it is the one that also supports the attributes — the same\n // treatment its siblings (MultiSelect, the entity comboboxes) now carry, so a\n // form row of pickers reads the same way throughout.\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-invalid={invalid || undefined}\n className={cn(FIELD_TRIGGER, \"pr-9\", label !== undefined && FIELD_FLOATING_PAD, invalid && FIELD_INVALID)}\n >\n {/* The trigger stays COMPACT — flag + code only — so it never clips in a\n narrow field; the full names live in the (wider) popup (feedback\n #308). */}\n <span className={cn(\"flex items-center gap-2 min-w-0\", selected ? \"text-[var(--text-primary)]\" : \"text-[var(--text-placeholder)]\")}>\n {selected ? (\n <>\n <CurrencyFlag country={selected.country} />\n <span className=\"font-medium\">{selected.code}</span>\n </>\n ) : (\n placeholder ?? text.currency\n )}\n </span>\n <FieldChevron />\n </button>\n {open && (\n <DropdownPanel\n // At least as wide as the trigger, but grows to fit the full currency\n // names and caps so it never runs off-screen. RIGHT-aligned so it grows\n // leftward instead of pushing the page width when there's content to the\n // right of the field (feedback #308).\n className=\"right-0 min-w-full w-max max-w-[min(20rem,calc(100vw-2rem))]\"\n empty={filtered.length === 0}\n listProps={{ id: listboxId }}\n header={\n <DropdownSearchHeader\n query={query}\n onQueryChange={setQuery}\n inputRef={inputRef}\n placeholder={text.search}\n />\n }\n >\n {filtered.map((c) => {\n const active = c.code === selected?.code;\n return (\n <li key={c.code}>\n <button\n type=\"button\"\n onClick={() => {\n onChange(c.code);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-2 text-sm text-left hover:bg-[var(--bg-hover)]\",\n active && \"bg-[var(--bg-active)]\",\n )}\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <CurrencyFlag country={c.country} />\n <span className=\"font-mono text-[var(--text-muted)] w-8 shrink-0\">{c.symbol}</span>\n <span className=\"font-medium text-[var(--text-primary)]\">{c.code}</span>\n <span className=\"text-[var(--text-muted)] truncate\">{currencyName(c, currencyNames, locale)}</span>\n </span>\n {active && <Check className=\"size-4 text-[var(--text-secondary)] shrink-0\" />}\n </button>\n </li>\n );\n })}\n </DropdownPanel>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/currency-select.tsx"],"sourcesContent":["import { useMemo, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from \"./dropdown\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_CURRENCY_LABELS,\n useKitLabels,\n useKitLocale,\n} from \"../i18n/kit-labels\";\n\nexport interface CurrencyOption {\n code: string;\n name: string;\n symbol: string;\n /** ISO 3166-1 alpha-2 country code (lowercase) used by flag-icons. */\n country: string;\n}\n\nexport const CURRENCIES: CurrencyOption[] = [\n { code: \"EUR\", name: \"Euro\", symbol: \"€\", country: \"eu\" },\n { code: \"USD\", name: \"US Dollar\", symbol: \"$\", country: \"us\" },\n { code: \"GBP\", name: \"British Pound\", symbol: \"£\", country: \"gb\" },\n { code: \"CHF\", name: \"Swiss Franc\", symbol: \"CHF\", country: \"ch\" },\n { code: \"JPY\", name: \"Japanese Yen\", symbol: \"¥\", country: \"jp\" },\n { code: \"CAD\", name: \"Canadian Dollar\", symbol: \"CA$\", country: \"ca\" },\n { code: \"AUD\", name: \"Australian Dollar\", symbol: \"A$\", country: \"au\" },\n { code: \"NZD\", name: \"New Zealand Dollar\", symbol: \"NZ$\", country: \"nz\" },\n { code: \"SEK\", name: \"Swedish Krona\", symbol: \"kr\", country: \"se\" },\n { code: \"NOK\", name: \"Norwegian Krone\", symbol: \"kr\", country: \"no\" },\n { code: \"DKK\", name: \"Danish Krone\", symbol: \"kr\", country: \"dk\" },\n { code: \"PLN\", name: \"Polish Zloty\", symbol: \"zł\", country: \"pl\" },\n { code: \"CZK\", name: \"Czech Koruna\", symbol: \"Kč\", country: \"cz\" },\n { code: \"HUF\", name: \"Hungarian Forint\", symbol: \"Ft\", country: \"hu\" },\n { code: \"RON\", name: \"Romanian Leu\", symbol: \"lei\", country: \"ro\" },\n { code: \"BGN\", name: \"Bulgarian Lev\", symbol: \"лв\", country: \"bg\" },\n { code: \"TRY\", name: \"Turkish Lira\", symbol: \"₺\", country: \"tr\" },\n { code: \"RUB\", name: \"Russian Ruble\", symbol: \"₽\", country: \"ru\" },\n { code: \"CNY\", name: \"Chinese Yuan\", symbol: \"¥\", country: \"cn\" },\n { code: \"HKD\", name: \"Hong Kong Dollar\", symbol: \"HK$\", country: \"hk\" },\n { code: \"SGD\", name: \"Singapore Dollar\", symbol: \"S$\", country: \"sg\" },\n { code: \"INR\", name: \"Indian Rupee\", symbol: \"₹\", country: \"in\" },\n { code: \"BRL\", name: \"Brazilian Real\", symbol: \"R$\", country: \"br\" },\n { code: \"MXN\", name: \"Mexican Peso\", symbol: \"MX$\", country: \"mx\" },\n { code: \"ZAR\", name: \"South African Rand\", symbol: \"R\", country: \"za\" },\n { code: \"ILS\", name: \"Israeli Shekel\", symbol: \"₪\", country: \"il\" },\n { code: \"AED\", name: \"UAE Dirham\", symbol: \"د.إ\", country: \"ae\" },\n];\n\nexport interface CurrencyFlagProps extends ComponentPropsWithoutRef<\"span\"> {\n /** ISO 3166-1 alpha-2, lowercase — the class flag-icons keys its sprite by. */\n country: string;\n}\n\nexport function CurrencyFlag({ country, className, ...rest }: CurrencyFlagProps) {\n return (\n <span\n {...rest}\n // Stays `aria-hidden` whatever a caller passes: the flag is a picture of the\n // code printed next to it, and a reader that met both would hear the currency\n // twice.\n aria-hidden\n className={cn(\"fi inline-block w-5 h-[15px] rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)] shrink-0\", `fi-${country}`, className)}\n />\n );\n}\n\nconst CURRENCY_BY_CODE = new Map(CURRENCIES.map((c) => [c.code, c]));\n\nexport function getCurrency(code: string | null | undefined): CurrencyOption | undefined {\n return code ? CURRENCY_BY_CODE.get(code.toUpperCase()) : undefined;\n}\n\n/** The name to SHOW for a currency: the caller's translation if they have one for\n * that code; else, when a `locale` is known, the name `Intl.DisplayNames` has for\n * it in that locale; else the English name {@link CURRENCIES} carries. Shared with\n * AmountInput, which renders the same list from the same data.\n *\n * `Intl` only when a locale was actually GIVEN (by prop or `<UiKitProvider>`): an\n * app that never set one keeps the shipped English names it has always shown,\n * rather than whatever language the test runner or the browser happens to be in. */\nexport function currencyName(\n c: CurrencyOption,\n names?: Record<string, string>,\n locale?: string,\n): string {\n const own = names?.[c.code];\n if (own !== undefined) return own;\n if (locale) {\n try {\n const shown = new Intl.DisplayNames([locale], { type: \"currency\" }).of(c.code);\n if (shown && shown !== c.code) return shown;\n } catch {\n // A malformed tag, or a runtime without `DisplayNames`: the shipped name.\n }\n }\n return c.name;\n}\n\n/** `onChange` is the kit's — \"a currency was picked\", carrying the code — rather\n * than the div's form event. */\nexport interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n value: string;\n onChange: (code: string) => void;\n placeholder?: string;\n label?: ReactNode;\n /**\n * Restrict the pickable currencies to these ISO codes (e.g. only the\n * currencies actually present in a budget). Order is preserved. Defaults to\n * the full {@link CURRENCIES} list.\n */\n options?: string[];\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /** Every user-facing string this control owns, shaped like `AmountInput`'s\n * `labels` so the two currency pickers in the kit are configured the same way.\n *\n * `search` exists because `placeholder` used to do BOTH jobs: it named the empty\n * field and, for want of anything else, it was also handed to the search box —\n * so a caller who wrote \"Currency\" got \"Currency\" as the prompt for typing a\n * filter, and a caller who wanted to name the filter had to rename the field.\n * `placeholder` still wins for the trigger, so nothing moves for the callers\n * that set it; what changes is that the search box no longer borrows it. */\n labels?: {\n /** The empty trigger, when `placeholder` is not given. */\n currency?: string;\n /** The filter box inside the open panel. */\n search?: string;\n };\n /**\n * Translated currency names, keyed by ISO code — `{ CHF: \"Schweizer Franken\" }`.\n *\n * {@link CURRENCIES} is data the package ships, so its 27 names were the one\n * string in the row a consumer could not translate: the code and the symbol are\n * language-neutral, and the name beside them stayed English in a German UI. This\n * is a sparse OVERRIDE rather than a replacement list, so an app translates the\n * eight currencies it actually uses and keeps the rest; forking `CURRENCIES` to\n * get there would also fork the flags and symbols, which nobody wants to maintain.\n *\n * The search matches what is SHOWN — type \"Franken\" and you find CHF — because a\n * filter that only matches the hidden English name is worse than no filter.\n */\n currencyNames?: Record<string, string>;\n}\n\nexport function CurrencySelect({\n value,\n onChange,\n placeholder,\n className,\n label,\n options,\n invalid,\n labels,\n currencyNames,\n \"aria-label\": ariaLabel,\n ...rest\n}: CurrencySelectProps) {\n const { open, setOpen, wrapperRef, query, setQuery, inputRef } = useDropdownSearch();\n // `labels` > `currency` from the provider > English.\n const text = useKitLabels(\"currency\", DEFAULT_CURRENCY_LABELS, labels);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale();\n\n const selected = getCurrency(value);\n const pool = useMemo(() => {\n if (!options) return CURRENCIES;\n // Keep the caller's order; drop codes we don't have a definition for.\n return options.map((code) => getCurrency(code)).filter((c): c is CurrencyOption => !!c);\n }, [options]);\n const filtered = useMemo(() => {\n const q = query.trim().toLocaleLowerCase(locale);\n if (!q) return pool;\n return pool.filter(\n (c) =>\n c.code.toLowerCase().includes(q) ||\n currencyName(c, currencyNames, locale).toLocaleLowerCase(locale).includes(q) ||\n c.symbol.toLowerCase().includes(q),\n );\n }, [query, pool, currencyNames, locale]);\n\n const listboxId = `${useId()}-listbox`;\n\n // The trigger's NAME. A combobox does not take its name from its content, and the\n // floating label is a <span>, not a <label for> — so without `aria-label` from the\n // caller this trigger had no name at all (keksdose's test suite, 0.5.0). Composed the\n // way MultiSelect and the entity comboboxes compose theirs, \"label: value\", with the\n // `currency` word standing in for a missing label so it is never nameless.\n const fieldName = typeof label === \"string\" ? label : text.currency;\n const name = ariaLabel ?? common.fieldValue(fieldName, selected ? selected.code : (placeholder ?? text.currency));\n\n return (\n // The wrapper takes `rest`; the trigger takes the name. This field's label is a\n // floating <span> rather than a <label for>, so a caller who wants the control to\n // announce more than the code inside it has nowhere else to put it.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n <button\n type=\"button\"\n aria-label={name}\n onClick={() => setOpen((v) => !v)}\n // The implicit `button` role supports neither `aria-expanded` nor\n // `aria-invalid`, so this trigger previously wore a red border and announced\n // nothing about being invalid or about there being a list behind it. The role\n // that DESCRIBES it is the one that also supports the attributes — the same\n // treatment its siblings (MultiSelect, the entity comboboxes) now carry, so a\n // form row of pickers reads the same way throughout.\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-invalid={invalid || undefined}\n className={cn(FIELD_TRIGGER, \"pr-9\", label !== undefined && FIELD_FLOATING_PAD, invalid && FIELD_INVALID)}\n >\n {/* The trigger stays COMPACT — flag + code only — so it never clips in a\n narrow field; the full names live in the (wider) popup (feedback\n #308). */}\n <span className={cn(\"flex items-center gap-2 min-w-0\", selected ? \"text-[var(--text-primary)]\" : \"text-[var(--text-placeholder)]\")}>\n {selected ? (\n <>\n <CurrencyFlag country={selected.country} />\n <span className=\"font-medium\">{selected.code}</span>\n </>\n ) : (\n placeholder ?? text.currency\n )}\n </span>\n <FieldChevron />\n </button>\n {open && (\n <DropdownPanel\n // At least as wide as the trigger, but grows to fit the full currency\n // names and caps so it never runs off-screen. RIGHT-aligned so it grows\n // leftward instead of pushing the page width when there's content to the\n // right of the field (feedback #308).\n className=\"right-0 min-w-full w-max max-w-[min(20rem,calc(100vw-2rem))]\"\n empty={filtered.length === 0}\n listProps={{ id: listboxId }}\n header={\n <DropdownSearchHeader\n query={query}\n onQueryChange={setQuery}\n inputRef={inputRef}\n placeholder={text.search}\n />\n }\n >\n {filtered.map((c) => {\n const active = c.code === selected?.code;\n return (\n <li key={c.code}>\n <button\n type=\"button\"\n onClick={() => {\n onChange(c.code);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-2 text-sm text-left hover:bg-[var(--bg-hover)]\",\n active && \"bg-[var(--bg-active)]\",\n )}\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <CurrencyFlag country={c.country} />\n <span className=\"font-mono text-[var(--text-muted)] w-8 shrink-0\">{c.symbol}</span>\n <span className=\"font-medium text-[var(--text-primary)]\">{c.code}</span>\n <span className=\"text-[var(--text-muted)] truncate\">{currencyName(c, currencyNames, locale)}</span>\n </span>\n {active && <Check className=\"size-4 text-[var(--text-secondary)] shrink-0\" />}\n </button>\n </li>\n );\n })}\n </DropdownPanel>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every string the tile renders — the `dangerConfirm` namespace of
|
|
6
|
+
* `<UiKitProvider labels>`, overridable per instance through `labels`. The arm and
|
|
7
|
+
* confirm buttons are usually worded per action ("Delete budget"), which is what the
|
|
8
|
+
* `armLabel` / `confirmLabel` props are for; these are the fallbacks.
|
|
9
|
+
*/
|
|
10
|
+
interface DangerConfirmLabels {
|
|
11
|
+
/** The button that arms the tile. */
|
|
12
|
+
arm: string;
|
|
13
|
+
/** The button that runs the action once the guards are satisfied. */
|
|
14
|
+
confirm: string;
|
|
15
|
+
cancel: string;
|
|
16
|
+
/** The warning above the fields, when no `prompt` is given. */
|
|
17
|
+
prompt: string;
|
|
18
|
+
/** Label of the password field (`requirePassword`). */
|
|
19
|
+
password: string;
|
|
20
|
+
/** Label of the type-to-confirm field, given the phrase to type. A FUNCTION, like
|
|
21
|
+
* every message that carries a value: where the phrase sits in the sentence moves
|
|
22
|
+
* with the language. */
|
|
23
|
+
phrase: (phrase: string) => string;
|
|
24
|
+
}
|
|
25
|
+
declare const DEFAULT_DANGER_CONFIRM_LABELS: DangerConfirmLabels;
|
|
26
|
+
interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
|
|
27
|
+
/**
|
|
28
|
+
* Runs the action, with the entered password when `requirePassword` is on.
|
|
29
|
+
*
|
|
30
|
+
* Return a promise and the tile manages itself: busy until it settles, disarmed
|
|
31
|
+
* (fields wiped) when it resolves, still armed when it rejects — so the user can
|
|
32
|
+
* correct a wrong password and retry. The rejection is not swallowed for you to
|
|
33
|
+
* miss: handle it (and show why) in the caller, as with any mutation.
|
|
34
|
+
*/
|
|
35
|
+
onConfirm: (password?: string) => void | Promise<unknown>;
|
|
36
|
+
/** Show a password field; confirm stays disabled until it is filled. */
|
|
37
|
+
requirePassword?: boolean;
|
|
38
|
+
/** Show a "type <phrase> to confirm" field; confirm stays disabled until the field
|
|
39
|
+
* matches exactly (case-sensitive, surrounding spaces ignored). */
|
|
40
|
+
phrase?: string;
|
|
41
|
+
/** The warning above the fields. Defaults to `labels.prompt`. */
|
|
42
|
+
prompt?: ReactNode;
|
|
43
|
+
/** `"danger"` (default) for what cannot be undone; `"warning"` for what can, at a
|
|
44
|
+
* cost (loading demo data over your own). Colours the prompt and the confirm. */
|
|
45
|
+
tone?: "danger" | "warning";
|
|
46
|
+
/** Visible text of the arm button; defaults to `labels.arm`. */
|
|
47
|
+
armLabel?: ReactNode;
|
|
48
|
+
/** Visible text of the confirm button; defaults to `labels.confirm`. */
|
|
49
|
+
confirmLabel?: ReactNode;
|
|
50
|
+
/** The action is running: confirm shows a spinner and nothing can be pressed. For a
|
|
51
|
+
* caller that tracks the mutation itself (a `useMutation`'s `isPending`); a
|
|
52
|
+
* promise returned from `onConfirm` does the same on its own. */
|
|
53
|
+
busy?: boolean;
|
|
54
|
+
/** The arm button is disabled. */
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Why the action is not available — a read-only demo, a write lock, a missing
|
|
58
|
+
* permission. Disables the arm button like `disabled`, and SAYS so: the sentence is
|
|
59
|
+
* shown under the button and attached to it with `aria-describedby`, and the button
|
|
60
|
+
* stays focusable (`aria-disabled`) so a keyboard user can land on it and hear why.
|
|
61
|
+
*
|
|
62
|
+
* On the ARM button rather than the confirm: arming asks for a password, and asking
|
|
63
|
+
* for a password for a write that can never land is the worse of the two.
|
|
64
|
+
*/
|
|
65
|
+
lockedReason?: ReactNode;
|
|
66
|
+
/** Controlled armed state. The parent can then collapse the tile from a mutation's
|
|
67
|
+
* own `onSuccess` without returning a promise. */
|
|
68
|
+
armed?: boolean;
|
|
69
|
+
/** Called with the next armed state — on arm, cancel, and a resolved `onConfirm`. */
|
|
70
|
+
onArmedChange?: (armed: boolean) => void;
|
|
71
|
+
/** User-facing strings; see {@link DangerConfirmLabels}. */
|
|
72
|
+
labels?: Partial<DangerConfirmLabels>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* An "arm → confirm" tile for destructive actions: one button, which expands into a
|
|
76
|
+
* warning, an optional password field, an optional type-to-confirm field and a
|
|
77
|
+
* confirm that stays disabled until every guard is satisfied.
|
|
78
|
+
*
|
|
79
|
+
* Keksdose hand-rolled it three times (load demo data, wipe everything, reset a
|
|
80
|
+
* budget) and then as `shared/components/danger-confirm.tsx`; the only app-specific
|
|
81
|
+
* part was its write-lock hook, which is `lockedReason` here.
|
|
82
|
+
*
|
|
83
|
+
* The fields are a `<form>`, so Enter confirms once the guards allow it. They are
|
|
84
|
+
* wiped every time the tile disarms — a password must not sit in a collapsed tile.
|
|
85
|
+
* Arming moves focus to the first field (or Cancel, when there is none — never to the
|
|
86
|
+
* destructive button itself), and disarming moves it back to the arm button, so the
|
|
87
|
+
* keyboard user is never left on an element that just vanished.
|
|
88
|
+
*/
|
|
89
|
+
declare function DangerConfirm({ onConfirm, requirePassword, phrase, prompt, tone, armLabel, confirmLabel, busy: busyProp, disabled, lockedReason, armed: armedProp, onArmedChange, labels: labelsProp, className, ...rest }: DangerConfirmProps): react.JSX.Element;
|
|
90
|
+
|
|
91
|
+
export { DEFAULT_DANGER_CONFIRM_LABELS, DangerConfirm, type DangerConfirmLabels, type DangerConfirmProps };
|