@antadesign/anta 0.3.3 → 0.3.4
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/dist/anta_helpers.d.ts +0 -4
- package/dist/anta_helpers.js +0 -7
- package/dist/components/Calendar.d.ts +6 -1
- package/dist/components/Calendar.js +6 -0
- package/dist/components/InputDate.d.ts +7 -5
- package/dist/components/InputDate.js +100 -105
- package/dist/components/Menu.d.ts +9 -0
- package/dist/components/MenuItem.js +1 -7
- package/dist/components/Select.d.ts +72 -8
- package/dist/components/Select.js +57 -6
- package/dist/components/TabPanel.d.ts +21 -13
- package/dist/components/TabPanel.js +13 -2
- package/dist/components/Tabs.d.ts +70 -52
- package/dist/components/Tabs.js +15 -85
- package/dist/elements/a-checkbox.css +1 -1
- package/dist/elements/a-input.css +1 -1
- package/dist/elements/a-input.js +1 -0
- package/dist/elements/a-menu-item.css +1 -1
- package/dist/elements/a-menu-item.d.ts +8 -6
- package/dist/elements/a-menu.d.ts +13 -15
- package/dist/elements/a-menu.js +60 -24
- package/dist/elements/a-radio.css +1 -1
- package/dist/elements/a-tabpanel.css +1 -1
- package/dist/elements/a-tabpanel.d.ts +17 -0
- package/dist/elements/a-tabpanel.js +66 -0
- package/dist/elements/index.d.ts +1 -1
- package/dist/elements/index.js +3 -1
- package/dist/general_types.d.ts +31 -11
- package/dist/index.d.ts +3 -5
- package/dist/index.js +2 -3
- package/package.json +1 -1
package/dist/anta_helpers.d.ts
CHANGED
|
@@ -22,10 +22,6 @@ export declare function nativeStateChange<D>(e: CustomEvent<D> | {
|
|
|
22
22
|
event: CustomEvent<D>;
|
|
23
23
|
detail?: D;
|
|
24
24
|
};
|
|
25
|
-
export declare const TABS_KIND: unique symbol;
|
|
26
|
-
export declare function markTabsKind<F>(component: F & {
|
|
27
|
-
[TABS_KIND]?: "tab" | "panel";
|
|
28
|
-
}, kind: "tab" | "panel"): F;
|
|
29
25
|
/** The six named tones every toned component shares. Anything else is a literal
|
|
30
26
|
* CSS colour the element resolves through its `--{component}-tone-source` var. */
|
|
31
27
|
export declare const NAMED_TONES: Set<string>;
|
package/dist/anta_helpers.js
CHANGED
|
@@ -16,11 +16,6 @@ function nativeStateChange(e) {
|
|
|
16
16
|
const event = "nativeEvent" in e ? e.nativeEvent : e;
|
|
17
17
|
return { event, detail: event?.detail };
|
|
18
18
|
}
|
|
19
|
-
const TABS_KIND = Symbol.for("@antadesign/anta/tabs.kind");
|
|
20
|
-
function markTabsKind(component, kind) {
|
|
21
|
-
component[TABS_KIND] = kind;
|
|
22
|
-
return component;
|
|
23
|
-
}
|
|
24
19
|
const NAMED_TONES = /* @__PURE__ */ new Set([
|
|
25
20
|
"brand",
|
|
26
21
|
"neutral",
|
|
@@ -112,12 +107,10 @@ export {
|
|
|
112
107
|
HTMLElementBase,
|
|
113
108
|
NAMED_TONES,
|
|
114
109
|
SelectableChildElement,
|
|
115
|
-
TABS_KIND,
|
|
116
110
|
anchorRect,
|
|
117
111
|
hasChildren,
|
|
118
112
|
isInsideOpenMenu,
|
|
119
113
|
isMenuOpen,
|
|
120
|
-
markTabsKind,
|
|
121
114
|
nativeStateChange,
|
|
122
115
|
roundStyle,
|
|
123
116
|
setMenuPresence,
|
|
@@ -36,6 +36,11 @@ export interface CalendarProps extends Omit<BaseProps, "children" | "onChange">
|
|
|
36
36
|
size?: "small" | "medium" | "large";
|
|
37
37
|
/** Disable the whole calendar (not focusable or selectable). */
|
|
38
38
|
disabled?: boolean;
|
|
39
|
+
/** Move keyboard focus onto the active day. Change this to a new value (e.g.
|
|
40
|
+
* increment a counter) to focus the cursor cell — `InputDate` bumps it when
|
|
41
|
+
* the calendar is opened from the field by keyboard (ArrowDown), so focus
|
|
42
|
+
* lands in the grid. The initial value never focuses; only a change does. */
|
|
43
|
+
focusSignal?: number;
|
|
39
44
|
/** Accessible name for the grid (defaults to the visible month heading). */
|
|
40
45
|
"aria-label"?: string;
|
|
41
46
|
/** Fired whenever the selection changes — event-first. `detail` is
|
|
@@ -76,5 +81,5 @@ export interface CalendarProps extends Omit<BaseProps, "children" | "onChange">
|
|
|
76
81
|
* hydration mismatch. Render it client-side only (Astro `client:only`, or a
|
|
77
82
|
* dynamic import inside `useEffect`).
|
|
78
83
|
*/
|
|
79
|
-
export declare const Calendar: ({ value, defaultValue, min, max, locale, name, size, disabled, onStateChange, onChange, onValueChange, className, style, "aria-label": ariaLabel, ...rest }: CalendarProps) => any;
|
|
84
|
+
export declare const Calendar: ({ value, defaultValue, min, max, locale, name, size, disabled, focusSignal, onStateChange, onChange, onValueChange, className, style, "aria-label": ariaLabel, ...rest }: CalendarProps) => any;
|
|
80
85
|
export {};
|
|
@@ -17,6 +17,7 @@ const Calendar = ({
|
|
|
17
17
|
name,
|
|
18
18
|
size,
|
|
19
19
|
disabled,
|
|
20
|
+
focusSignal,
|
|
20
21
|
onStateChange,
|
|
21
22
|
onChange,
|
|
22
23
|
onValueChange,
|
|
@@ -42,6 +43,11 @@ const Calendar = ({
|
|
|
42
43
|
const headingId = useId();
|
|
43
44
|
const month = buildMonth({ anchor: cursor, locale: resolvedLocale, min: minD, max: maxD, selected, today });
|
|
44
45
|
const cursorIso = cursor.toString();
|
|
46
|
+
const [lastFocusSignal, setLastFocusSignal] = useState(focusSignal);
|
|
47
|
+
if (focusSignal !== lastFocusSignal) {
|
|
48
|
+
setLastFocusSignal(focusSignal);
|
|
49
|
+
if (focusSignal !== void 0) setFocusReq({ iso: cursorIso, n: focusSignal });
|
|
50
|
+
}
|
|
45
51
|
const minYear = minD ? minD.year : today.year - 3;
|
|
46
52
|
const maxYear = maxD ? maxD.year : today.year + 3;
|
|
47
53
|
const years = [];
|
|
@@ -45,7 +45,8 @@ export interface InputDateProps extends Omit<BaseProps, 'children'> {
|
|
|
45
45
|
round?: boolean | number | string;
|
|
46
46
|
/** Show a clear button once the field has a value. */
|
|
47
47
|
clearable?: boolean;
|
|
48
|
-
/** Leading icon at the start of the field.
|
|
48
|
+
/** Leading icon at the start of the field — the calendar affordance.
|
|
49
|
+
* @defaultValue calendar-days */
|
|
49
50
|
icon?: IconShape;
|
|
50
51
|
/** Include a time. The value becomes ISO `YYYY-MM-DDTHH:mm`, the field parses a
|
|
51
52
|
* trailing time after a space (`06/15/2026 14:30`, `… 2:30pm`), and the menu
|
|
@@ -67,10 +68,11 @@ export interface InputDateProps extends Omit<BaseProps, 'children'> {
|
|
|
67
68
|
* `Menu` + `Calendar` (no `a-inputdate` element; the wrapper is the coordinator).
|
|
68
69
|
* The field accepts free text and resolves it on commit (blur / Enter) with a
|
|
69
70
|
* lenient, locale-aware parser, then rewrites the entry to the canonical format;
|
|
70
|
-
* an unrecognized entry marks the field `critical` and keeps the text.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* an unrecognized entry marks the field `critical` and keeps the text. Clicking the
|
|
72
|
+
* field (or pressing ArrowDown) opens a `Calendar` in a menu; a leading calendar
|
|
73
|
+
* icon marks the affordance. Mouse-open keeps focus in the field so you can keep
|
|
74
|
+
* typing; ArrowDown moves focus into the grid. With `time`, a time row sits under
|
|
75
|
+
* the grid. The value is an ISO `YYYY-MM-DD` string, or `YYYY-MM-DDTHH:mm` with `time`.
|
|
74
76
|
*
|
|
75
77
|
* Controlled (`value` + `onValueChange`) or uncontrolled (`defaultValue`).
|
|
76
78
|
* Requires `@antadesign/anta/elements` (client-side only).
|
|
@@ -72,6 +72,7 @@ const InputDate = ({
|
|
|
72
72
|
const [text, setText] = useState(() => fmt(current));
|
|
73
73
|
const [open, setOpen] = useState(false);
|
|
74
74
|
const [invalid, setInvalid] = useState(false);
|
|
75
|
+
const [focusNonce, setFocusNonce] = useState(0);
|
|
75
76
|
const [hourText, setHourText] = useState(curHourShown);
|
|
76
77
|
const [minuteText, setMinuteText] = useState(curM);
|
|
77
78
|
const meridiem = curMer;
|
|
@@ -147,127 +148,121 @@ const InputDate = ({
|
|
|
147
148
|
round,
|
|
148
149
|
disabled,
|
|
149
150
|
clearable,
|
|
150
|
-
leading:
|
|
151
|
+
leading: /* @__PURE__ */ jsx(Icon, { shape: icon ?? "calendar-days" }),
|
|
151
152
|
value: text,
|
|
152
153
|
placeholder: pattern,
|
|
153
154
|
inputMode: time ? "text" : "numeric",
|
|
154
155
|
autoComplete: "off",
|
|
156
|
+
"aria-haspopup": "dialog",
|
|
157
|
+
"aria-expanded": open ? "true" : "false",
|
|
155
158
|
onInput: (e) => {
|
|
156
159
|
setText(e.currentTarget.value);
|
|
157
160
|
if (invalid) setInvalid(false);
|
|
158
161
|
},
|
|
159
|
-
onChange: (e) =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
onChange: (e) => resolve(e.currentTarget.value),
|
|
163
|
+
onKeyDown: (e) => {
|
|
164
|
+
if (e.key === "ArrowDown" && !disabled) {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
if (!open) e.currentTarget.click();
|
|
167
|
+
setFocusNonce((n) => n + 1);
|
|
168
|
+
}
|
|
162
169
|
},
|
|
163
170
|
className: cn(styles.dateField, className),
|
|
164
171
|
style,
|
|
165
|
-
|
|
172
|
+
...rest
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
/* @__PURE__ */ jsx(
|
|
176
|
+
Menu,
|
|
177
|
+
{
|
|
178
|
+
open,
|
|
179
|
+
placement: "bottom-start",
|
|
180
|
+
className: styles.calendarMenu,
|
|
181
|
+
onStateChange: (_e, { next }) => setOpen(next),
|
|
182
|
+
children: /* @__PURE__ */ jsxs("div", { "data-menu-open": "", children: [
|
|
166
183
|
/* @__PURE__ */ jsx(
|
|
167
|
-
|
|
184
|
+
Calendar,
|
|
168
185
|
{
|
|
169
|
-
|
|
186
|
+
value: dateISO,
|
|
187
|
+
min,
|
|
188
|
+
max,
|
|
189
|
+
locale,
|
|
170
190
|
size,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
191
|
+
disabled,
|
|
192
|
+
focusSignal: focusNonce,
|
|
193
|
+
onStateChange: (_e, { next, reason }) => {
|
|
194
|
+
if (reason !== "user") return;
|
|
195
|
+
const d = next ?? "";
|
|
196
|
+
apply(time && d ? withTime(d) : d);
|
|
197
|
+
if (!time) setOpen(false);
|
|
198
|
+
}
|
|
175
199
|
}
|
|
176
200
|
),
|
|
177
|
-
/* @__PURE__ */
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
"aria-label": "AM or PM",
|
|
243
|
-
options: [
|
|
244
|
-
{ value: "AM", label: "AM" },
|
|
245
|
-
{ value: "PM", label: "PM" }
|
|
246
|
-
],
|
|
247
|
-
value: meridiem,
|
|
248
|
-
onStateChange: (_e, { next }) => {
|
|
249
|
-
if (next === "AM" || next === "PM") commitTime(hourText, minuteText, next);
|
|
250
|
-
},
|
|
251
|
-
disabled
|
|
252
|
-
}
|
|
253
|
-
)
|
|
254
|
-
] }),
|
|
255
|
-
/* @__PURE__ */ jsx(
|
|
256
|
-
Button,
|
|
257
|
-
{
|
|
258
|
-
priority: "tertiary",
|
|
259
|
-
size,
|
|
260
|
-
icon: "check",
|
|
261
|
-
"aria-label": "Done",
|
|
262
|
-
onClick: () => setOpen(false)
|
|
263
|
-
}
|
|
264
|
-
)
|
|
265
|
-
] })
|
|
266
|
-
] })
|
|
267
|
-
}
|
|
268
|
-
)
|
|
269
|
-
] }),
|
|
270
|
-
...rest
|
|
201
|
+
time && /* @__PURE__ */ jsxs("div", { className: styles.timeRow, children: [
|
|
202
|
+
/* @__PURE__ */ jsxs("div", { className: styles.time, role: "group", "aria-label": "Time", children: [
|
|
203
|
+
/* @__PURE__ */ jsx(Icon, { shape: "clock", "aria-hidden": "true", className: styles.clock }),
|
|
204
|
+
/* @__PURE__ */ jsx(
|
|
205
|
+
Input,
|
|
206
|
+
{
|
|
207
|
+
size,
|
|
208
|
+
className: styles.timeField,
|
|
209
|
+
value: hourText,
|
|
210
|
+
placeholder: "HH",
|
|
211
|
+
type: "number",
|
|
212
|
+
inputMode: "numeric",
|
|
213
|
+
"aria-label": "Hours",
|
|
214
|
+
disabled,
|
|
215
|
+
onInput: (e) => setHourText(e.currentTarget.value.replace(/\D/g, "").slice(0, 2)),
|
|
216
|
+
onChange: (e) => commitTime(e.currentTarget.value, minuteText, meridiem)
|
|
217
|
+
}
|
|
218
|
+
),
|
|
219
|
+
/* @__PURE__ */ jsx("span", { className: styles.sep, "aria-hidden": "true", children: ":" }),
|
|
220
|
+
/* @__PURE__ */ jsx(
|
|
221
|
+
Input,
|
|
222
|
+
{
|
|
223
|
+
size,
|
|
224
|
+
className: styles.timeField,
|
|
225
|
+
value: minuteText,
|
|
226
|
+
placeholder: "MM",
|
|
227
|
+
type: "number",
|
|
228
|
+
inputMode: "numeric",
|
|
229
|
+
"aria-label": "Minutes",
|
|
230
|
+
disabled,
|
|
231
|
+
onInput: (e) => setMinuteText(e.currentTarget.value.replace(/\D/g, "").slice(0, 2)),
|
|
232
|
+
onChange: (e) => commitTime(hourText, e.currentTarget.value, meridiem)
|
|
233
|
+
}
|
|
234
|
+
),
|
|
235
|
+
twelveHour && /* @__PURE__ */ jsx(
|
|
236
|
+
Tabs,
|
|
237
|
+
{
|
|
238
|
+
className: styles.meridiem,
|
|
239
|
+
size,
|
|
240
|
+
priority: "primary",
|
|
241
|
+
"aria-label": "AM or PM",
|
|
242
|
+
options: [
|
|
243
|
+
{ value: "AM", label: "AM" },
|
|
244
|
+
{ value: "PM", label: "PM" }
|
|
245
|
+
],
|
|
246
|
+
value: meridiem,
|
|
247
|
+
onStateChange: (_e, { next }) => {
|
|
248
|
+
if (next === "AM" || next === "PM") commitTime(hourText, minuteText, next);
|
|
249
|
+
},
|
|
250
|
+
disabled
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
] }),
|
|
254
|
+
/* @__PURE__ */ jsx(
|
|
255
|
+
Button,
|
|
256
|
+
{
|
|
257
|
+
priority: "tertiary",
|
|
258
|
+
size,
|
|
259
|
+
icon: "check",
|
|
260
|
+
"aria-label": "Done",
|
|
261
|
+
onClick: () => setOpen(false)
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
] })
|
|
265
|
+
] })
|
|
271
266
|
}
|
|
272
267
|
),
|
|
273
268
|
name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: current ?? "" }) : null
|
|
@@ -44,6 +44,15 @@ export interface MenuProps extends BaseProps {
|
|
|
44
44
|
next: boolean;
|
|
45
45
|
prev: boolean;
|
|
46
46
|
}) => void;
|
|
47
|
+
/** Combobox-mode cursor report (raw element event). Fires when the active
|
|
48
|
+
* option changes as arrow keys move the cursor while focus stays in a
|
|
49
|
+
* `[data-menu-search]` filter field; `detail.id` is the active option's `id`
|
|
50
|
+
* (`null` when none). The reactive layer that owns the field reflects this
|
|
51
|
+
* onto its `aria-activedescendant` — the element can't write that light-DOM
|
|
52
|
+
* attribute itself. Passed straight through to the element. */
|
|
53
|
+
onactivedescendant?: (e: CustomEvent<{
|
|
54
|
+
id: string | null;
|
|
55
|
+
}>) => void;
|
|
47
56
|
/** The menu's contents: `MenuItem`, `MenuSeparator`, `MenuGroup`, or any
|
|
48
57
|
* custom element. */
|
|
49
58
|
children?: React.ReactNode;
|
|
@@ -39,14 +39,8 @@ const MenuItem = ({
|
|
|
39
39
|
style: toneStyle(effectiveTone, "--menu-item-tone-source", style),
|
|
40
40
|
submenu: submenu ? "" : void 0,
|
|
41
41
|
"aria-haspopup": submenu ? "menu" : void 0,
|
|
42
|
-
"aria-expanded": submenu ? "false" : void 0,
|
|
43
42
|
"aria-disabled": disabled ? "true" : void 0,
|
|
44
|
-
|
|
45
|
-
if (submenu) return;
|
|
46
|
-
const t = e.target;
|
|
47
|
-
if (t?.closest?.("a-menu-item") !== e.currentTarget) return;
|
|
48
|
-
onSelect(e, { value, label });
|
|
49
|
-
},
|
|
43
|
+
onmenuselect: onSelect ? (e) => onSelect(e, { value, label }) : void 0,
|
|
50
44
|
class: className,
|
|
51
45
|
...rest,
|
|
52
46
|
children: [
|
|
@@ -5,7 +5,9 @@ import type { IconShape } from '../elements/a-icon.shapes';
|
|
|
5
5
|
* arbitrary fields (a `ranAt` date, a `status`, …) and read them back in
|
|
6
6
|
* `renderOption`; the built-in filter still matches on `value`/`label`/`hint`. */
|
|
7
7
|
export interface SelectOption {
|
|
8
|
-
/** The option's value — its identity
|
|
8
|
+
/** The option's value — its identity, what `value` / `defaultValue` name, and what
|
|
9
|
+
* `onValueChange` reports. Unique across the whole `options` tree (selection is
|
|
10
|
+
* value-keyed and global across groups / submenus). */
|
|
9
11
|
value: string;
|
|
10
12
|
/** Visible label. Defaults to `value`. */
|
|
11
13
|
label?: string;
|
|
@@ -18,6 +20,11 @@ export interface SelectOption {
|
|
|
18
20
|
/** Tone for this option's row (label, icon, hint, selected tint, and the
|
|
19
21
|
* checkbox/radio indicator). A named tone or a custom CSS color. */
|
|
20
22
|
tone?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical' | (string & {});
|
|
23
|
+
/** Tooltip for this option's row — a string or any node. In a `multiple`
|
|
24
|
+
* select with `selectAll`, a row with no `tooltip` falls back to a default
|
|
25
|
+
* hint for the Alt/Option-click "select only this" accelerator; set `tooltip`
|
|
26
|
+
* to override that, or `''` to suppress it. */
|
|
27
|
+
tooltip?: React.ReactNode;
|
|
21
28
|
/** Your own data — attach anything and read it in `renderOption`. */
|
|
22
29
|
[key: string]: unknown;
|
|
23
30
|
}
|
|
@@ -113,7 +120,12 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
|
|
|
113
120
|
/** The options to choose from — bare strings, `SelectOption` objects, `SelectGroup`s
|
|
114
121
|
* (inline titled sections), or `SelectSubmenu`s (flyout branches). Groups and
|
|
115
122
|
* submenus nest and mix with plain options. Selection stays global (one `value`,
|
|
116
|
-
* leaf options only); a filter query flattens the tree into grouped results.
|
|
123
|
+
* leaf options only); a filter query flattens the tree into grouped results.
|
|
124
|
+
*
|
|
125
|
+
* Each leaf `value` is the option's identity and must be **unique across the whole
|
|
126
|
+
* tree** — selection is value-keyed, so a value repeated in two sections is one
|
|
127
|
+
* logical pick (both rows toggle together; the trigger resolves to the last). Dev
|
|
128
|
+
* builds `console.warn` on a duplicate. */
|
|
117
129
|
options: SelectItem[];
|
|
118
130
|
/** The per-row mark for **single**-select: `'none'` (a tint-only highlight),
|
|
119
131
|
* `'check'` (a trailing checkmark on the selected row, keeping the tint — the
|
|
@@ -204,11 +216,13 @@ export type SelectProps = SelectCommonProps & ({
|
|
|
204
216
|
* array value.
|
|
205
217
|
* @defaultValue single */
|
|
206
218
|
selection?: 'single';
|
|
207
|
-
/** Controlled value
|
|
208
|
-
*
|
|
209
|
-
* (reject by not updating).
|
|
219
|
+
/** Controlled value — the `value` string of the selected option. When
|
|
220
|
+
* provided, the consumer owns selection: the field follows this prop and a
|
|
221
|
+
* pick only *requests* a change via `onValueChange` (reject by not updating).
|
|
222
|
+
* Leave undefined for uncontrolled. */
|
|
210
223
|
value?: string;
|
|
211
|
-
/** Initial value for the uncontrolled case
|
|
224
|
+
/** Initial value (an option's `value` string) for the uncontrolled case — the
|
|
225
|
+
* wrapper then owns it. */
|
|
212
226
|
defaultValue?: string;
|
|
213
227
|
/** Fires after the selection changes, with the new value and a
|
|
214
228
|
* `{ value, option }` snapshot. Select has no discrete element state, so
|
|
@@ -218,15 +232,65 @@ export type SelectProps = SelectCommonProps & ({
|
|
|
218
232
|
/** Multi-select: checkboxes on every row, the menu stays open while
|
|
219
233
|
* toggling, the field shows an "N selected" count, and `value` is an array. */
|
|
220
234
|
selection: 'multiple';
|
|
221
|
-
/** Controlled values
|
|
235
|
+
/** Controlled values — the `value` strings of the selected options (see the
|
|
236
|
+
* single-select `value` note). */
|
|
222
237
|
value?: string[];
|
|
223
|
-
/** Initial values for the uncontrolled case. */
|
|
238
|
+
/** Initial values (option `value` strings) for the uncontrolled case. */
|
|
224
239
|
defaultValue?: string[];
|
|
225
240
|
/** Fires after any toggle, with the new value array and a `{ value, option,
|
|
226
241
|
* selected }` snapshot of the row that changed (or `{ all: true }` for the
|
|
227
242
|
* Select-all row). */
|
|
228
243
|
onValueChange?: (value: string[], attrs: SelectChangeAttrs) => void;
|
|
229
244
|
});
|
|
245
|
+
/** Rolled-up selection of a group / submenu subtree: `'none'` / `'all'` of the
|
|
246
|
+
* descendant leaves selected, or `'some'` in between. On `SelectedGroup` /
|
|
247
|
+
* `SelectedSubmenu`, for a section indicator or custom styling. */
|
|
248
|
+
export type SelectionState = 'none' | 'some' | 'all';
|
|
249
|
+
/** A leaf option annotated with its current selection — a `SelectOption` plus
|
|
250
|
+
* `selected`. Returned by {@link optionsWithSelection}. */
|
|
251
|
+
export type SelectedOption = SelectOption & {
|
|
252
|
+
selected: boolean;
|
|
253
|
+
};
|
|
254
|
+
/** A group with its descendants annotated and a rolled-up `selectionState`.
|
|
255
|
+
* Returned by {@link optionsWithSelection}. */
|
|
256
|
+
export interface SelectedGroup extends Omit<SelectGroup, 'options'> {
|
|
257
|
+
options: SelectedItem[];
|
|
258
|
+
selectionState: SelectionState;
|
|
259
|
+
}
|
|
260
|
+
/** A submenu with its descendants annotated and a rolled-up `selectionState`.
|
|
261
|
+
* Returned by {@link optionsWithSelection}. */
|
|
262
|
+
export interface SelectedSubmenu extends Omit<SelectSubmenu, 'submenu'> {
|
|
263
|
+
submenu: SelectedItem[];
|
|
264
|
+
selectionState: SelectionState;
|
|
265
|
+
}
|
|
266
|
+
/** One node of the tree from {@link optionsWithSelection}: a leaf `SelectedOption`
|
|
267
|
+
* (with `selected`), or a `SelectedGroup` / `SelectedSubmenu` (annotated children
|
|
268
|
+
* + rolled-up `selectionState`). Mirrors `SelectItem` minus the bare-string
|
|
269
|
+
* shorthand — strings are normalized to `SelectedOption`. */
|
|
270
|
+
export type SelectedItem = SelectedOption | SelectedGroup | SelectedSubmenu;
|
|
271
|
+
/**
|
|
272
|
+
* Project a `Select` `options` tree onto a set of selected values: returns a mirror
|
|
273
|
+
* of the tree with every leaf marked `selected` and every group / submenu carrying a
|
|
274
|
+
* rolled-up `selectionState` (`'none'` / `'some'` / `'all'` of its descendant leaves).
|
|
275
|
+
* Bare-string options are normalized to `{ value, label }`; structure and order are
|
|
276
|
+
* preserved.
|
|
277
|
+
*
|
|
278
|
+
* A pure function of `(options, values)` — it needs no `Select` instance and reads
|
|
279
|
+
* nothing off the change event, so it behaves identically in controlled and
|
|
280
|
+
* uncontrolled code. Pass the current `value` (a string, an array, or `undefined`)
|
|
281
|
+
* and render a grouped summary, drive section indicators, or diff picks.
|
|
282
|
+
*
|
|
283
|
+
* A value that appears under more than one section marks the leaf in *every* place it
|
|
284
|
+
* occurs (selection is value-keyed), mirroring how the menu itself renders it.
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```tsx
|
|
288
|
+
* const tree = optionsWithSelection(options, values)
|
|
289
|
+
* // tree[1] === { label: 'Engineering', selectionState: 'some', options: [
|
|
290
|
+
* // { value: 'eng-fe', label: 'Frontend', selected: false }, … ] }
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export declare function optionsWithSelection(options: SelectItem[], values: string | string[] | undefined): SelectedItem[];
|
|
230
294
|
/**
|
|
231
295
|
* `<Select>` — a single- or multi-select dropdown, composed from `<Input>` (a
|
|
232
296
|
* read-only trigger) and `<Menu>` (the options). `selection` sets behaviour:
|
|
@@ -6,8 +6,34 @@ import { Menu } from "./Menu";
|
|
|
6
6
|
import { MenuItem } from "./MenuItem";
|
|
7
7
|
import { MenuGroup } from "./MenuGroup";
|
|
8
8
|
import { MenuSeparator } from "./MenuSeparator";
|
|
9
|
+
import { Tooltip } from "./Tooltip";
|
|
9
10
|
import styles from "./Select.module.css";
|
|
11
|
+
const IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad/i.test(navigator.userAgent || "");
|
|
10
12
|
const normalize = (o) => typeof o === "string" ? { value: o, label: o } : o;
|
|
13
|
+
function optionsWithSelection(options, values) {
|
|
14
|
+
const set = new Set(values == null ? [] : Array.isArray(values) ? values : [values]);
|
|
15
|
+
const state = (on, total) => on === 0 ? "none" : on === total ? "all" : "some";
|
|
16
|
+
const walk = (items) => items.map((raw) => {
|
|
17
|
+
if (typeof raw !== "string" && Array.isArray(raw.submenu)) {
|
|
18
|
+
const sm = raw;
|
|
19
|
+
const children = walk(sm.submenu);
|
|
20
|
+
const on = children.reduce((n, c) => n + c.on, 0);
|
|
21
|
+
const total = children.reduce((n, c) => n + c.total, 0);
|
|
22
|
+
return { node: { ...sm, submenu: children.map((c) => c.node), selectionState: state(on, total) }, on, total };
|
|
23
|
+
}
|
|
24
|
+
if (typeof raw !== "string" && Array.isArray(raw.options)) {
|
|
25
|
+
const g = raw;
|
|
26
|
+
const children = walk(g.options);
|
|
27
|
+
const on = children.reduce((n, c) => n + c.on, 0);
|
|
28
|
+
const total = children.reduce((n, c) => n + c.total, 0);
|
|
29
|
+
return { node: { ...g, options: children.map((c) => c.node), selectionState: state(on, total) }, on, total };
|
|
30
|
+
}
|
|
31
|
+
const o = normalize(raw);
|
|
32
|
+
const selected = set.has(o.value);
|
|
33
|
+
return { node: { ...o, selected }, on: selected ? 1 : 0, total: 1 };
|
|
34
|
+
});
|
|
35
|
+
return walk(options).map((c) => c.node);
|
|
36
|
+
}
|
|
11
37
|
const Select = (props) => {
|
|
12
38
|
const {
|
|
13
39
|
options,
|
|
@@ -47,6 +73,7 @@ const Select = (props) => {
|
|
|
47
73
|
const currentRaw = controlled ? value : internal;
|
|
48
74
|
const [open, setOpen] = useState(false);
|
|
49
75
|
const [query, setQuery] = useState("");
|
|
76
|
+
const [activeId, setActiveId] = useState(null);
|
|
50
77
|
const uid = useId();
|
|
51
78
|
const isSubmenu = (it) => typeof it === "object" && Array.isArray(it.submenu);
|
|
52
79
|
const isGroup = (it) => typeof it === "object" && Array.isArray(it.options);
|
|
@@ -62,6 +89,12 @@ const Select = (props) => {
|
|
|
62
89
|
};
|
|
63
90
|
const allLeaves = [];
|
|
64
91
|
collectLeaves(options, void 0, false, allLeaves);
|
|
92
|
+
const seenValues = /* @__PURE__ */ new Set();
|
|
93
|
+
for (const l of allLeaves) {
|
|
94
|
+
if (seenValues.has(l.opt.value))
|
|
95
|
+
console.warn(`[anta] <Select> duplicate option value=${JSON.stringify(l.opt.value)} \u2014 values must be unique.`);
|
|
96
|
+
seenValues.add(l.opt.value);
|
|
97
|
+
}
|
|
65
98
|
const byValue = new Map(allLeaves.map((l) => [l.opt.value, l.opt]));
|
|
66
99
|
const filtering = filter !== void 0 && filter !== false;
|
|
67
100
|
const q = query.trim();
|
|
@@ -98,8 +131,14 @@ const Select = (props) => {
|
|
|
98
131
|
const o = byValue.get(currentRaw);
|
|
99
132
|
display = o ? o.label ?? o.value : "";
|
|
100
133
|
}
|
|
101
|
-
const choose = (o) => {
|
|
134
|
+
const choose = (o, e) => {
|
|
102
135
|
if (multiple) {
|
|
136
|
+
if (selectAll && e?.altKey) {
|
|
137
|
+
const next2 = [o.value];
|
|
138
|
+
if (!controlled) setInternal(next2);
|
|
139
|
+
emit?.(next2, { value: o.value, option: o, selected: true });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
103
142
|
const has = selectedValues.includes(o.value);
|
|
104
143
|
const next = has ? selectedValues.filter((v) => v !== o.value) : [...selectedValues, o.value];
|
|
105
144
|
if (!controlled) setInternal(next);
|
|
@@ -124,7 +163,10 @@ const Select = (props) => {
|
|
|
124
163
|
const custom = renderOption?.(o, optState);
|
|
125
164
|
const customMark = renderIndicator?.(optState);
|
|
126
165
|
const ariaSelectable = !multiple && mark === "none" ? { role: "menuitemradio", "aria-checked": isSelected(o.value) ? "true" : "false" } : void 0;
|
|
127
|
-
|
|
166
|
+
const isolateHint = multiple && selectAll && !disabled2 ? IS_MAC ? "\u2325+Click to select only this" : "Alt+Click to select only this" : void 0;
|
|
167
|
+
const tip = o.tooltip ?? isolateHint;
|
|
168
|
+
const hintOnly = o.tooltip == null && isolateHint != null;
|
|
169
|
+
return /* @__PURE__ */ jsxs(
|
|
128
170
|
MenuItem,
|
|
129
171
|
{
|
|
130
172
|
id: `${uid}-opt-${o.value}`,
|
|
@@ -139,8 +181,11 @@ const Select = (props) => {
|
|
|
139
181
|
selected: isSelected(o.value),
|
|
140
182
|
disabled: disabled2 || void 0,
|
|
141
183
|
"data-menu-open": multiple ? "" : void 0,
|
|
142
|
-
onSelect: () => choose(o),
|
|
143
|
-
children:
|
|
184
|
+
onSelect: (e) => choose(o, e),
|
|
185
|
+
children: [
|
|
186
|
+
custom,
|
|
187
|
+
tip && /* @__PURE__ */ jsx(Tooltip, { ...hintOnly ? { follow: true, delay: 700 } : {}, children: tip })
|
|
188
|
+
]
|
|
144
189
|
},
|
|
145
190
|
o.value
|
|
146
191
|
);
|
|
@@ -246,8 +291,12 @@ const Select = (props) => {
|
|
|
246
291
|
{
|
|
247
292
|
onStateChange: (_e, { next }) => {
|
|
248
293
|
setOpen(next);
|
|
249
|
-
if (!next)
|
|
294
|
+
if (!next) {
|
|
295
|
+
setQuery("");
|
|
296
|
+
setActiveId(null);
|
|
297
|
+
}
|
|
250
298
|
},
|
|
299
|
+
onactivedescendant: (e) => setActiveId((e.nativeEvent ?? e).detail?.id ?? null),
|
|
251
300
|
children: [
|
|
252
301
|
filtering && // `slot="header"` pins the field in the Menu's fixed header region (above
|
|
253
302
|
// the scrolling options); `data-menu-search` puts the menu in combobox
|
|
@@ -261,6 +310,7 @@ const Select = (props) => {
|
|
|
261
310
|
placeholder: "Filter\u2026",
|
|
262
311
|
"aria-label": "Filter options",
|
|
263
312
|
"aria-autocomplete": "list",
|
|
313
|
+
"aria-activedescendant": open && activeId ? activeId : void 0,
|
|
264
314
|
onInput: (e) => setQuery(e.currentTarget.value)
|
|
265
315
|
}
|
|
266
316
|
) }),
|
|
@@ -286,5 +336,6 @@ const Select = (props) => {
|
|
|
286
336
|
] });
|
|
287
337
|
};
|
|
288
338
|
export {
|
|
289
|
-
Select
|
|
339
|
+
Select,
|
|
340
|
+
optionsWithSelection
|
|
290
341
|
};
|