atlas-react-kit 1.0.0 → 1.2.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/COMPONENTS.md +43 -39
- package/DESIGN_RULES.md +23 -32
- package/README.md +5 -47
- package/dist/accordion.js +2 -2
- package/dist/badge.js +5 -2
- package/dist/breadcrumb.js +2 -2
- package/dist/button.js +7 -4
- package/dist/combobox.js +4 -5
- package/dist/date-picker.d.ts +13 -2
- package/dist/date-picker.js +35 -22
- package/dist/date-time-picker.d.ts +1 -1
- package/dist/date-time-picker.js +24 -38
- package/dist/dropdown-menu.js +3 -3
- package/dist/form/date-picker-field.d.ts +6 -1
- package/dist/form/date-picker-field.js +3 -2
- package/dist/form/date-time-picker-field.d.ts +1 -1
- package/dist/form/date-time-picker-field.js +3 -2
- package/dist/form/field-error.d.ts +4 -3
- package/dist/form/field-shell.d.ts +6 -5
- package/dist/form/field-shell.js +1 -1
- package/dist/form/focus-first-invalid.d.ts +28 -0
- package/dist/form/focus-first-invalid.js +29 -0
- package/dist/form/select-field.d.ts +7 -1
- package/dist/form/select-field.js +3 -2
- package/dist/info-tip.js +7 -11
- package/dist/internal/listbox-navigation.d.ts +0 -16
- package/dist/internal/listbox-navigation.js +2 -9
- package/dist/internal/listbox-option.d.ts +0 -15
- package/dist/internal/listbox-option.js +1 -1
- package/dist/internal/overlay-styles.d.ts +0 -2
- package/dist/internal/overlay-styles.js +0 -2
- package/dist/internal/search.d.ts +5 -1
- package/dist/internal/search.js +8 -5
- package/dist/internal/styles.d.ts +27 -0
- package/dist/internal/styles.js +27 -0
- package/dist/internal/tabs-styles.d.ts +8 -7
- package/dist/internal/tabs-styles.js +15 -13
- package/dist/internal/use-drag-scroll.d.ts +17 -0
- package/dist/internal/use-drag-scroll.js +98 -0
- package/dist/internal/use-media-query.d.ts +5 -0
- package/dist/internal/use-media-query.js +5 -0
- package/dist/kanban.d.ts +1 -1
- package/dist/kanban.js +4 -3
- package/dist/nav-link.d.ts +12 -4
- package/dist/nav-link.js +14 -4
- package/dist/next/nav-link.d.ts +6 -3
- package/dist/next/nav-link.js +8 -6
- package/dist/next/tabs-nav-link.d.ts +6 -0
- package/dist/next/tabs-nav-link.js +8 -3
- package/dist/picker-shell.d.ts +24 -21
- package/dist/picker-shell.js +12 -10
- package/dist/search-picker.d.ts +1 -1
- package/dist/search-picker.js +5 -6
- package/dist/select.js +5 -5
- package/dist/tabs.d.ts +21 -10
- package/dist/tabs.js +26 -13
- package/dist/toggle.d.ts +2 -0
- package/dist/toggle.js +8 -3
- package/package.json +7 -3
- package/skills/atlas-react-kit/SKILL.md +46 -40
- package/theme.css +2 -0
package/dist/date-picker.d.ts
CHANGED
|
@@ -26,10 +26,20 @@ export type DatePickerSingleProps = DatePickerSharedProps & {
|
|
|
26
26
|
mode?: "single";
|
|
27
27
|
/** Controlled day, as a `Date` or `yyyy-MM-dd`. Omitted is empty. */
|
|
28
28
|
value?: DateLike;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Called with the picked day. With `clearable`, Clear calls it with
|
|
31
|
+
* `undefined`; without, a second click on the selected day does.
|
|
32
|
+
*/
|
|
30
33
|
onValueChange?: (date: Date | undefined) => void;
|
|
31
34
|
/** @deprecated Use `onValueChange`; `onChange` keeps working until 2.0. */
|
|
32
35
|
onChange?: (date: Date | undefined) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Shows a Clear row under the calendar, which emits `undefined` and
|
|
38
|
+
* closes the popover. A click on the selected day then keeps it instead
|
|
39
|
+
* of clearing it, so Clear is the one way to empty the field. Default
|
|
40
|
+
* `false`.
|
|
41
|
+
*/
|
|
42
|
+
clearable?: boolean;
|
|
33
43
|
};
|
|
34
44
|
export type DatePickerRangeProps = DatePickerSharedProps & {
|
|
35
45
|
/** Pick a from/to range of days. */
|
|
@@ -46,7 +56,8 @@ export type DatePickerButtonProps = DatePickerSingleProps | DatePickerRangeProps
|
|
|
46
56
|
* Calendar popover trigger styled as a form control (same chrome as Input
|
|
47
57
|
* and SelectTrigger). `mode="single"` (default) accepts `Date` or
|
|
48
58
|
* `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
|
|
49
|
-
* `{ from, to }` and emits `DateRange | undefined`. Range mode
|
|
59
|
+
* `{ from, to }` and emits `DateRange | undefined`. Range mode always has a
|
|
60
|
+
* Clear row; single mode shows one with `clearable`. Range mode shows one
|
|
50
61
|
* month below `md` and two at `md+` unless `numberOfMonths` is set. The
|
|
51
62
|
* popover clamps to the viewport (`side` is overridable). `className` styles
|
|
52
63
|
* the trigger, `contentClassName` the popover, and `calendarProps` reaches
|
package/dist/date-picker.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { format, isSameDay } from "date-fns";
|
|
5
5
|
import { toDate, } from "./internal/date-like.js";
|
|
6
6
|
import { MD_MEDIA_QUERY, useMediaQuery } from "./internal/use-media-query.js";
|
|
7
|
-
import {
|
|
8
|
-
import { PickerClearFooter, PickerShell, limitCalendarProps, } from "./picker-shell.js";
|
|
7
|
+
import { PickerShell, } from "./picker-shell.js";
|
|
9
8
|
export { toDate };
|
|
10
9
|
/** Format a Date as a timezone-safe calendar date (`yyyy-MM-dd`). */
|
|
11
10
|
export function formatLocalDate(date) {
|
|
@@ -22,16 +21,27 @@ function formatRangeTriggerLabel(from, to, placeholder) {
|
|
|
22
21
|
return format(from, "PPP");
|
|
23
22
|
return `${format(from, "PPP")} – ${format(to, "PPP")}`;
|
|
24
23
|
}
|
|
25
|
-
function DatePickerSingleButton({ value, onValueChange, onChange, placeholder = "Pick a date", numberOfMonths, defaultMonth,
|
|
24
|
+
function DatePickerSingleButton({ value, onValueChange, onChange, placeholder = "Pick a date", numberOfMonths, defaultMonth, clearable = false, ...shell }) {
|
|
26
25
|
const [open, setOpen] = React.useState(false);
|
|
27
26
|
const emit = onValueChange ?? onChange;
|
|
28
27
|
const date = toDate(value);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
const onSelect = (next) => {
|
|
29
|
+
emit?.(next);
|
|
30
|
+
setOpen(false);
|
|
31
|
+
};
|
|
32
|
+
return (_jsx(PickerShell, { ...shell, empty: !date, label: date ? format(date, "PPP") : placeholder, dialogLabel: "Choose date", open: open, onOpenChange: setOpen, defaultMonth: defaultMonth ?? date, calendar: clearable
|
|
33
|
+
? // Day Picker's `required` keeps the day on a second click, so
|
|
34
|
+
// Clear is the one way out; its type needs the literal `true`.
|
|
35
|
+
{
|
|
36
|
+
mode: "single",
|
|
37
|
+
required: true,
|
|
38
|
+
selected: date,
|
|
39
|
+
numberOfMonths,
|
|
40
|
+
onSelect,
|
|
41
|
+
}
|
|
42
|
+
: { mode: "single", selected: date, numberOfMonths, onSelect }, onClear: clearable ? () => emit?.(undefined) : undefined, clearDisabled: !date }));
|
|
33
43
|
}
|
|
34
|
-
function DatePickerRangeButton({ value, onValueChange, onChange, placeholder = "Pick a date range", numberOfMonths, defaultMonth,
|
|
44
|
+
function DatePickerRangeButton({ value, onValueChange, onChange, placeholder = "Pick a date range", numberOfMonths, defaultMonth, ...shell }) {
|
|
35
45
|
const [open, setOpen] = React.useState(false);
|
|
36
46
|
const emit = onValueChange ?? onChange;
|
|
37
47
|
const mdUp = useMediaQuery(MD_MEDIA_QUERY);
|
|
@@ -39,25 +49,28 @@ function DatePickerRangeButton({ value, onValueChange, onChange, placeholder = "
|
|
|
39
49
|
const from = toDate(value?.from);
|
|
40
50
|
const to = toDate(value?.to);
|
|
41
51
|
const selected = from || to ? { from, to } : undefined;
|
|
42
|
-
return (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
return (_jsx(PickerShell, { ...shell, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), dialogLabel: "Choose dates", open: open, onOpenChange: setOpen, defaultMonth: defaultMonth ?? from ?? to, calendar: {
|
|
53
|
+
mode: "range",
|
|
54
|
+
// A click on a complete range (or on none) starts a new one, so a
|
|
55
|
+
// fresh range never needs Clear first. The start emits `to: undefined`.
|
|
56
|
+
resetOnSelect: true,
|
|
57
|
+
selected,
|
|
58
|
+
numberOfMonths: months,
|
|
59
|
+
onSelect: (range) => {
|
|
60
|
+
emit?.(range);
|
|
61
|
+
// Close once the ends differ; a start or a one-day range stays open.
|
|
62
|
+
if (range?.from && range.to && !isSameDay(range.from, range.to)) {
|
|
53
63
|
setOpen(false);
|
|
54
|
-
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}, onClear: () => emit?.(undefined), clearDisabled: !from && !to }));
|
|
55
67
|
}
|
|
56
68
|
/**
|
|
57
69
|
* Calendar popover trigger styled as a form control (same chrome as Input
|
|
58
70
|
* and SelectTrigger). `mode="single"` (default) accepts `Date` or
|
|
59
71
|
* `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
|
|
60
|
-
* `{ from, to }` and emits `DateRange | undefined`. Range mode
|
|
72
|
+
* `{ from, to }` and emits `DateRange | undefined`. Range mode always has a
|
|
73
|
+
* Clear row; single mode shows one with `clearable`. Range mode shows one
|
|
61
74
|
* month below `md` and two at `md+` unless `numberOfMonths` is set. The
|
|
62
75
|
* popover clamps to the viewport (`side` is overridable). `className` styles
|
|
63
76
|
* the trigger, `contentClassName` the popover, and `calendarProps` reaches
|
|
@@ -28,4 +28,4 @@ export declare function resolveTimeStep(step: number | undefined): number;
|
|
|
28
28
|
* stays on-screen at a 390px width. `className` styles the trigger,
|
|
29
29
|
* `contentClassName` the popover, and `calendarProps` reaches Day Picker.
|
|
30
30
|
*/
|
|
31
|
-
export declare function DateTimePickerButton({ value, onValueChange, onChange, timeStep, placeholder, defaultMonth,
|
|
31
|
+
export declare function DateTimePickerButton({ value, onValueChange, onChange, timeStep, placeholder, defaultMonth, ...shell }: DateTimePickerButtonProps): React.JSX.Element;
|
package/dist/date-time-picker.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { format } from "date-fns";
|
|
5
|
-
import {
|
|
6
|
-
import { PickerClearFooter, PickerShell, limitCalendarProps, } from "./picker-shell.js";
|
|
5
|
+
import { PickerShell } from "./picker-shell.js";
|
|
7
6
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select.js";
|
|
8
7
|
/** Default clock increment, in minutes. Must divide 60. */
|
|
9
8
|
export const DEFAULT_TIME_STEP = 15;
|
|
@@ -48,18 +47,6 @@ function minuteOptions(step, current) {
|
|
|
48
47
|
return minutes;
|
|
49
48
|
}
|
|
50
49
|
const HOUR_OPTIONS = Array.from({ length: 24 }, (_, hour) => padTimePart(hour));
|
|
51
|
-
/**
|
|
52
|
-
* Keep the date-time popover open when a portaled Select option is clicked.
|
|
53
|
-
* Select content renders outside the popover, so Radix would otherwise treat
|
|
54
|
-
* that pointer down as an outside dismiss.
|
|
55
|
-
*/
|
|
56
|
-
function preventDismissForPortaledSelect(event) {
|
|
57
|
-
const target = event.target;
|
|
58
|
-
if (target instanceof Element &&
|
|
59
|
-
target.closest("[data-slot=select-content]")) {
|
|
60
|
-
event.preventDefault();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
50
|
/**
|
|
64
51
|
* Calendar plus hour/minute popover trigger styled as a form control (same
|
|
65
52
|
* chrome as Input, SelectTrigger, and DatePickerButton). `value` is a `Date`
|
|
@@ -68,33 +55,32 @@ function preventDismissForPortaledSelect(event) {
|
|
|
68
55
|
* stays on-screen at a 390px width. `className` styles the trigger,
|
|
69
56
|
* `contentClassName` the popover, and `calendarProps` reaches Day Picker.
|
|
70
57
|
*/
|
|
71
|
-
export function DateTimePickerButton({ value, onValueChange, onChange, timeStep, placeholder = "Pick a date and time", defaultMonth,
|
|
58
|
+
export function DateTimePickerButton({ value, onValueChange, onChange, timeStep, placeholder = "Pick a date and time", defaultMonth, ...shell }) {
|
|
72
59
|
const [open, setOpen] = React.useState(false);
|
|
73
60
|
const emit = onValueChange ?? onChange;
|
|
74
61
|
const date = value ?? null;
|
|
75
62
|
const step = resolveTimeStep(timeStep);
|
|
76
63
|
const empty = date == null;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} })] }));
|
|
64
|
+
return (_jsx(PickerShell, { ...shell, empty: empty, label: date ? format(date, "PPP p") : placeholder, dialogLabel: "Choose date and time", open: open, onOpenChange: setOpen, defaultMonth: defaultMonth ?? date ?? undefined, calendar: {
|
|
65
|
+
mode: "single",
|
|
66
|
+
selected: date ?? undefined,
|
|
67
|
+
onSelect: (next) => {
|
|
68
|
+
// Re-clicking a selected day would clear it; require Clear instead
|
|
69
|
+
// so a time already set is not dropped by an accidental second tap.
|
|
70
|
+
if (!next)
|
|
71
|
+
return;
|
|
72
|
+
emit?.(combineDateAndTime(next, date?.getHours() ?? 0, date?.getMinutes() ?? 0));
|
|
73
|
+
},
|
|
74
|
+
}, onClear: () => emit?.(null), clearDisabled: empty, children: _jsxs("div", { role: "group", "aria-label": "Time", "data-slot": "date-time-picker-time", className: "flex min-w-0 flex-col gap-2 border-t p-3", children: [_jsx("p", { className: "text-xs font-semibold tracking-wide uppercase", children: "Time" }), _jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsxs(Select, { value: date ? padTimePart(date.getHours()) : undefined, onValueChange: (nextHour) => {
|
|
75
|
+
if (!date)
|
|
76
|
+
return;
|
|
77
|
+
emit?.(combineDateAndTime(date, Number(nextHour), date.getMinutes()));
|
|
78
|
+
}, disabled: empty, children: [_jsx(SelectTrigger, { "aria-label": "Hour", className: "min-w-0 flex-1", children: _jsx(SelectValue, { placeholder: "\u2013\u2013" }) }), _jsx(SelectContent, { children: HOUR_OPTIONS.map((hour) => (_jsx(SelectItem, { value: hour, children: hour }, hour))) })] }), _jsx("span", { className: "text-muted-foreground", "aria-hidden": "true", children: ":" }), _jsxs(Select, { value: date ? padTimePart(date.getMinutes()) : undefined, onValueChange: (nextMinute) => {
|
|
79
|
+
if (!date)
|
|
80
|
+
return;
|
|
81
|
+
emit?.(combineDateAndTime(date, date.getHours(), Number(nextMinute)));
|
|
82
|
+
}, disabled: empty, children: [_jsx(SelectTrigger, { "aria-label": "Minute", className: "min-w-0 flex-1", children: _jsx(SelectValue, { placeholder: "\u2013\u2013" }) }), _jsx(SelectContent, { children: minuteOptions(step, date?.getMinutes()).map((minute) => {
|
|
83
|
+
const value = padTimePart(minute);
|
|
84
|
+
return (_jsx(SelectItem, { value: value, children: value }, value));
|
|
85
|
+
}) })] })] })] }) }));
|
|
100
86
|
}
|
package/dist/dropdown-menu.js
CHANGED
|
@@ -3,11 +3,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
|
5
5
|
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
|
6
|
-
import { menuLabel,
|
|
7
|
-
import { dataDisabledState, optionState } from "./internal/styles.js";
|
|
6
|
+
import { menuLabel, optionRow, overlayFadeZoom, overlaySideSlide, overlaySurface, } from "./internal/overlay-styles.js";
|
|
7
|
+
import { dataDisabledState, inlineIcon, optionState, } from "./internal/styles.js";
|
|
8
8
|
import { cn } from "./utils.js";
|
|
9
9
|
/** Menu row: the shared option row with menu padding and the inset slot. */
|
|
10
|
-
const menuItemClassName = cn(optionRow, optionState.focus, dataDisabledState,
|
|
10
|
+
const menuItemClassName = cn(optionRow, optionState.focus, dataDisabledState, inlineIcon.default, "pr-3 data-inset:pl-9");
|
|
11
11
|
const menuContentClassName = cn(overlaySurface, overlaySideSlide, overlayFadeZoom, "z-50 overflow-x-hidden overflow-y-auto p-1");
|
|
12
12
|
/** Action menu for commands (row actions, overflow menus). Use `Select` to pick a value. */
|
|
13
13
|
function DropdownMenu({ ...props }) {
|
|
@@ -7,7 +7,12 @@ type Props = PickerFieldBaseProps & {
|
|
|
7
7
|
onValueChange: (date: Date | undefined) => void;
|
|
8
8
|
/** Trigger text while no day is picked. Default "Pick a date". */
|
|
9
9
|
placeholder?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Shows a Clear row in the popover, which calls `onValueChange` with
|
|
12
|
+
* `undefined`. Default `false`.
|
|
13
|
+
*/
|
|
14
|
+
clearable?: boolean;
|
|
10
15
|
};
|
|
11
16
|
/** Date picker with built-in field error text and invalid styling. */
|
|
12
|
-
export declare function FormDatePickerField({ onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }: Props): import("react").JSX.Element;
|
|
17
|
+
export declare function FormDatePickerField({ id: idProp, onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }: Props): import("react").JSX.Element;
|
|
13
18
|
export {};
|
|
@@ -4,8 +4,9 @@ import { useId } from "react";
|
|
|
4
4
|
import { DatePickerButton } from "../date-picker.js";
|
|
5
5
|
import { FormFieldShell, metaErrorText, pickerFieldAria, } from "./field-shell.js";
|
|
6
6
|
/** Date picker with built-in field error text and invalid styling. */
|
|
7
|
-
export function FormDatePickerField({ onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }) {
|
|
7
|
+
export function FormDatePickerField({ id: idProp, onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }) {
|
|
8
8
|
const errorText = metaErrorText(meta, { submitted, showError });
|
|
9
|
-
const
|
|
9
|
+
const generatedId = useId();
|
|
10
|
+
const id = idProp ?? generatedId;
|
|
10
11
|
return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsx(DatePickerButton, { ...picker, id: id, onValueChange: onValueChange, ...pickerFieldAria(id, errorText, required) }) }));
|
|
11
12
|
}
|
|
@@ -10,5 +10,5 @@ type Props = PickerFieldBaseProps & {
|
|
|
10
10
|
timeStep?: number;
|
|
11
11
|
};
|
|
12
12
|
/** Date-time picker with built-in field error text and invalid styling. */
|
|
13
|
-
export declare function FormDateTimePickerField({ onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }: Props): import("react").JSX.Element;
|
|
13
|
+
export declare function FormDateTimePickerField({ id: idProp, onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }: Props): import("react").JSX.Element;
|
|
14
14
|
export {};
|
|
@@ -4,8 +4,9 @@ import { useId } from "react";
|
|
|
4
4
|
import { DateTimePickerButton } from "../date-time-picker.js";
|
|
5
5
|
import { FormFieldShell, metaErrorText, pickerFieldAria, } from "./field-shell.js";
|
|
6
6
|
/** Date-time picker with built-in field error text and invalid styling. */
|
|
7
|
-
export function FormDateTimePickerField({ onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }) {
|
|
7
|
+
export function FormDateTimePickerField({ id: idProp, onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }) {
|
|
8
8
|
const errorText = metaErrorText(meta, { submitted, showError });
|
|
9
|
-
const
|
|
9
|
+
const generatedId = useId();
|
|
10
|
+
const id = idProp ?? generatedId;
|
|
10
11
|
return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsx(DateTimePickerButton, { ...picker, id: id, onValueChange: onValueChange, ...pickerFieldAria(id, errorText, required) }) }));
|
|
11
12
|
}
|
|
@@ -16,9 +16,10 @@ export type FieldMetaState = {
|
|
|
16
16
|
/** When a field shows its error: the `form/*` fields' shared timing props. */
|
|
17
17
|
export type ErrorTimingProps = {
|
|
18
18
|
/**
|
|
19
|
-
* Set once the form has been submitted
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Set once the form has been submitted, so an untouched field shows its
|
|
20
|
+
* error too. For form libraries that do not mark fields touched on
|
|
21
|
+
* submit; TanStack Form does, so its users pass `meta` only. Only matters
|
|
22
|
+
* when `meta.isTouched` is passed.
|
|
22
23
|
*/
|
|
23
24
|
submitted?: boolean;
|
|
24
25
|
/** When false, never shows error text or invalid styling. Default true. */
|
|
@@ -20,11 +20,6 @@ export declare function labelIdFor(id: string): string;
|
|
|
20
20
|
export declare function descriptionIdFor(id: string): string;
|
|
21
21
|
/** Id of the error text `FormFieldShell` renders for control `id`. */
|
|
22
22
|
export declare function errorIdFor(id: string): string;
|
|
23
|
-
/**
|
|
24
|
-
* Id of the hidden "Required" text `FormFieldShell` renders for a required
|
|
25
|
-
* control `id`, for controls that cannot carry `required` themselves.
|
|
26
|
-
*/
|
|
27
|
-
export declare function requiredIdFor(id: string): string;
|
|
28
23
|
/**
|
|
29
24
|
* `aria-describedby` for the control: the given hint ids, plus the error
|
|
30
25
|
* text while there is one, or `undefined` when there is nothing to read.
|
|
@@ -35,6 +30,12 @@ export declare function describedBy(id: string, errorText: string | undefined, .
|
|
|
35
30
|
* `onValueChange`, and `placeholder`.
|
|
36
31
|
*/
|
|
37
32
|
export type PickerFieldBaseProps = FormFieldBaseProps & FormFieldMetaProps & {
|
|
33
|
+
/**
|
|
34
|
+
* Id of the trigger the label points at. Defaults to a generated id;
|
|
35
|
+
* pass the field name (`id={field.name}`) so a submit handler such as
|
|
36
|
+
* `focusFirstInvalid` can find the control.
|
|
37
|
+
*/
|
|
38
|
+
id?: string;
|
|
38
39
|
/** Disables the trigger so no date can be picked. */
|
|
39
40
|
disabled?: boolean;
|
|
40
41
|
/** Marks the label and adds "Required" to the trigger's description. */
|
package/dist/form/field-shell.js
CHANGED
|
@@ -40,7 +40,7 @@ export function errorIdFor(id) {
|
|
|
40
40
|
* Id of the hidden "Required" text `FormFieldShell` renders for a required
|
|
41
41
|
* control `id`, for controls that cannot carry `required` themselves.
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
function requiredIdFor(id) {
|
|
44
44
|
return `${id}-required`;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The part of a form library's invalid-submit event this helper reads: the
|
|
3
|
+
* form state's per-field meta, keyed by field name (TanStack Form's
|
|
4
|
+
* `onSubmitInvalid` passes `{ formApi }` with this shape).
|
|
5
|
+
*/
|
|
6
|
+
export type InvalidSubmitEvent = {
|
|
7
|
+
formApi: {
|
|
8
|
+
state: {
|
|
9
|
+
fieldMeta: Partial<Record<string, {
|
|
10
|
+
errors: readonly unknown[];
|
|
11
|
+
}>>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Invalid-submit handler that focuses the first field, in page order, whose
|
|
17
|
+
* meta has errors. It reads the form state, not the rendered `aria-invalid`,
|
|
18
|
+
* so it works before React has re-rendered. Fields are matched by control
|
|
19
|
+
* id, so give each `form/*` field `id={field.name}` (`FormTextField`
|
|
20
|
+
* defaults to it). The form library needs no focus API of its own.
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* const form = useForm({ onSubmitInvalid: focusFirstInvalid("order-form") })
|
|
24
|
+
* <form id="order-form" onSubmit={...}>
|
|
25
|
+
* <FormSelectField id={field.name} ... />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function focusFirstInvalid(formId: string): ({ formApi }: InvalidSubmitEvent) => void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invalid-submit handler that focuses the first field, in page order, whose
|
|
3
|
+
* meta has errors. It reads the form state, not the rendered `aria-invalid`,
|
|
4
|
+
* so it works before React has re-rendered. Fields are matched by control
|
|
5
|
+
* id, so give each `form/*` field `id={field.name}` (`FormTextField`
|
|
6
|
+
* defaults to it). The form library needs no focus API of its own.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const form = useForm({ onSubmitInvalid: focusFirstInvalid("order-form") })
|
|
10
|
+
* <form id="order-form" onSubmit={...}>
|
|
11
|
+
* <FormSelectField id={field.name} ... />
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function focusFirstInvalid(formId) {
|
|
15
|
+
return ({ formApi }) => {
|
|
16
|
+
const form = document.getElementById(formId);
|
|
17
|
+
if (!(form instanceof HTMLFormElement))
|
|
18
|
+
return;
|
|
19
|
+
const invalid = new Set(Object.entries(formApi.state.fieldMeta)
|
|
20
|
+
.filter(([, meta]) => meta?.errors.length)
|
|
21
|
+
.map(([name]) => name));
|
|
22
|
+
for (const element of form.elements) {
|
|
23
|
+
if (element instanceof HTMLElement && invalid.has(element.id)) {
|
|
24
|
+
element.focus();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -7,6 +7,12 @@ type Props = FormFieldBaseProps & FormFieldMetaProps & {
|
|
|
7
7
|
onValueChange: (value: string) => void;
|
|
8
8
|
/** Trigger text while no item is selected. */
|
|
9
9
|
placeholder?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Id of the trigger the label points at. Defaults to a generated id;
|
|
12
|
+
* pass the field name (`id={field.name}`) so a submit handler such as
|
|
13
|
+
* `focusFirstInvalid` can find the control.
|
|
14
|
+
*/
|
|
15
|
+
id?: string;
|
|
10
16
|
/** Requires a value (native form validation) and marks the label. */
|
|
11
17
|
required?: boolean;
|
|
12
18
|
/** Forwarded to the select trigger. */
|
|
@@ -17,5 +23,5 @@ type Props = FormFieldBaseProps & FormFieldMetaProps & {
|
|
|
17
23
|
children: ReactNode;
|
|
18
24
|
};
|
|
19
25
|
/** Select control with built-in field error text and invalid styling. */
|
|
20
|
-
export declare function FormSelectField({ value, meta, submitted, showError, label, onValueChange, placeholder, required, className, fieldClassName, contentClassName, children, }: Props): import("react").JSX.Element;
|
|
26
|
+
export declare function FormSelectField({ id: idProp, value, meta, submitted, showError, label, onValueChange, placeholder, required, className, fieldClassName, contentClassName, children, }: Props): import("react").JSX.Element;
|
|
21
27
|
export {};
|
|
@@ -4,8 +4,9 @@ import { useId } from "react";
|
|
|
4
4
|
import { Select, SelectContent, SelectTrigger, SelectValue } from "../select.js";
|
|
5
5
|
import { describedBy, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
|
|
6
6
|
/** Select control with built-in field error text and invalid styling. */
|
|
7
|
-
export function FormSelectField({ value, meta, submitted, showError, label, onValueChange, placeholder, required, className, fieldClassName, contentClassName, children, }) {
|
|
7
|
+
export function FormSelectField({ id: idProp, value, meta, submitted, showError, label, onValueChange, placeholder, required, className, fieldClassName, contentClassName, children, }) {
|
|
8
8
|
const errorText = metaErrorText(meta, { submitted, showError });
|
|
9
|
-
const
|
|
9
|
+
const generatedId = useId();
|
|
10
|
+
const id = idProp ?? generatedId;
|
|
10
11
|
return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsxs(Select, { value: value, onValueChange: onValueChange, required: required, children: [_jsx(SelectTrigger, { id: id, className: className, "aria-required": required || undefined, "aria-invalid": invalidWhen(errorText), "aria-describedby": describedBy(id, errorText), children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { className: contentClassName, children: children })] }) }));
|
|
11
12
|
}
|
package/dist/info-tip.js
CHANGED
|
@@ -3,15 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { CircleHelpIcon } from "lucide-react";
|
|
5
5
|
import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, } from "./popover.js";
|
|
6
|
-
import { focusRing, hitArea44 } from "./internal/styles.js";
|
|
6
|
+
import { focusRing, hitArea44, quietTextHover } from "./internal/styles.js";
|
|
7
|
+
import { HOVER_MEDIA_QUERY, useMediaQuery } from "./internal/use-media-query.js";
|
|
7
8
|
import { cn } from "./utils.js";
|
|
8
|
-
/**
|
|
9
|
-
* True when a mouse can hover. Touch and coarse pointers must use click;
|
|
10
|
-
* a hover-open popover would trap them after the first tap.
|
|
11
|
-
*/
|
|
12
|
-
function canHoverOpen() {
|
|
13
|
-
return window.matchMedia("(hover: hover) and (pointer: fine)").matches;
|
|
14
|
-
}
|
|
15
9
|
/**
|
|
16
10
|
* Longer help than a Tooltip: a Popover with a 20px glyph and a 44px hit
|
|
17
11
|
* area. Hover opens only on a fine pointer; click, tap, and keyboard always
|
|
@@ -25,6 +19,8 @@ function InfoTip({ label, title, description, children, layout = "box", side = "
|
|
|
25
19
|
const openedBy = React.useRef(null);
|
|
26
20
|
const titleId = React.useId();
|
|
27
21
|
const inline = layout === "inline";
|
|
22
|
+
// Hover opens only for a pointer that can hover; touch must click.
|
|
23
|
+
const canHoverOpen = useMediaQuery(HOVER_MEDIA_QUERY);
|
|
28
24
|
const cancelClose = () => {
|
|
29
25
|
if (closeTimer.current !== null) {
|
|
30
26
|
window.clearTimeout(closeTimer.current);
|
|
@@ -32,7 +28,7 @@ function InfoTip({ label, title, description, children, layout = "box", side = "
|
|
|
32
28
|
}
|
|
33
29
|
};
|
|
34
30
|
const openFromHover = () => {
|
|
35
|
-
if (!canHoverOpen
|
|
31
|
+
if (!canHoverOpen)
|
|
36
32
|
return;
|
|
37
33
|
cancelClose();
|
|
38
34
|
if (open)
|
|
@@ -41,7 +37,7 @@ function InfoTip({ label, title, description, children, layout = "box", side = "
|
|
|
41
37
|
setOpen(true);
|
|
42
38
|
};
|
|
43
39
|
const closeFromHover = () => {
|
|
44
|
-
if (!canHoverOpen
|
|
40
|
+
if (!canHoverOpen || openedBy.current !== "hover")
|
|
45
41
|
return;
|
|
46
42
|
cancelClose();
|
|
47
43
|
// Leave a beat so the pointer can travel from the glyph into the panel.
|
|
@@ -55,7 +51,7 @@ function InfoTip({ label, title, description, children, layout = "box", side = "
|
|
|
55
51
|
if (next)
|
|
56
52
|
openedBy.current = "click";
|
|
57
53
|
setOpen(next);
|
|
58
|
-
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { ...props, type: "button", "aria-label": label, "data-slot": "info-tip-trigger", "data-layout": layout, className: cn(focusRing, "relative inline-flex shrink-0 items-center justify-center rounded-none text-muted-foreground
|
|
54
|
+
}, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { ...props, type: "button", "aria-label": label, "data-slot": "info-tip-trigger", "data-layout": layout, className: cn(focusRing, "relative inline-flex shrink-0 items-center justify-center rounded-none text-muted-foreground", quietTextHover, inline ? cn("size-5 align-middle", hitArea44) : "size-11", className), onMouseEnter: openFromHover, onMouseLeave: closeFromHover, onClick: (event) => {
|
|
59
55
|
// Hover already opened the panel; this click pins it instead of
|
|
60
56
|
// toggling it closed (Playwright and real mice both hover before
|
|
61
57
|
// they click). Preventing default skips Radix's toggle.
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Return the next highlighted index for a listbox navigation key. */
|
|
2
|
-
export declare function moveListboxHighlight(key: string, index: number, length: number): number | undefined;
|
|
3
1
|
/**
|
|
4
2
|
* Option id for `aria-activedescendant`. Index-based because option values
|
|
5
3
|
* may contain spaces, which are not valid in an id reference.
|
|
@@ -12,15 +10,6 @@ export declare function listboxOptionId(listId: string, index: number): string;
|
|
|
12
10
|
* so the ref is stable without `useCallback`.
|
|
13
11
|
*/
|
|
14
12
|
export declare function scrollOptionIntoView(node: HTMLElement | null): void;
|
|
15
|
-
/**
|
|
16
|
-
* True while an IME is composing (Japanese, Chinese, Korean input). Enter
|
|
17
|
-
* then commits the composition and must not select an option. `keyCode`
|
|
18
|
-
* 229 covers Safari, which ends composition before the keydown.
|
|
19
|
-
*/
|
|
20
|
-
export declare function isComposingKey(event: {
|
|
21
|
-
nativeEvent: KeyboardEvent;
|
|
22
|
-
keyCode: number;
|
|
23
|
-
}): boolean;
|
|
24
13
|
/**
|
|
25
14
|
* Shared keydown for a search input that drives a listbox: navigation keys
|
|
26
15
|
* move the highlight, Enter picks the highlighted index. Returns `true` when
|
|
@@ -39,8 +28,3 @@ export declare function handleListboxKey(event: {
|
|
|
39
28
|
setHighlight: (index: number) => void;
|
|
40
29
|
onEnter: (index: number) => void;
|
|
41
30
|
}): boolean;
|
|
42
|
-
/**
|
|
43
|
-
* Case-fold and strip combining marks so "nino" matches "Niño". Shared by
|
|
44
|
-
* Combobox and SearchPicker (which re-exports it).
|
|
45
|
-
*/
|
|
46
|
-
export declare function foldSearchText(value: string): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Page keys move ten rows, a little more than one max-h-60 list viewport. */
|
|
2
2
|
const PAGE_SIZE = 10;
|
|
3
3
|
/** Return the next highlighted index for a listbox navigation key. */
|
|
4
|
-
|
|
4
|
+
function moveListboxHighlight(key, index, length) {
|
|
5
5
|
switch (key) {
|
|
6
6
|
case "ArrowDown":
|
|
7
7
|
return length === 0 ? 0 : Math.min(index + 1, length - 1);
|
|
@@ -40,7 +40,7 @@ export function scrollOptionIntoView(node) {
|
|
|
40
40
|
* then commits the composition and must not select an option. `keyCode`
|
|
41
41
|
* 229 covers Safari, which ends composition before the keydown.
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
function isComposingKey(event) {
|
|
44
44
|
return event.nativeEvent.isComposing || event.keyCode === 229;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
@@ -66,10 +66,3 @@ export function handleListboxKey(event, { highlight, length, setHighlight, onEnt
|
|
|
66
66
|
}
|
|
67
67
|
return false;
|
|
68
68
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Case-fold and strip combining marks so "nino" matches "Niño". Shared by
|
|
71
|
-
* Combobox and SearchPicker (which re-exports it).
|
|
72
|
-
*/
|
|
73
|
-
export function foldSearchText(value) {
|
|
74
|
-
return value.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
75
|
-
}
|
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
type ListboxOptionProps = Omit<React.ComponentProps<"button">, "id" | "role" | "type" | "tabIndex" | "aria-selected"> & {
|
|
3
|
-
/** The listbox id; the option id derives from it for aria-activedescendant. */
|
|
4
|
-
listId: string;
|
|
5
|
-
index: number;
|
|
6
|
-
/** Keyboard or pointer highlight (tracked in state, not DOM focus). */
|
|
7
|
-
active: boolean;
|
|
8
|
-
selected: boolean;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* One option of a search-driven listbox (Combobox, SearchPicker): a
|
|
12
|
-
* non-focusable button in a presentational `li`, highlighted by state and
|
|
13
|
-
* scrolled into view while active. Pass `data-slot`, handlers, and the
|
|
14
|
-
* component's right padding and indicator.
|
|
15
|
-
*/
|
|
16
|
-
export declare function ListboxOption({ listId, index, active, selected, className, ...props }: ListboxOptionProps): React.JSX.Element;
|
|
17
2
|
type PickerOptionProps<T> = {
|
|
18
3
|
item: T;
|
|
19
4
|
getLabel: (item: T) => string;
|
|
@@ -11,7 +11,7 @@ import { listboxEmpty, optionState } from "./styles.js";
|
|
|
11
11
|
* scrolled into view while active. Pass `data-slot`, handlers, and the
|
|
12
12
|
* component's right padding and indicator.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
function ListboxOption({ listId, index, active, selected, className, ...props }) {
|
|
15
15
|
return (_jsx("li", { role: "none", children: _jsx("button", { ref: active ? scrollOptionIntoView : undefined, id: listboxOptionId(listId, index), type: "button", role: "option", tabIndex: -1, "aria-selected": selected, className: cn(optionRow, selected && optionState.selected, active && optionState.active, className), ...props }) }));
|
|
16
16
|
}
|
|
17
17
|
/** Only the old and new active rows rerender during keyboard navigation. */
|
|
@@ -22,7 +22,5 @@ export declare const overlayFadeZoom = "duration-100 data-open:animate-in data-o
|
|
|
22
22
|
* indicator, plus the highlight from `optionState`.
|
|
23
23
|
*/
|
|
24
24
|
export declare const optionRow = "relative flex w-full cursor-default items-center gap-2.5 rounded-none py-2 pl-3 text-left text-sm outline-hidden select-none";
|
|
25
|
-
/** Icons in a row: no pointer events, no shrinking, 14px unless sized. */
|
|
26
|
-
export declare const optionIcon = "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5";
|
|
27
25
|
/** Group label in a menu or listbox (DropdownMenuLabel, SelectLabel). */
|
|
28
26
|
export declare const menuLabel = "px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase";
|
|
@@ -22,7 +22,5 @@ export const overlayFadeZoom = `${overlayFade} data-open:zoom-in-95 data-closed:
|
|
|
22
22
|
* indicator, plus the highlight from `optionState`.
|
|
23
23
|
*/
|
|
24
24
|
export const optionRow = "relative flex w-full cursor-default items-center gap-2.5 rounded-none py-2 pl-3 text-left text-sm outline-hidden select-none";
|
|
25
|
-
/** Icons in a row: no pointer events, no shrinking, 14px unless sized. */
|
|
26
|
-
export const optionIcon = "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5";
|
|
27
25
|
/** Group label in a menu or listbox (DropdownMenuLabel, SelectLabel). */
|
|
28
26
|
export const menuLabel = "px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase";
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Case-fold and strip combining marks so "nino" matches "Niño". Shared by
|
|
3
|
+
* Combobox and SearchPicker (which re-exports it).
|
|
4
|
+
*/
|
|
5
|
+
export declare function foldSearchText(value: string): string;
|
|
1
6
|
/** Fold the query once; item labels are folded when their catalog changes. */
|
|
2
7
|
export declare function normalizeSearchQuery(query: string): string;
|
|
3
|
-
export declare function matchesSearchLabel(label: string, needle: string): boolean;
|
|
4
8
|
export declare function createSearchIndex<T>(items: T[], getLabel: (item: T) => string): {
|
|
5
9
|
item: T;
|
|
6
10
|
label: string;
|