@eifi1/ui-kit 0.49.0 → 0.50.1
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 +12 -8
- package/dist/admin/user-roster.d.ts +16 -10
- package/dist/admin/user-roster.js +7 -3
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/verify-email.d.ts +9 -1
- package/dist/auth/verify-email.js +3 -1
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -1
- package/dist/components/amount-input.js +3 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +14 -0
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/combobox-core.js +1 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox-suggestions.js +1 -0
- package/dist/components/combobox-suggestions.js.map +1 -1
- package/dist/components/combobox.js +3 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.d.ts +28 -2
- package/dist/components/currency-select.js +56 -10
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +11 -2
- package/dist/components/data-table-mobile.js +62 -8
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-row.d.ts +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table.js +3 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/dropdown.js +1 -0
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/full-bleed-dialog.js +1 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +12 -0
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/modal.js +1 -0
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-input.d.ts +2 -1
- package/dist/components/number-input.js +6 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +2 -0
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -0
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +1 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +14 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +16 -1
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +14 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +14 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +14 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +14 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +14 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +1 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/lib/data-attributes.d.ts +11 -0
- package/dist/lib/data-attributes.js +10 -0
- package/dist/lib/data-attributes.js.map +1 -0
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.js +2 -0
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/minus.d.ts +10 -0
- package/dist/lib/minus.js.map +1 -1
- package/dist/lib/quiet-focus.d.ts +4 -0
- package/dist/lib/quiet-focus.js +19 -0
- package/dist/lib/quiet-focus.js.map +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/react.d.ts +25 -0
- package/dist/testing/react.js +19 -0
- package/dist/testing/react.js.map +1 -0
- package/dist/testing/tour.d.ts +31 -0
- package/dist/testing/tour.js +12 -0
- package/dist/testing/tour.js.map +1 -0
- package/dist/tour/tour-dock.d.ts +42 -0
- package/dist/tour/tour-dock.js +80 -0
- package/dist/tour/tour-dock.js.map +1 -0
- package/dist/tour/tour-engine.d.ts +12 -0
- package/dist/tour/tour-engine.js +31 -0
- package/dist/tour/tour-engine.js.map +1 -0
- package/dist/tour/tour-gates.d.ts +71 -0
- package/dist/tour/tour-gates.js +114 -0
- package/dist/tour/tour-gates.js.map +1 -0
- package/dist/tour/tour-guide-button.d.ts +27 -0
- package/dist/tour/tour-guide-button.js +45 -0
- package/dist/tour/tour-guide-button.js.map +1 -0
- package/dist/tour/tour-guide-labels.d.ts +8 -0
- package/dist/tour/tour-guide-labels.js +9 -0
- package/dist/tour/tour-guide-labels.js.map +1 -0
- package/dist/tour/tour-labels.js +9 -1
- package/dist/tour/tour-labels.js.map +1 -1
- package/dist/tour/tour.d.ts +144 -9
- package/dist/tour/tour.js +598 -59
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-url-sync.d.ts +7 -2
- package/dist/tour/use-tour-url-sync.js +27 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.js +15 -2
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +11 -0
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +11 -5
- package/dist/wizard/use-wizard.js.map +1 -1
- package/package.json +9 -1
- package/src/admin/user-roster.tsx +22 -12
- package/src/auth/verify-email.tsx +10 -0
- package/src/components/amount-input.tsx +5 -4
- package/src/components/autocomplete.tsx +20 -0
- package/src/components/combobox-core.tsx +2 -0
- package/src/components/combobox-suggestions.tsx +2 -0
- package/src/components/combobox.tsx +5 -1
- package/src/components/currency-select.tsx +95 -6
- package/src/components/data-table-mobile.tsx +99 -8
- package/src/components/data-table-row.tsx +1 -0
- package/src/components/data-table.tsx +9 -1
- package/src/components/dropdown.tsx +2 -0
- package/src/components/full-bleed-dialog.tsx +3 -0
- package/src/components/inline-entity-combobox.tsx +17 -0
- package/src/components/modal.tsx +3 -0
- package/src/components/number-input.tsx +8 -3
- package/src/components/numpad-sheet.tsx +4 -0
- package/src/components/picker-sheet.tsx +4 -0
- package/src/components/popover.tsx +2 -0
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +13 -0
- package/src/i18n/kit-labels.tsx +3 -0
- package/src/i18n/locales/es.ts +13 -0
- package/src/i18n/locales/fr.ts +13 -0
- package/src/i18n/locales/hu.ts +13 -0
- package/src/i18n/locales/it.ts +13 -0
- package/src/i18n/locales/zh.ts +13 -0
- package/src/i18n/review.ts +1 -0
- package/src/lib/data-attributes.ts +14 -0
- package/src/lib/format.ts +2 -0
- package/src/lib/minus.ts +10 -0
- package/src/lib/quiet-focus.ts +32 -0
- package/src/testing/index.ts +8 -0
- package/src/testing/react.ts +59 -0
- package/src/testing/tour.ts +41 -0
- package/src/tour/tour-dock.ts +132 -0
- package/src/tour/tour-engine.ts +41 -0
- package/src/tour/tour-gates.ts +195 -0
- package/src/tour/tour-guide-button.tsx +64 -0
- package/src/tour/tour-guide-labels.ts +15 -0
- package/src/tour/tour-labels.ts +8 -0
- package/src/tour/tour.tsx +989 -86
- package/src/tour/use-tour-url-sync.ts +43 -4
- package/src/tour.ts +3 -0
- package/src/wizard/stepper-nav.tsx +23 -3
- package/src/wizard/types.ts +12 -0
- package/src/wizard/use-wizard.ts +16 -9
|
@@ -41,10 +41,11 @@ import {
|
|
|
41
41
|
} from "./field-parts";
|
|
42
42
|
import { useEndWidth, useNumericField } from "./numeric-field";
|
|
43
43
|
import type { CommitScope } from "./write-lock";
|
|
44
|
+
import { dataAttributes, type DataAttributes } from "../lib/data-attributes";
|
|
44
45
|
|
|
45
46
|
/** {@link AmountInput}'s props — exported since 0.34 (refactor audit 2026-10, API-9): a
|
|
46
47
|
* wrapper typed itself off `ComponentProps<typeof AmountInput>` until then. */
|
|
47
|
-
export interface AmountInputProps extends FieldHelpProps {
|
|
48
|
+
export interface AmountInputProps extends FieldHelpProps, DataAttributes {
|
|
48
49
|
value: string;
|
|
49
50
|
onChange: (value: string) => void;
|
|
50
51
|
currency?: string;
|
|
@@ -427,8 +428,8 @@ export function AmountInput(props: AmountInputProps) {
|
|
|
427
428
|
const { lock, locked, off, isMobile, asDisplay, showCalc, showNumpad, setFocused, mark, draft, setDraft, derived } =
|
|
428
429
|
numeric;
|
|
429
430
|
const invalid = numeric.isInvalid;
|
|
430
|
-
// A locked chip is a read-out: picking a currency is a change too.
|
|
431
|
-
const editable = !!onCurrencyChange && !locked;
|
|
431
|
+
// A locked or disabled chip is a read-out: picking a currency is a change too.
|
|
432
|
+
const editable = !!onCurrencyChange && !locked && !off;
|
|
432
433
|
// Room for the trailing controls — the calculator and the currency chip — once
|
|
433
434
|
// measured, as NumberInput reserves its own (keksdose live #367, DUP-10): their
|
|
434
435
|
// width, the track's `end-1` inset in the boxed shape, and NumberInput's 4px gap.
|
|
@@ -533,7 +534,7 @@ export function AmountInput(props: AmountInputProps) {
|
|
|
533
534
|
const selected = getCurrency(currency);
|
|
534
535
|
|
|
535
536
|
return (
|
|
536
|
-
<div className={cn("w-full", className)}>
|
|
537
|
+
<div {...dataAttributes(props)} className={cn("w-full", className)}>
|
|
537
538
|
{/* Inner relative box holds the input + its absolutely-positioned
|
|
538
539
|
trailing controls (calculator / currency), so those stay anchored to
|
|
539
540
|
the INPUT even when the mobile math bar is rendered below (#334). It is
|
|
@@ -7,6 +7,7 @@ import { Spinner } from "./spinner";
|
|
|
7
7
|
import { useAnchoredPanel } from "../hooks/use-anchored-panel";
|
|
8
8
|
import { takeEscape, useDismissLayer } from "../hooks/use-dismiss";
|
|
9
9
|
import { isImeEnter } from "../lib/ime";
|
|
10
|
+
import { isQuietFocus } from "../lib/quiet-focus";
|
|
10
11
|
import { useAnchorDir } from "./use-anchor-dir";
|
|
11
12
|
import {
|
|
12
13
|
ComboboxFieldLabel,
|
|
@@ -268,6 +269,8 @@ export function Autocomplete<V extends string | number = string>(props: Autocomp
|
|
|
268
269
|
// either way, so a caller that controls only SOMETIMES (`open={confirming ? false :
|
|
269
270
|
// undefined}`) hands back a field that is exactly as open as focus says it is.
|
|
270
271
|
const open = openProp ?? ownOpen;
|
|
272
|
+
// The field took the tour's quiet focus (§11.8) and has not opened since.
|
|
273
|
+
const quietRef = useRef(false);
|
|
271
274
|
const setOpen = (next: boolean) => {
|
|
272
275
|
setOwnOpen(next);
|
|
273
276
|
if (next !== open) onOpenChange?.(next);
|
|
@@ -410,11 +413,26 @@ export function Autocomplete<V extends string | number = string>(props: Autocomp
|
|
|
410
413
|
onFocus?.(e);
|
|
411
414
|
// A locked field takes focus to say why, and opens nothing.
|
|
412
415
|
if (locked) return;
|
|
416
|
+
// The tour's focus on arrival (§11.8) looks nothing up: the list would
|
|
417
|
+
// cover its card (0.50.1). A click, ArrowDown or typing opens it.
|
|
418
|
+
if (isQuietFocus(e.currentTarget)) {
|
|
419
|
+
quietRef.current = true;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
413
422
|
// A seeded field (keksdose's `initialQuery`) is looked up as it is
|
|
414
423
|
// focused; an empty one asks nothing, because of `minChars`.
|
|
415
424
|
setOpen(true);
|
|
416
425
|
}}
|
|
426
|
+
// After a quiet focus, the person's click on the already-focused field
|
|
427
|
+
// fires no `focus`: the click is their "open" instead.
|
|
428
|
+
onClick={(e) => {
|
|
429
|
+
rest.onClick?.(e);
|
|
430
|
+
if (!quietRef.current || locked) return;
|
|
431
|
+
quietRef.current = false;
|
|
432
|
+
setOpen(true);
|
|
433
|
+
}}
|
|
417
434
|
onBlur={(e) => {
|
|
435
|
+
quietRef.current = false;
|
|
418
436
|
onBlur?.(e);
|
|
419
437
|
close();
|
|
420
438
|
}}
|
|
@@ -519,6 +537,8 @@ export function Autocomplete<V extends string | number = string>(props: Autocomp
|
|
|
519
537
|
// Every press inside the list keeps focus in the field — rows, the status
|
|
520
538
|
// line, the scrollbar — or the input's blur would close the list first.
|
|
521
539
|
role="presentation"
|
|
540
|
+
// A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).
|
|
541
|
+
data-kit-layer=""
|
|
522
542
|
// Portalled out of the form's `dir`; the field's is put back. The list is
|
|
523
543
|
// exactly the field's width, so `left` places it in either direction.
|
|
524
544
|
dir={dir}
|
|
@@ -1064,6 +1064,8 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
1064
1064
|
// `aria-describedby` has nothing to collide with.
|
|
1065
1065
|
{...rest}
|
|
1066
1066
|
ref={panelRef}
|
|
1067
|
+
// A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).
|
|
1068
|
+
data-kit-layer=""
|
|
1067
1069
|
dir={dir}
|
|
1068
1070
|
onKeyDown={onKeyDown}
|
|
1069
1071
|
// A press in a panel opened from a row is inside this list for the core's outside
|
|
@@ -92,6 +92,8 @@ export function SuggestionList({
|
|
|
92
92
|
return createPortal(
|
|
93
93
|
<div
|
|
94
94
|
ref={panelRef}
|
|
95
|
+
// A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).
|
|
96
|
+
data-kit-layer=""
|
|
95
97
|
// z-50, not the old z-30: the list is a child of <body> now, so it is
|
|
96
98
|
// competing with the app's own overlays rather than with its own siblings.
|
|
97
99
|
className="fixed z-50"
|
|
@@ -6,6 +6,7 @@ import { cn } from "../lib/cn";
|
|
|
6
6
|
import { useDropdown } from "./dropdown";
|
|
7
7
|
import { takeEscape, useDismissLayer } from "../hooks/use-dismiss";
|
|
8
8
|
import { isImeEnter } from "../lib/ime";
|
|
9
|
+
import { isQuietFocus } from "../lib/quiet-focus";
|
|
9
10
|
import { usePhoneLayout } from "../hooks/use-breakpoint";
|
|
10
11
|
import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet";
|
|
11
12
|
import { ComboboxFieldLabel, endHintRowProps, GROUP_HEADING_STICKY, useActiveOptionScroll } from "./combobox-core";
|
|
@@ -421,11 +422,14 @@ export function Combobox(props: ComboboxProps) {
|
|
|
421
422
|
autoFocus={autoFocus}
|
|
422
423
|
onBlur={onBlur}
|
|
423
424
|
onMouseDown={primaryOnly.onMouseDown}
|
|
424
|
-
onFocus={() => {
|
|
425
|
+
onFocus={(e) => {
|
|
425
426
|
// A back/forward mouse button focuses this field on its way to
|
|
426
427
|
// navigating; it is not a request to open anything (live #309 rework).
|
|
427
428
|
// A locked field takes focus to say why, and opens nothing.
|
|
428
429
|
if (primaryOnly.fromAuxButton() || locked) return;
|
|
430
|
+
// The tour's focus on arrival (§11.8) opens nothing: the list would cover
|
|
431
|
+
// its card (0.50.1). The click handler below still opens on a click.
|
|
432
|
+
if (isQuietFocus(e.currentTarget)) return;
|
|
429
433
|
setOpen(true);
|
|
430
434
|
setTyping(false);
|
|
431
435
|
// The sheet carries its own input, so the field behind it must not also
|
|
@@ -2,7 +2,16 @@ import { useCallback, useId } from "react";
|
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
|
|
3
3
|
import { FieldChevron, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from "./field-base";
|
|
4
4
|
import { FieldLabel } from "./field-parts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
assignRef,
|
|
7
|
+
EndHintRow,
|
|
8
|
+
LockedReason,
|
|
9
|
+
useFieldMessages,
|
|
10
|
+
useLockReason,
|
|
11
|
+
type DisabledReasonDisplay,
|
|
12
|
+
type FieldHelpProps,
|
|
13
|
+
} from "./field-parts";
|
|
14
|
+
import type { CommitScope } from "./write-lock";
|
|
6
15
|
import { cn } from "../lib/cn";
|
|
7
16
|
import { useComboboxCore } from "./combobox-core";
|
|
8
17
|
import {
|
|
@@ -90,6 +99,31 @@ export interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div"
|
|
|
90
99
|
* filter that only matches the hidden English name is worse than no filter.
|
|
91
100
|
*/
|
|
92
101
|
currencyNames?: Record<string, string>;
|
|
102
|
+
/** As {@link Select}'s: no focus, no list, the settled look, no chevron. */
|
|
103
|
+
disabled?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Why the currency cannot be changed — {@link Select}'s `disabledReason` (keksdose,
|
|
106
|
+
* 2026-10-23: a budget's currency for a guest or a viewer, and while offline, which
|
|
107
|
+
* keksdose drew as a one-option `Select` to keep the reason and lost the flag). The
|
|
108
|
+
* trigger stays focusable and `aria-disabled` (never the `disabled` attribute), keeps
|
|
109
|
+
* its flag and code, opens nothing, and `onChange` is never called; the reason is a
|
|
110
|
+
* line under the field at Large and on a touch screen, else in the kit
|
|
111
|
+
* {@link Tooltip} (`disabledReasonDisplay`), and on the trigger's `aria-describedby`
|
|
112
|
+
* (after a caption, before an error). It wears the settled look of a disabled field
|
|
113
|
+
* and drops its chevron, as a locked Select does. It wins over `disabled`.
|
|
114
|
+
*/
|
|
115
|
+
disabledReason?: ReactNode;
|
|
116
|
+
/** Where the reason shows — {@link Select}'s `disabledReasonDisplay`: a line under the
|
|
117
|
+
* field at Large and on a touch screen by default, the tooltip otherwise. */
|
|
118
|
+
disabledReasonDisplay?: DisabledReasonDisplay;
|
|
119
|
+
/**
|
|
120
|
+
* This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is
|
|
121
|
+
* locked the `disabledReason` way with the lock's reason (which wins over its own).
|
|
122
|
+
* No provider, or an unlocked one: no effect. {@link Select}'s `commit`. A currency
|
|
123
|
+
* field inside a form with its own Save stays editable under the lock — leave this off
|
|
124
|
+
* there, and put `commit` on the Save.
|
|
125
|
+
*/
|
|
126
|
+
commit?: CommitScope;
|
|
93
127
|
/** The TRIGGER — the focusable `<button>` — as a React 19 ref prop, as on
|
|
94
128
|
* {@link MultiSelect} and {@link CountrySelect}: react-hook-form's focus-on-error
|
|
95
129
|
* calls `focus()` on it (0.34, API-3, refactor audit 2026-10). Not the wrapper. */
|
|
@@ -109,6 +143,10 @@ export function CurrencySelect({
|
|
|
109
143
|
help,
|
|
110
144
|
labels,
|
|
111
145
|
currencyNames,
|
|
146
|
+
disabled,
|
|
147
|
+
disabledReason,
|
|
148
|
+
disabledReasonDisplay,
|
|
149
|
+
commit,
|
|
112
150
|
"aria-label": ariaLabel,
|
|
113
151
|
"aria-describedby": ariaDescribedBy,
|
|
114
152
|
"aria-invalid": ariaInvalid,
|
|
@@ -137,6 +175,15 @@ export function CurrencySelect({
|
|
|
137
175
|
|
|
138
176
|
const selected = getCurrency(value);
|
|
139
177
|
|
|
178
|
+
// The lock — Select's (`useLockReason`): the WriteLock's reason under `commit`, else
|
|
179
|
+
// the caller's own. Locked or `disabled`, nothing opens the list or picks a value.
|
|
180
|
+
const lock = useLockReason(commit, disabledReason);
|
|
181
|
+
const locked = lock.locked;
|
|
182
|
+
const inert = locked || Boolean(disabled);
|
|
183
|
+
// A lock (or `disabled`) arriving while the list is up closes it — adjusted while
|
|
184
|
+
// rendering, as CountrySelect does, so no frame shows a list that cannot be used.
|
|
185
|
+
if (inert && open) setOpen(false);
|
|
186
|
+
|
|
140
187
|
const listboxId = `${useId()}-listbox`;
|
|
141
188
|
// Text is a caption under the field; anything else (a FieldHint) rides the label
|
|
142
189
|
// line — and a FieldHint is that caption at Large and on touch (§4). See `hint`. On
|
|
@@ -152,6 +199,8 @@ export function CurrencySelect({
|
|
|
152
199
|
invalid,
|
|
153
200
|
describedBy: ariaDescribedBy,
|
|
154
201
|
ariaInvalid,
|
|
202
|
+
// A lock's reason sits between the caption and the error, as on Select.
|
|
203
|
+
reasonId: locked && lock.reasonId,
|
|
155
204
|
});
|
|
156
205
|
const { labelHint, describedBy, isInvalid } = field;
|
|
157
206
|
|
|
@@ -169,10 +218,21 @@ export function CurrencySelect({
|
|
|
169
218
|
ref={setTrigger}
|
|
170
219
|
type="button"
|
|
171
220
|
aria-label={name}
|
|
172
|
-
|
|
221
|
+
// The focusable kind of disabled while locked (Select's rule): native `disabled`
|
|
222
|
+
// would take the field out of the tab order, and the reason with it.
|
|
223
|
+
disabled={locked ? undefined : disabled}
|
|
224
|
+
aria-disabled={locked || undefined}
|
|
225
|
+
onClick={(e) => {
|
|
226
|
+
if (inert) {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
setOpen((v) => !v);
|
|
231
|
+
}}
|
|
173
232
|
// Down/Up opens the list from the closed trigger, per the APG; Enter and Space
|
|
174
233
|
// already do through the button's own click.
|
|
175
234
|
onKeyDown={(e) => {
|
|
235
|
+
if (inert) return;
|
|
176
236
|
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
177
237
|
e.preventDefault();
|
|
178
238
|
setOpen(true);
|
|
@@ -191,12 +251,27 @@ export function CurrencySelect({
|
|
|
191
251
|
aria-expanded={open}
|
|
192
252
|
aria-invalid={isInvalid || undefined}
|
|
193
253
|
aria-describedby={describedBy}
|
|
194
|
-
className={cn(
|
|
254
|
+
className={cn(
|
|
255
|
+
FIELD_TRIGGER,
|
|
256
|
+
"pe-9",
|
|
257
|
+
label !== undefined && FIELD_FLOATING_PAD,
|
|
258
|
+
// FIELD_BASE's `disabled:` look, for a trigger locked without being `disabled`
|
|
259
|
+
// (the `:disabled` variants cannot match it) — Select's FIELD_LOCKED. No hover:
|
|
260
|
+
// nothing here answers a press.
|
|
261
|
+
locked && "cursor-not-allowed bg-surface-2 text-muted hover:bg-surface-2",
|
|
262
|
+
isInvalid && FIELD_INVALID,
|
|
263
|
+
)}
|
|
195
264
|
>
|
|
196
265
|
{/* The trigger stays COMPACT — flag + code only — so it never clips in a
|
|
197
266
|
narrow field; the full names live in the (wider) popup (feedback
|
|
198
267
|
#308). */}
|
|
199
|
-
<span
|
|
268
|
+
<span
|
|
269
|
+
className={cn(
|
|
270
|
+
"flex items-center gap-2 min-w-0",
|
|
271
|
+
// The settled look reaches the code too: `text-primary` here would outweigh it.
|
|
272
|
+
!selected ? "text-placeholder" : inert ? "text-muted" : "text-primary",
|
|
273
|
+
)}
|
|
274
|
+
>
|
|
200
275
|
{selected ? (
|
|
201
276
|
<>
|
|
202
277
|
<CurrencyFlag country={selected.country} />
|
|
@@ -206,10 +281,23 @@ export function CurrencySelect({
|
|
|
206
281
|
placeholder ?? text.currency
|
|
207
282
|
)}
|
|
208
283
|
</span>
|
|
209
|
-
|
|
284
|
+
{/* No menu to drop while disabled or locked, so no chevron — the arrow is the one
|
|
285
|
+
thing on the control that promises a choice (dev#474, as on Select). */}
|
|
286
|
+
{!inert && <FieldChevron />}
|
|
210
287
|
</button>
|
|
211
288
|
);
|
|
212
289
|
|
|
290
|
+
// Select's frame: the line under the trigger, or the Tooltip with a hidden copy for
|
|
291
|
+
// `aria-describedby`. The wrapper `<div>` keeps `className`, so the layout holds.
|
|
292
|
+
// `flex w-full items-stretch`: the trigger keeps the field's full width in it.
|
|
293
|
+
const control = locked ? (
|
|
294
|
+
<LockedReason lock={lock} display={disabledReasonDisplay} className="flex w-full items-stretch">
|
|
295
|
+
{trigger}
|
|
296
|
+
</LockedReason>
|
|
297
|
+
) : (
|
|
298
|
+
trigger
|
|
299
|
+
);
|
|
300
|
+
|
|
213
301
|
return (
|
|
214
302
|
// The wrapper takes `rest`; the trigger takes the name. This field's label is a
|
|
215
303
|
// floating <span> rather than a <label for>, so a caller who wants the control to
|
|
@@ -230,7 +318,7 @@ export function CurrencySelect({
|
|
|
230
318
|
))}
|
|
231
319
|
{/* No label line to ride: at the field's end edge, outside the box, as an
|
|
232
320
|
unlabelled Select places its FieldHint. */}
|
|
233
|
-
<EndHintRow hint={label === undefined ? labelHint : undefined}>{
|
|
321
|
+
<EndHintRow hint={label === undefined ? labelHint : undefined}>{control}</EndHintRow>
|
|
234
322
|
{field.below}
|
|
235
323
|
{/* Portalled to <body> and anchored to the trigger (dev#548), so where it stands
|
|
236
324
|
in the flow no longer moves it. */}
|
|
@@ -246,6 +334,7 @@ export function CurrencySelect({
|
|
|
246
334
|
// to say what it asks for (live #200).
|
|
247
335
|
sheetTitle={label ?? text.currency}
|
|
248
336
|
onChoose={(code) => {
|
|
337
|
+
if (inert) return;
|
|
249
338
|
onChange(code);
|
|
250
339
|
// Back to the trigger: the panel holding focus is about to unmount, and a
|
|
251
340
|
// keyboard user who has answered the field should stand on it, ready to Tab on.
|
|
@@ -72,7 +72,18 @@ const CARD_NO_TRUNCATE =
|
|
|
72
72
|
* BETWEEN facts rather than between a label and its own value. `relative` holds a
|
|
73
73
|
* header's `sr-only` text (an actions column's), as every kit container must.
|
|
74
74
|
*/
|
|
75
|
-
export function StackedFields<T>({
|
|
75
|
+
export function StackedFields<T>({
|
|
76
|
+
columns,
|
|
77
|
+
row,
|
|
78
|
+
compact,
|
|
79
|
+
liftOwnClicks = false,
|
|
80
|
+
}: {
|
|
81
|
+
columns: DataTableColumn<T>[];
|
|
82
|
+
row: T;
|
|
83
|
+
compact: boolean;
|
|
84
|
+
/** On a stretched card (0.50 K4): lift the own-click fields above its title link. */
|
|
85
|
+
liftOwnClicks?: boolean;
|
|
86
|
+
}) {
|
|
76
87
|
const fields = filledFields(columns, row);
|
|
77
88
|
if (fields.length === 0) return null;
|
|
78
89
|
return (
|
|
@@ -85,7 +96,7 @@ export function StackedFields<T>({ columns, row, compact }: { columns: DataTable
|
|
|
85
96
|
<dt className="text-xs uppercase tracking-wide text-muted">{col.header}</dt>
|
|
86
97
|
<dd
|
|
87
98
|
{...dataAttrsOf(col.cellProps?.(row))}
|
|
88
|
-
className="mt-0.5 min-w-0 break-words text-secondary"
|
|
99
|
+
className={cn("mt-0.5 min-w-0 break-words text-secondary", liftOwnClicks && keepsOwnClicks(col) && LIFTED_FIELD)}
|
|
89
100
|
>
|
|
90
101
|
{value}
|
|
91
102
|
</dd>
|
|
@@ -95,6 +106,21 @@ export function StackedFields<T>({ columns, row, compact }: { columns: DataTable
|
|
|
95
106
|
);
|
|
96
107
|
}
|
|
97
108
|
|
|
109
|
+
/**
|
|
110
|
+
* A stretched card's own-click cell (0.50 K4): above the title control's overlay
|
|
111
|
+
* (`after:z-[1]`), so its link or buttons take their own taps. `w-fit` keeps the lifted
|
|
112
|
+
* box to the cell's content, so the field's empty rest still opens the row.
|
|
113
|
+
*/
|
|
114
|
+
const LIFTED_CELL = "relative z-[2]";
|
|
115
|
+
const LIFTED_FIELD = cn(LIFTED_CELL, "w-fit max-w-full justify-self-start");
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The keyboard frame of a stretched card (0.50 K4): FOCUS_RING's, inside the card,
|
|
119
|
+
* while its title control (`data-card-link`) has the focus.
|
|
120
|
+
*/
|
|
121
|
+
const STRETCHED_RING =
|
|
122
|
+
"has-[[data-card-link]:focus-visible]:ring-inset has-[[data-card-link]:focus-visible]:ring-focus has-[[data-card-link]:focus-visible]:ring-brand";
|
|
123
|
+
|
|
98
124
|
/** The most fields the default phone card shows under its title (0.49 KIT-7). */
|
|
99
125
|
export const MOBILE_CARD_FIELDS = 3;
|
|
100
126
|
|
|
@@ -308,6 +334,13 @@ export interface MobileRowProps<T> {
|
|
|
308
334
|
mobileDir: Direction;
|
|
309
335
|
/** No column the card renders owns a link, so the card itself may be one. */
|
|
310
336
|
mobileCardLinkable: boolean;
|
|
337
|
+
/**
|
|
338
|
+
* The card draws cells that keep their own clicks, so it cannot be one control (0.50
|
|
339
|
+
* K4): its TITLE is the row's link (`rowHref`) or button (`onRowClick`), stretched over
|
|
340
|
+
* the card, and those cells sit above it. Only for the default card, never a
|
|
341
|
+
* `mobileCard`, and never when the title itself keeps its own clicks.
|
|
342
|
+
*/
|
|
343
|
+
mobileCardStretched: boolean;
|
|
311
344
|
mobileCard: DataTableProps<T>["mobileCard"];
|
|
312
345
|
mobilePrimaryCol: DataTableColumn<T> | null;
|
|
313
346
|
/** The `role: "status"` columns, drawn as chips beside the title (0.49 KIT-7). */
|
|
@@ -349,6 +382,7 @@ export function MobileRow<T>({
|
|
|
349
382
|
swipeActionsFor,
|
|
350
383
|
mobileDir,
|
|
351
384
|
mobileCardLinkable,
|
|
385
|
+
mobileCardStretched,
|
|
352
386
|
mobileCard,
|
|
353
387
|
mobilePrimaryCol,
|
|
354
388
|
mobileStatusColumns,
|
|
@@ -389,7 +423,10 @@ export function MobileRow<T>({
|
|
|
389
423
|
// scrolling never meet the gesture. Until 0.40 an open card silently lost it.
|
|
390
424
|
// In selection mode a sideways drag would be a second meaning for a selecting touch.
|
|
391
425
|
const swipeEnabled = !!swipe && !selecting;
|
|
392
|
-
|
|
426
|
+
// The stretched card (0.50 K4): the title is the control, the card a plain box.
|
|
427
|
+
// Selection mode keeps its own card (the whole card is the toggle there).
|
|
428
|
+
const stretched = mobileCardStretched && interactive && !selecting && !mobileCard && !!mobilePrimaryCol;
|
|
429
|
+
const href = mobileCardLinkable || stretched ? rowHref?.(row) : undefined;
|
|
393
430
|
const cardClass = cn(
|
|
394
431
|
"w-full text-start flex items-center",
|
|
395
432
|
// §4: more room per row at Large — a taller row is a bigger tap target too.
|
|
@@ -409,7 +446,13 @@ export function MobileRow<T>({
|
|
|
409
446
|
// AppShell main's `overflow-x-clip` — so the frame went missing on one side. A press
|
|
410
447
|
// keeps the fill and the squeeze above; an outline on every tap of a long list would
|
|
411
448
|
// be louder than the acknowledgement it replaces.
|
|
412
|
-
(interactive || selecting) &&
|
|
449
|
+
(interactive || selecting) &&
|
|
450
|
+
!stretched &&
|
|
451
|
+
cn("[-webkit-tap-highlight-color:transparent] focus-visible:ring-inset", FOCUS_RING),
|
|
452
|
+
// A stretched card (0.50 K4) draws the same frame, inside the card, while its TITLE
|
|
453
|
+
// control has the keyboard: the ring outlines the whole tap target, as on every other
|
|
454
|
+
// card, not the title's line alone. The own-click cells draw their own.
|
|
455
|
+
stretched && cn("relative [-webkit-tap-highlight-color:transparent]", STRETCHED_RING),
|
|
413
456
|
selecting && "cursor-pointer",
|
|
414
457
|
select?.selected && selecting && "bg-brand-soft",
|
|
415
458
|
// A 500 ms hold would otherwise start a text selection, and iOS's link callout —
|
|
@@ -424,6 +467,45 @@ export function MobileRow<T>({
|
|
|
424
467
|
// exactly one anchor exists per row.
|
|
425
468
|
// The default card's filled fields and status values (0.49 KIT-7): an empty one
|
|
426
469
|
// gets no line. Not computed for a `mobileCard`, which draws its own.
|
|
470
|
+
// The stretched card's title control (0.50 K4): the row's link, or a real <button>
|
|
471
|
+
// when the table only has `onRowClick` — a plain control, since nothing interactive
|
|
472
|
+
// sits inside it any more. Its `::after` covers the whole card, so a tap anywhere
|
|
473
|
+
// but on an own-click cell (lifted above it) lands on it, and a long press on the
|
|
474
|
+
// card is a long press on the link ("open in new tab"). Its accessible name is the
|
|
475
|
+
// title alone; the fields read as text after it. `data-row-link` marks it as the
|
|
476
|
+
// row itself for `fromOwnControl`, so the long press still arms on it.
|
|
477
|
+
const stretchedClass =
|
|
478
|
+
"block w-full text-start focus-visible:outline-none after:absolute after:inset-0 after:z-[1] after:content-['']";
|
|
479
|
+
const titleControl = (title: ReactNode) =>
|
|
480
|
+
href ? (
|
|
481
|
+
<RowLink
|
|
482
|
+
href={href}
|
|
483
|
+
data-card-link=""
|
|
484
|
+
data-row-link=""
|
|
485
|
+
onActivate={() => onRowClick!(row)}
|
|
486
|
+
aria-expanded={opensRow ? expanded : undefined}
|
|
487
|
+
className={stretchedClass}
|
|
488
|
+
// Enter follows RowLink's own onClick; Space opens the row too, not the list's scroll.
|
|
489
|
+
onKeyDown={(e) => {
|
|
490
|
+
if (e.key !== " ") return;
|
|
491
|
+
e.preventDefault();
|
|
492
|
+
onRowClick!(row);
|
|
493
|
+
}}
|
|
494
|
+
>
|
|
495
|
+
{title}
|
|
496
|
+
</RowLink>
|
|
497
|
+
) : (
|
|
498
|
+
<button
|
|
499
|
+
type="button"
|
|
500
|
+
data-card-link=""
|
|
501
|
+
data-row-link=""
|
|
502
|
+
aria-expanded={opensRow ? expanded : undefined}
|
|
503
|
+
onClick={() => onRowClick!(row)}
|
|
504
|
+
className={cn(stretchedClass, "font-[inherit]")}
|
|
505
|
+
>
|
|
506
|
+
{title}
|
|
507
|
+
</button>
|
|
508
|
+
);
|
|
427
509
|
const fields = mobileCard ? [] : filledFields(mobileSecondaryColumns, row);
|
|
428
510
|
const statuses = mobileCard ? [] : filledFields(mobileStatusColumns, row);
|
|
429
511
|
const actionCells = mobileCard ? [] : filledFields(mobileActionColumns, row);
|
|
@@ -455,7 +537,7 @@ export function MobileRow<T>({
|
|
|
455
537
|
data-card-title=""
|
|
456
538
|
className="min-w-0 flex-1 break-words font-medium"
|
|
457
539
|
>
|
|
458
|
-
{mobilePrimaryCol.cell(row)}
|
|
540
|
+
{stretched ? titleControl(mobilePrimaryCol.cell(row)) : mobilePrimaryCol.cell(row)}
|
|
459
541
|
</div>
|
|
460
542
|
)}
|
|
461
543
|
{statuses.length > 0 && (
|
|
@@ -472,7 +554,7 @@ export function MobileRow<T>({
|
|
|
472
554
|
{fields.length > 0 && large ? (
|
|
473
555
|
// §4: each label ABOVE its value at Large. Side by side, a label column
|
|
474
556
|
// sized to the widest label leaves a value a sliver of a 240 px card.
|
|
475
|
-
<StackedFields columns={mobileSecondaryColumns} row={row} compact={compact} />
|
|
557
|
+
<StackedFields columns={mobileSecondaryColumns} row={row} compact={compact} liftOwnClicks={stretched} />
|
|
476
558
|
) : fields.length > 0 && (
|
|
477
559
|
<dl
|
|
478
560
|
className={cn(
|
|
@@ -487,7 +569,7 @@ export function MobileRow<T>({
|
|
|
487
569
|
</dt>
|
|
488
570
|
<dd
|
|
489
571
|
{...dataAttrsOf(col.cellProps?.(row))}
|
|
490
|
-
className="min-w-0 text-secondary self-center"
|
|
572
|
+
className={cn("min-w-0 text-secondary self-center", stretched && keepsOwnClicks(col) && LIFTED_FIELD)}
|
|
491
573
|
>
|
|
492
574
|
{value}
|
|
493
575
|
</dd>
|
|
@@ -500,7 +582,11 @@ export function MobileRow<T>({
|
|
|
500
582
|
{actionCells.length > 0 && (
|
|
501
583
|
<div data-card-actions="" className="flex min-w-0 flex-wrap items-center justify-end gap-2">
|
|
502
584
|
{actionCells.map(({ col, value }) => (
|
|
503
|
-
<div
|
|
585
|
+
<div
|
|
586
|
+
key={col.key}
|
|
587
|
+
{...dataAttrsOf(col.cellProps?.(row))}
|
|
588
|
+
className={cn("flex min-w-0 justify-end", stretched && LIFTED_CELL)}
|
|
589
|
+
>
|
|
504
590
|
{value}
|
|
505
591
|
</div>
|
|
506
592
|
))}
|
|
@@ -592,6 +678,11 @@ export function MobileRow<T>({
|
|
|
592
678
|
/>
|
|
593
679
|
{cardInner}
|
|
594
680
|
</div>
|
|
681
|
+
) : stretched ? (
|
|
682
|
+
// The stretched card (0.50 K4): a plain box; its title is the control (above).
|
|
683
|
+
<div {...press} data-card-stretched="" className={cardClass}>
|
|
684
|
+
{cardInner}
|
|
685
|
+
</div>
|
|
595
686
|
) : interactive && href ? (
|
|
596
687
|
// A card with a URL of its own is a LINK, not a div wearing role="button"
|
|
597
688
|
// (feedback #451): the phone's long-press "open in new tab" and a screen
|
|
@@ -1483,6 +1483,13 @@ export function DataTable<T>({
|
|
|
1483
1483
|
// renderer is opaque to us; a link inside one is the caller's to declare by
|
|
1484
1484
|
// marking the column it comes from.
|
|
1485
1485
|
const mobileCardLinkable = (mobileCard ? mobileColumns : [...onCard]).every((c) => !keepsOwnClicks(c));
|
|
1486
|
+
// ...except on the default card (0.50 K4, kastlan): its title becomes the row's link
|
|
1487
|
+
// (or, with only `onRowClick`, a button), stretched over the card, and the own-click
|
|
1488
|
+
// cells — a tel: link, the actions row — sit above it, so "tap to open" stays and no
|
|
1489
|
+
// control nests in another. A `mobileCard` stays the fallback above (its controls
|
|
1490
|
+
// cannot be lifted), and so does a card whose title keeps its own clicks.
|
|
1491
|
+
const mobileCardStretched =
|
|
1492
|
+
!mobileCard && !mobileCardLinkable && !!mobilePrimaryCol && !keepsOwnClicks(mobilePrimaryCol);
|
|
1486
1493
|
// The card list's reading direction, for the one thing on it CSS cannot flip: which
|
|
1487
1494
|
// physical drag a logical `start`/`end` swipe action means. Read off the DOM through
|
|
1488
1495
|
// a ref callback (it runs at commit, like a layout effect) because `dir` is an
|
|
@@ -1646,10 +1653,11 @@ export function DataTable<T>({
|
|
|
1646
1653
|
className={rowClassName?.(row)}
|
|
1647
1654
|
attrs={rowAttributes?.(row)}
|
|
1648
1655
|
interactive={!!onRowClick}
|
|
1649
|
-
href={mobileCardLinkable ? rowHref?.(row) : undefined}
|
|
1656
|
+
href={mobileCardLinkable || mobileCardStretched ? rowHref?.(row) : undefined}
|
|
1650
1657
|
swipeActionsFor={swipeActionsFor}
|
|
1651
1658
|
mobileDir={mobileDir}
|
|
1652
1659
|
mobileCardLinkable={mobileCardLinkable}
|
|
1660
|
+
mobileCardStretched={mobileCardStretched}
|
|
1653
1661
|
mobileCard={mobileCard}
|
|
1654
1662
|
mobilePrimaryCol={mobilePrimaryCol}
|
|
1655
1663
|
mobileStatusColumns={mobileStatusColumns}
|
|
@@ -432,6 +432,8 @@ export function DropdownPanel({
|
|
|
432
432
|
<div
|
|
433
433
|
{...rest}
|
|
434
434
|
ref={setPanel}
|
|
435
|
+
// A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).
|
|
436
|
+
data-kit-layer=""
|
|
435
437
|
dir={dir}
|
|
436
438
|
onKeyDown={handleKeyDown}
|
|
437
439
|
onPointerDownCapture={handlePointerDownCapture}
|
|
@@ -208,6 +208,9 @@ export function FullBleedDialog({
|
|
|
208
208
|
// `tabIndex` below by passing one of them too — this is the element the focus
|
|
209
209
|
// trap holds, and each of those failures is invisible on screen.
|
|
210
210
|
{...rest}
|
|
211
|
+
// A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6):
|
|
212
|
+
// a backdrop, so the tour measures the panel inside it to tell a full-screen sheet.
|
|
213
|
+
data-kit-layer="backdrop"
|
|
211
214
|
className={cn(
|
|
212
215
|
"fixed inset-0 z-50 flex items-stretch justify-center bg-black/40 outline-none",
|
|
213
216
|
closing ? "animate-overlay-out" : "animate-overlay",
|
|
@@ -8,6 +8,7 @@ import { cn } from "../lib/cn";
|
|
|
8
8
|
import { useDropdown } from "./dropdown";
|
|
9
9
|
import { takeEscape, useDismissLayer } from "../hooks/use-dismiss";
|
|
10
10
|
import { isImeEnter } from "../lib/ime";
|
|
11
|
+
import { isQuietFocus } from "../lib/quiet-focus";
|
|
11
12
|
import { usePhoneLayout } from "../hooks/use-breakpoint";
|
|
12
13
|
import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet";
|
|
13
14
|
import {
|
|
@@ -255,6 +256,8 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
255
256
|
// Escape layer is this field's own.
|
|
256
257
|
const { open, setOpen, wrapperRef, panelRef } = useDropdown({ backCloses: !isPhone, escapeLayer: false });
|
|
257
258
|
const primaryOnly = usePrimaryPressOnly();
|
|
259
|
+
// The field took the tour's quiet focus (§11.8) and has not opened since.
|
|
260
|
+
const quietRef = useRef(false);
|
|
258
261
|
const [active, setActive] = useState(-1);
|
|
259
262
|
// null = not editing → the input shows the selected option's label.
|
|
260
263
|
const [text, setText] = useState<string | null>(null);
|
|
@@ -508,13 +511,27 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
508
511
|
// say why, and opens nothing.
|
|
509
512
|
if (primaryOnly.fromAuxButton() || locked) return;
|
|
510
513
|
setText(shown);
|
|
514
|
+
// The tour's focus on arrival (§11.8) opens nothing: the list would cover
|
|
515
|
+
// its card (0.50.1). A click, ArrowDown or typing opens it.
|
|
516
|
+
if (isQuietFocus(e.currentTarget)) {
|
|
517
|
+
quietRef.current = true;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
511
520
|
e.currentTarget.select();
|
|
512
521
|
setOpen(true);
|
|
513
522
|
// The sheet has its own input; the field behind it must not also pull up
|
|
514
523
|
// the keyboard and scroll the page under the dialog.
|
|
515
524
|
if (isPhone) sheetInputRef.current?.focus();
|
|
516
525
|
}}
|
|
526
|
+
// After a quiet focus, the person's click on the already-focused field
|
|
527
|
+
// fires no `focus`: the click is their "open" instead.
|
|
528
|
+
onClick={() => {
|
|
529
|
+
if (!quietRef.current || locked) return;
|
|
530
|
+
quietRef.current = false;
|
|
531
|
+
setOpen(true);
|
|
532
|
+
}}
|
|
517
533
|
onBlur={() => {
|
|
534
|
+
quietRef.current = false;
|
|
518
535
|
// On a phone the blur is the SHEET taking focus, not the user leaving the
|
|
519
536
|
// field — reconciling there would close the sheet the instant it opened.
|
|
520
537
|
if (!isPhone) reconcile();
|
package/src/components/modal.tsx
CHANGED
|
@@ -394,6 +394,9 @@ function ModalPanel({
|
|
|
394
394
|
// that page from a screen reader for 220ms. Not `inert` — that would let a tap
|
|
395
395
|
// fall through the fading backdrop to the row underneath.
|
|
396
396
|
aria-hidden={exiting || undefined}
|
|
397
|
+
// A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6):
|
|
398
|
+
// a backdrop, so the tour measures the panel inside it to tell a full-screen sheet.
|
|
399
|
+
data-kit-layer="backdrop"
|
|
397
400
|
className={cn(
|
|
398
401
|
// Live #320. The panel below rises only where it is bottom-anchored: from md up
|
|
399
402
|
// it is centred, and a centred box sliding up from off-screen reads as a
|
|
@@ -16,10 +16,11 @@ import { useEndWidth, useNumericField } from "./numeric-field";
|
|
|
16
16
|
import type { CommitScope } from "./write-lock";
|
|
17
17
|
import { cn } from "../lib/cn";
|
|
18
18
|
import { commitExpression, formatResult } from "../lib/calc";
|
|
19
|
+
import { dataAttributes, type DataAttributes } from "../lib/data-attributes";
|
|
19
20
|
|
|
20
21
|
/** {@link NumberInput}'s props — exported since 0.34 (refactor audit 2026-10, API-9): a
|
|
21
22
|
* wrapper typed itself off `ComponentProps<typeof NumberInput>` until then. */
|
|
22
|
-
export interface NumberInputProps extends FieldHelpProps {
|
|
23
|
+
export interface NumberInputProps extends FieldHelpProps, DataAttributes {
|
|
23
24
|
/** The text, DOT-decimal ("1234.5", or a calculation being typed: "12+5"), as
|
|
24
25
|
* `onChange` reports it — so a caller's `Number(value)` reads it. The field SHOWS it
|
|
25
26
|
* in the locale's mark (see `locale`). A comma here is read as the decimal mark too,
|
|
@@ -549,12 +550,16 @@ export function NumberInput(props: NumberInputProps) {
|
|
|
549
550
|
) : (
|
|
550
551
|
field
|
|
551
552
|
);
|
|
552
|
-
|
|
553
|
+
const data = dataAttributes(props);
|
|
554
|
+
if (numeric.hint.captionText === undefined && numeric.errorEl === null && !reserve) {
|
|
555
|
+
// Bare, unless the caller tags it: then the same plain root as below carries the tags.
|
|
556
|
+
return Object.keys(data).length > 0 ? <div {...data}>{box}</div> : box;
|
|
557
|
+
}
|
|
553
558
|
// Outside the field's own `relative` box, as Select's caption is: the calculator
|
|
554
559
|
// and the unit are `inset-y-0` in it and would stretch down over a second line.
|
|
555
560
|
// `className` stays on the field, so adding a caption cannot change what it styles.
|
|
556
561
|
return (
|
|
557
|
-
<div>
|
|
562
|
+
<div {...data}>
|
|
558
563
|
{box}
|
|
559
564
|
<FieldCaption parts={numeric.hint} />
|
|
560
565
|
{numeric.errorEl}
|