@antadesign/anta 0.3.3 → 0.3.5
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 +9 -3
- package/dist/components/Calendar.js +24 -7
- package/dist/components/InputDate.d.ts +9 -6
- package/dist/components/InputDate.js +83 -143
- package/dist/components/InputDate.module.css +1 -1
- package/dist/components/InputTime.d.ts +105 -0
- package/dist/components/InputTime.js +110 -0
- package/dist/components/Menu.d.ts +15 -1
- package/dist/components/Menu.js +2 -0
- package/dist/components/MenuItem.js +1 -7
- package/dist/components/Select.d.ts +96 -14
- package/dist/components/Select.js +69 -8
- package/dist/components/Select.module.css +1 -1
- package/dist/components/SelectFaceted.d.ts +184 -0
- package/dist/components/SelectFaceted.js +331 -0
- package/dist/components/SelectFaceted.module.css +1 -0
- package/dist/components/TabPanel.d.ts +21 -13
- package/dist/components/TabPanel.js +13 -2
- package/dist/components/Tabs.d.ts +76 -52
- package/dist/components/Tabs.js +18 -86
- package/dist/elements/a-button.css +1 -1
- package/dist/elements/a-calendar.css +1 -1
- package/dist/elements/a-checkbox.css +1 -1
- package/dist/elements/a-input-time.css +1 -0
- package/dist/elements/a-input-time.d.ts +27 -0
- package/dist/elements/a-input-time.js +856 -0
- package/dist/elements/a-input.css +1 -1
- package/dist/elements/a-input.js +8 -1
- 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 +71 -25
- package/dist/elements/a-radio.css +1 -1
- package/dist/elements/a-tab.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/a-tabs.css +1 -1
- package/dist/elements/a-tooltip.js +1 -1
- package/dist/elements/index.d.ts +2 -1
- package/dist/elements/index.js +6 -1
- package/dist/general_types.d.ts +83 -11
- package/dist/index.d.ts +7 -5
- package/dist/index.js +6 -3
- package/dist/jsx-runtime.d.ts +2 -1
- package/dist/reset.css +1 -1
- package/dist/tokens.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx, jsxs } from "@antadesign/anta/jsx-runtime";
|
|
2
|
+
import { nativeStateChange, toneStyle, roundStyle } from "../anta_helpers";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
import { Icon } from "./Icon";
|
|
5
|
+
const presence = (on) => on ? "" : void 0;
|
|
6
|
+
const isStringish = (n) => typeof n === "string" || typeof n === "number";
|
|
7
|
+
const attrsOf = (e) => {
|
|
8
|
+
const el = e?.target ?? {};
|
|
9
|
+
const value = el.value ?? "";
|
|
10
|
+
return {
|
|
11
|
+
value,
|
|
12
|
+
name: el.getAttribute?.("name") ?? void 0,
|
|
13
|
+
empty: !value,
|
|
14
|
+
valid: el.validity ? el.validity.valid : void 0,
|
|
15
|
+
validationMessage: el.validationMessage ?? ""
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
const STATUS_ICON = {
|
|
19
|
+
critical: "warning-diamond",
|
|
20
|
+
warning: "warning-triangle",
|
|
21
|
+
success: "circle-check",
|
|
22
|
+
info: "info",
|
|
23
|
+
brand: "circle-small-solid"
|
|
24
|
+
};
|
|
25
|
+
const InputTime = ({
|
|
26
|
+
label,
|
|
27
|
+
hint,
|
|
28
|
+
status,
|
|
29
|
+
statusIcon,
|
|
30
|
+
tone,
|
|
31
|
+
size,
|
|
32
|
+
round,
|
|
33
|
+
value,
|
|
34
|
+
defaultValue,
|
|
35
|
+
locale,
|
|
36
|
+
hour12,
|
|
37
|
+
min,
|
|
38
|
+
max,
|
|
39
|
+
name,
|
|
40
|
+
disabled,
|
|
41
|
+
required,
|
|
42
|
+
icon,
|
|
43
|
+
clearable,
|
|
44
|
+
dimActions,
|
|
45
|
+
trailing,
|
|
46
|
+
onValueChange,
|
|
47
|
+
onClearInput,
|
|
48
|
+
children,
|
|
49
|
+
className,
|
|
50
|
+
style,
|
|
51
|
+
...rest
|
|
52
|
+
}) => {
|
|
53
|
+
const statusTone = status && status !== "neutral" ? status : void 0;
|
|
54
|
+
const glyph = statusIcon === void 0 ? statusTone ? STATUS_ICON[statusTone] : void 0 : statusIcon;
|
|
55
|
+
return /* @__PURE__ */ jsxs(
|
|
56
|
+
"a-input-time",
|
|
57
|
+
{
|
|
58
|
+
size: size && size !== "medium" ? size : void 0,
|
|
59
|
+
round: round ? "" : void 0,
|
|
60
|
+
value,
|
|
61
|
+
defaultvalue: defaultValue,
|
|
62
|
+
locale,
|
|
63
|
+
hour12: hour12 === void 0 ? void 0 : hour12 ? "true" : "false",
|
|
64
|
+
min,
|
|
65
|
+
max,
|
|
66
|
+
status: statusTone,
|
|
67
|
+
tone: tone || void 0,
|
|
68
|
+
name,
|
|
69
|
+
disabled: presence(disabled),
|
|
70
|
+
required: presence(required),
|
|
71
|
+
"dim-actions": presence(dimActions),
|
|
72
|
+
"aria-invalid": status === "critical" ? "true" : void 0,
|
|
73
|
+
oninput: onValueChange ? (e) => onValueChange(e, attrsOf(e)) : void 0,
|
|
74
|
+
onchange: onValueChange ? (e) => onValueChange(e, attrsOf(e)) : void 0,
|
|
75
|
+
onclearinput: onClearInput ? (e) => onClearInput(nativeStateChange(e).event) : void 0,
|
|
76
|
+
class: className,
|
|
77
|
+
style: roundStyle(round, "--input-time-round", toneStyle(tone, "--input-time-tone-source", style)),
|
|
78
|
+
...rest,
|
|
79
|
+
children: [
|
|
80
|
+
label != null && (isStringish(label) ? /* @__PURE__ */ jsx("span", { slot: "label", children: label }) : /* @__PURE__ */ jsx("span", { slot: "label", style: { display: "contents" }, children: label })),
|
|
81
|
+
icon !== false && /* @__PURE__ */ jsx("span", { slot: "leading", style: { display: "contents" }, children: /* @__PURE__ */ jsx(Icon, { shape: icon ?? "clock" }) }),
|
|
82
|
+
clearable && // Real <a-button> in the element's `clear` slot; the element owns its
|
|
83
|
+
// visibility (shown only when filled). It fires `clearrequest`, which the
|
|
84
|
+
// element turns into clear(). CONTRACT: `data-custom-event` MUST match
|
|
85
|
+
// CLEAR_TRIGGER in src/elements/a-input-time.ts (string duplicated, not
|
|
86
|
+
// shared, to keep the wrapper/element decoupled).
|
|
87
|
+
/* @__PURE__ */ jsx("span", { slot: "clear", style: { display: "contents" }, children: /* @__PURE__ */ jsx(
|
|
88
|
+
Button,
|
|
89
|
+
{
|
|
90
|
+
priority: "tertiary",
|
|
91
|
+
size,
|
|
92
|
+
round: !!round,
|
|
93
|
+
icon: "x",
|
|
94
|
+
"aria-label": "Clear",
|
|
95
|
+
"data-custom-event": "clearrequest"
|
|
96
|
+
}
|
|
97
|
+
) }),
|
|
98
|
+
trailing != null && /* @__PURE__ */ jsx("span", { slot: "trailing", style: { display: "contents" }, children: trailing }),
|
|
99
|
+
hint != null && /* @__PURE__ */ jsxs("span", { slot: "hint", style: { display: "contents" }, children: [
|
|
100
|
+
glyph && /* @__PURE__ */ jsx(Icon, { shape: glyph, "aria-hidden": "true" }),
|
|
101
|
+
/* @__PURE__ */ jsx("span", { children: hint })
|
|
102
|
+
] }),
|
|
103
|
+
children
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
export {
|
|
109
|
+
InputTime
|
|
110
|
+
};
|
|
@@ -22,6 +22,11 @@ export interface MenuProps extends BaseProps {
|
|
|
22
22
|
/** Gap in pixels between the trigger and the menu.
|
|
23
23
|
* @defaultValue 4 */
|
|
24
24
|
offset?: number;
|
|
25
|
+
/** Size the menu to its content instead of flooring its width to the trigger.
|
|
26
|
+
* A root menu is never narrower than its trigger by default; set this for a
|
|
27
|
+
* content menu under a wide trigger (e.g. a calendar below a full-width field)
|
|
28
|
+
* so it wraps its content and left-aligns under the trigger. */
|
|
29
|
+
autoWidth?: boolean;
|
|
25
30
|
/** Controlled open state. Omit for the default **uncontrolled** menu (it
|
|
26
31
|
* opens/closes itself via its triggers). Pass a boolean to **control** it:
|
|
27
32
|
* the menu's visibility follows `open`, and user dismiss (Esc, outside-click,
|
|
@@ -44,6 +49,15 @@ export interface MenuProps extends BaseProps {
|
|
|
44
49
|
next: boolean;
|
|
45
50
|
prev: boolean;
|
|
46
51
|
}) => void;
|
|
52
|
+
/** Combobox-mode cursor report (raw element event). Fires when the active
|
|
53
|
+
* option changes as arrow keys move the cursor while focus stays in a
|
|
54
|
+
* `[data-menu-search]` filter field; `detail.id` is the active option's `id`
|
|
55
|
+
* (`null` when none). The reactive layer that owns the field reflects this
|
|
56
|
+
* onto its `aria-activedescendant` — the element can't write that light-DOM
|
|
57
|
+
* attribute itself. Passed straight through to the element. */
|
|
58
|
+
onactivedescendant?: (e: CustomEvent<{
|
|
59
|
+
id: string | null;
|
|
60
|
+
}>) => void;
|
|
47
61
|
/** The menu's contents: `MenuItem`, `MenuSeparator`, `MenuGroup`, or any
|
|
48
62
|
* custom element. */
|
|
49
63
|
children?: React.ReactNode;
|
|
@@ -74,4 +88,4 @@ export interface MenuProps extends BaseProps {
|
|
|
74
88
|
* </Menu>
|
|
75
89
|
* ```
|
|
76
90
|
*/
|
|
77
|
-
export declare const Menu: ({ placement, context, coord, nohover, offset, open, onStateChange, round, className, style, children, ...rest }: MenuProps) => any;
|
|
91
|
+
export declare const Menu: ({ placement, context, coord, nohover, offset, autoWidth, open, onStateChange, round, className, style, children, ...rest }: MenuProps) => any;
|
package/dist/components/Menu.js
CHANGED
|
@@ -6,6 +6,7 @@ const Menu = ({
|
|
|
6
6
|
coord,
|
|
7
7
|
nohover,
|
|
8
8
|
offset,
|
|
9
|
+
autoWidth,
|
|
9
10
|
open,
|
|
10
11
|
onStateChange,
|
|
11
12
|
round,
|
|
@@ -22,6 +23,7 @@ const Menu = ({
|
|
|
22
23
|
coord: coord ? "" : void 0,
|
|
23
24
|
nohover: nohover ? "" : void 0,
|
|
24
25
|
offset: offset != null ? String(offset) : void 0,
|
|
26
|
+
autowidth: autoWidth ? "" : void 0,
|
|
25
27
|
state: open === void 0 ? void 0 : open ? "open" : "closed",
|
|
26
28
|
onstatechange: onStateChange ? (e) => {
|
|
27
29
|
const { event, detail } = nativeStateChange(e);
|
|
@@ -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
|
}
|
|
@@ -90,8 +97,10 @@ export interface TriggerState {
|
|
|
90
97
|
}
|
|
91
98
|
/** Snapshot passed as the 2nd argument to `onValueChange` — describes *what*
|
|
92
99
|
* changed, alongside the new full value in the 1st argument. A discriminated
|
|
93
|
-
* union: a row toggle carries `value` + `option`; the "Select all"
|
|
94
|
-
*
|
|
100
|
+
* union: a row toggle carries `value` + `option`; a bulk change (the "Select all"
|
|
101
|
+
* row, or the `clearable` "Clear" footer) carries `all: true` instead. **Always
|
|
102
|
+
* narrow on `'all' in attrs` before reading `option`** — the `all` variant fires
|
|
103
|
+
* for single-select too (via `clearable`), not just multiple. */
|
|
95
104
|
export type SelectChangeAttrs = {
|
|
96
105
|
/** The option value that changed — the chosen value (single) or the toggled
|
|
97
106
|
* row (multiple). */
|
|
@@ -101,9 +110,11 @@ export type SelectChangeAttrs = {
|
|
|
101
110
|
/** Multiple only: whether the change turned selection **on** (true) or off. */
|
|
102
111
|
selected?: boolean;
|
|
103
112
|
} | {
|
|
104
|
-
/** Marks the change as
|
|
113
|
+
/** Marks the change as a bulk one with no single `option`: the "Select all"
|
|
114
|
+
* row (multiple), or the `clearable` "Clear" footer (single or multiple). */
|
|
105
115
|
all: true;
|
|
106
|
-
/** Whether
|
|
116
|
+
/** Whether the bulk change turned everything **on** (true) or cleared it
|
|
117
|
+
* (false — always false for a "Clear"). */
|
|
107
118
|
selected: boolean;
|
|
108
119
|
};
|
|
109
120
|
/** Props shared by both selection modes, intersected into `SelectProps`. Exported
|
|
@@ -113,7 +124,12 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
|
|
|
113
124
|
/** The options to choose from — bare strings, `SelectOption` objects, `SelectGroup`s
|
|
114
125
|
* (inline titled sections), or `SelectSubmenu`s (flyout branches). Groups and
|
|
115
126
|
* 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.
|
|
127
|
+
* leaf options only); a filter query flattens the tree into grouped results.
|
|
128
|
+
*
|
|
129
|
+
* Each leaf `value` is the option's identity and must be **unique across the whole
|
|
130
|
+
* tree** — selection is value-keyed, so a value repeated in two sections is one
|
|
131
|
+
* logical pick (both rows toggle together; the trigger resolves to the last). Dev
|
|
132
|
+
* builds `console.warn` on a duplicate. */
|
|
117
133
|
options: SelectItem[];
|
|
118
134
|
/** The per-row mark for **single**-select: `'none'` (a tint-only highlight),
|
|
119
135
|
* `'check'` (a trailing checkmark on the selected row, keeping the tint — the
|
|
@@ -127,6 +143,11 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
|
|
|
127
143
|
* `leading` slot). With a custom `renderTrigger`, it's passed through as
|
|
128
144
|
* `state.icon` instead — the consumer places it. */
|
|
129
145
|
icon?: IconShape;
|
|
146
|
+
/** Arbitrary content for the default trigger's leading slot (piped straight to
|
|
147
|
+
* `Input`'s `leading`) — e.g. a key prefix before the value. Overrides the
|
|
148
|
+
* `icon`-derived glyph when both are set; include your own `<Icon>` if you want
|
|
149
|
+
* one alongside. Ignored with a custom `renderTrigger`. */
|
|
150
|
+
leading?: React.ReactNode;
|
|
130
151
|
/** Field label, above the trigger (Input's `label`). */
|
|
131
152
|
label?: string;
|
|
132
153
|
/** Helper text under the field (Input's `hint`). */
|
|
@@ -162,6 +183,13 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
|
|
|
162
183
|
/** Label for the `selectAll` row.
|
|
163
184
|
* @defaultValue Select all */
|
|
164
185
|
selectAllLabel?: string;
|
|
186
|
+
/** Add a "Clear" row pinned in the menu **footer** that empties the selection
|
|
187
|
+
* (single → none, multiple → `[]`). Shown only while something is selected, so
|
|
188
|
+
* it never scrolls away in a long or filtered list. */
|
|
189
|
+
clearable?: boolean;
|
|
190
|
+
/** Label for the `clearable` footer row.
|
|
191
|
+
* @defaultValue Clear */
|
|
192
|
+
clearLabel?: string;
|
|
165
193
|
/** Render the **content** of each option row yourself, replacing the built-in
|
|
166
194
|
* `label`/`hint`/`icon` layout. Select still supplies the row box, click, ARIA,
|
|
167
195
|
* and the selection indicator — you return only what goes *inside*. Read extra
|
|
@@ -182,8 +210,10 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
|
|
|
182
210
|
* the menu anchors to it (its own previous DOM sibling) and opens it on click,
|
|
183
211
|
* so a fragment, multiple siblings, or a non-focusable wrapper will misanchor
|
|
184
212
|
* (the menu warns in the console if the trigger has no focusable element). Give
|
|
185
|
-
* the element `aria-haspopup="menu"` plus `aria-expanded={state.open}`. The
|
|
186
|
-
* props (`label`, `hint`, `size`, `status`,
|
|
213
|
+
* the element `aria-haspopup="menu"` plus `aria-expanded={state.open}`. The custom
|
|
214
|
+
* trigger owns its own styling: the field props (`label`, `hint`, `size`, `status`,
|
|
215
|
+
* `placeholder`, `round`) and `className` / `style` apply to the *default* trigger
|
|
216
|
+
* only — put your own on the element you return. */
|
|
187
217
|
renderTrigger?: (state: TriggerState) => React.ReactNode;
|
|
188
218
|
/** Render content in the menu body when the (filtered) option list is empty —
|
|
189
219
|
* a "no results" message, a loading indicator (gated on your own external
|
|
@@ -204,11 +234,13 @@ export type SelectProps = SelectCommonProps & ({
|
|
|
204
234
|
* array value.
|
|
205
235
|
* @defaultValue single */
|
|
206
236
|
selection?: 'single';
|
|
207
|
-
/** Controlled value
|
|
208
|
-
*
|
|
209
|
-
* (reject by not updating).
|
|
237
|
+
/** Controlled value — the `value` string of the selected option. When
|
|
238
|
+
* provided, the consumer owns selection: the field follows this prop and a
|
|
239
|
+
* pick only *requests* a change via `onValueChange` (reject by not updating).
|
|
240
|
+
* Leave undefined for uncontrolled. */
|
|
210
241
|
value?: string;
|
|
211
|
-
/** Initial value for the uncontrolled case
|
|
242
|
+
/** Initial value (an option's `value` string) for the uncontrolled case — the
|
|
243
|
+
* wrapper then owns it. */
|
|
212
244
|
defaultValue?: string;
|
|
213
245
|
/** Fires after the selection changes, with the new value and a
|
|
214
246
|
* `{ value, option }` snapshot. Select has no discrete element state, so
|
|
@@ -218,15 +250,65 @@ export type SelectProps = SelectCommonProps & ({
|
|
|
218
250
|
/** Multi-select: checkboxes on every row, the menu stays open while
|
|
219
251
|
* toggling, the field shows an "N selected" count, and `value` is an array. */
|
|
220
252
|
selection: 'multiple';
|
|
221
|
-
/** Controlled values
|
|
253
|
+
/** Controlled values — the `value` strings of the selected options (see the
|
|
254
|
+
* single-select `value` note). */
|
|
222
255
|
value?: string[];
|
|
223
|
-
/** Initial values for the uncontrolled case. */
|
|
256
|
+
/** Initial values (option `value` strings) for the uncontrolled case. */
|
|
224
257
|
defaultValue?: string[];
|
|
225
258
|
/** Fires after any toggle, with the new value array and a `{ value, option,
|
|
226
259
|
* selected }` snapshot of the row that changed (or `{ all: true }` for the
|
|
227
260
|
* Select-all row). */
|
|
228
261
|
onValueChange?: (value: string[], attrs: SelectChangeAttrs) => void;
|
|
229
262
|
});
|
|
263
|
+
/** Rolled-up selection of a group / submenu subtree: `'none'` / `'all'` of the
|
|
264
|
+
* descendant leaves selected, or `'some'` in between. On `SelectedGroup` /
|
|
265
|
+
* `SelectedSubmenu`, for a section indicator or custom styling. */
|
|
266
|
+
export type SelectionState = 'none' | 'some' | 'all';
|
|
267
|
+
/** A leaf option annotated with its current selection — a `SelectOption` plus
|
|
268
|
+
* `selected`. Returned by {@link optionsWithSelection}. */
|
|
269
|
+
export type SelectedOption = SelectOption & {
|
|
270
|
+
selected: boolean;
|
|
271
|
+
};
|
|
272
|
+
/** A group with its descendants annotated and a rolled-up `selectionState`.
|
|
273
|
+
* Returned by {@link optionsWithSelection}. */
|
|
274
|
+
export interface SelectedGroup extends Omit<SelectGroup, 'options'> {
|
|
275
|
+
options: SelectedItem[];
|
|
276
|
+
selectionState: SelectionState;
|
|
277
|
+
}
|
|
278
|
+
/** A submenu with its descendants annotated and a rolled-up `selectionState`.
|
|
279
|
+
* Returned by {@link optionsWithSelection}. */
|
|
280
|
+
export interface SelectedSubmenu extends Omit<SelectSubmenu, 'submenu'> {
|
|
281
|
+
submenu: SelectedItem[];
|
|
282
|
+
selectionState: SelectionState;
|
|
283
|
+
}
|
|
284
|
+
/** One node of the tree from {@link optionsWithSelection}: a leaf `SelectedOption`
|
|
285
|
+
* (with `selected`), or a `SelectedGroup` / `SelectedSubmenu` (annotated children
|
|
286
|
+
* + rolled-up `selectionState`). Mirrors `SelectItem` minus the bare-string
|
|
287
|
+
* shorthand — strings are normalized to `SelectedOption`. */
|
|
288
|
+
export type SelectedItem = SelectedOption | SelectedGroup | SelectedSubmenu;
|
|
289
|
+
/**
|
|
290
|
+
* Project a `Select` `options` tree onto a set of selected values: returns a mirror
|
|
291
|
+
* of the tree with every leaf marked `selected` and every group / submenu carrying a
|
|
292
|
+
* rolled-up `selectionState` (`'none'` / `'some'` / `'all'` of its descendant leaves).
|
|
293
|
+
* Bare-string options are normalized to `{ value, label }`; structure and order are
|
|
294
|
+
* preserved.
|
|
295
|
+
*
|
|
296
|
+
* A pure function of `(options, values)` — it needs no `Select` instance and reads
|
|
297
|
+
* nothing off the change event, so it behaves identically in controlled and
|
|
298
|
+
* uncontrolled code. Pass the current `value` (a string, an array, or `undefined`)
|
|
299
|
+
* and render a grouped summary, drive section indicators, or diff picks.
|
|
300
|
+
*
|
|
301
|
+
* A value that appears under more than one section marks the leaf in *every* place it
|
|
302
|
+
* occurs (selection is value-keyed), mirroring how the menu itself renders it.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```tsx
|
|
306
|
+
* const tree = optionsWithSelection(options, values)
|
|
307
|
+
* // tree[1] === { label: 'Engineering', selectionState: 'some', options: [
|
|
308
|
+
* // { value: 'eng-fe', label: 'Frontend', selected: false }, … ] }
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
export declare function optionsWithSelection(options: SelectItem[], values: string | string[] | undefined): SelectedItem[];
|
|
230
312
|
/**
|
|
231
313
|
* `<Select>` — a single- or multi-select dropdown, composed from `<Input>` (a
|
|
232
314
|
* 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,
|
|
@@ -18,6 +44,7 @@ const Select = (props) => {
|
|
|
18
44
|
onValueChange,
|
|
19
45
|
placeholder,
|
|
20
46
|
icon,
|
|
47
|
+
leading,
|
|
21
48
|
label,
|
|
22
49
|
hint,
|
|
23
50
|
size,
|
|
@@ -29,6 +56,8 @@ const Select = (props) => {
|
|
|
29
56
|
filter,
|
|
30
57
|
selectAll,
|
|
31
58
|
selectAllLabel = "Select all",
|
|
59
|
+
clearable,
|
|
60
|
+
clearLabel = "Clear",
|
|
32
61
|
renderOption,
|
|
33
62
|
renderIndicator,
|
|
34
63
|
renderTrigger,
|
|
@@ -47,6 +76,7 @@ const Select = (props) => {
|
|
|
47
76
|
const currentRaw = controlled ? value : internal;
|
|
48
77
|
const [open, setOpen] = useState(false);
|
|
49
78
|
const [query, setQuery] = useState("");
|
|
79
|
+
const [activeId, setActiveId] = useState(null);
|
|
50
80
|
const uid = useId();
|
|
51
81
|
const isSubmenu = (it) => typeof it === "object" && Array.isArray(it.submenu);
|
|
52
82
|
const isGroup = (it) => typeof it === "object" && Array.isArray(it.options);
|
|
@@ -62,6 +92,12 @@ const Select = (props) => {
|
|
|
62
92
|
};
|
|
63
93
|
const allLeaves = [];
|
|
64
94
|
collectLeaves(options, void 0, false, allLeaves);
|
|
95
|
+
const seenValues = /* @__PURE__ */ new Set();
|
|
96
|
+
for (const l of allLeaves) {
|
|
97
|
+
if (seenValues.has(l.opt.value))
|
|
98
|
+
console.warn(`[anta] <Select> duplicate option value=${JSON.stringify(l.opt.value)} \u2014 values must be unique.`);
|
|
99
|
+
seenValues.add(l.opt.value);
|
|
100
|
+
}
|
|
65
101
|
const byValue = new Map(allLeaves.map((l) => [l.opt.value, l.opt]));
|
|
66
102
|
const filtering = filter !== void 0 && filter !== false;
|
|
67
103
|
const q = query.trim();
|
|
@@ -98,8 +134,14 @@ const Select = (props) => {
|
|
|
98
134
|
const o = byValue.get(currentRaw);
|
|
99
135
|
display = o ? o.label ?? o.value : "";
|
|
100
136
|
}
|
|
101
|
-
const choose = (o) => {
|
|
137
|
+
const choose = (o, e) => {
|
|
102
138
|
if (multiple) {
|
|
139
|
+
if (selectAll && e?.altKey) {
|
|
140
|
+
const next2 = [o.value];
|
|
141
|
+
if (!controlled) setInternal(next2);
|
|
142
|
+
emit?.(next2, { value: o.value, option: o, selected: true });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
103
145
|
const has = selectedValues.includes(o.value);
|
|
104
146
|
const next = has ? selectedValues.filter((v) => v !== o.value) : [...selectedValues, o.value];
|
|
105
147
|
if (!controlled) setInternal(next);
|
|
@@ -119,12 +161,20 @@ const Select = (props) => {
|
|
|
119
161
|
if (!controlled) setInternal(next);
|
|
120
162
|
emit?.(next, { all: true, selected: !allSelected });
|
|
121
163
|
};
|
|
164
|
+
const clear = () => {
|
|
165
|
+
const next = multiple ? [] : void 0;
|
|
166
|
+
if (!controlled) setInternal(next);
|
|
167
|
+
emit?.(next, { all: true, selected: false });
|
|
168
|
+
};
|
|
122
169
|
const renderOptionRow = (o, disabled2) => {
|
|
123
170
|
const optState = { value: o.value, selected: isSelected(o.value), disabled: disabled2 };
|
|
124
171
|
const custom = renderOption?.(o, optState);
|
|
125
172
|
const customMark = renderIndicator?.(optState);
|
|
126
173
|
const ariaSelectable = !multiple && mark === "none" ? { role: "menuitemradio", "aria-checked": isSelected(o.value) ? "true" : "false" } : void 0;
|
|
127
|
-
|
|
174
|
+
const isolateHint = multiple && selectAll && !disabled2 ? IS_MAC ? "\u2325+Click to select only this" : "Alt+Click to select only this" : void 0;
|
|
175
|
+
const tip = o.tooltip ?? isolateHint;
|
|
176
|
+
const hintOnly = o.tooltip == null && isolateHint != null;
|
|
177
|
+
return /* @__PURE__ */ jsxs(
|
|
128
178
|
MenuItem,
|
|
129
179
|
{
|
|
130
180
|
id: `${uid}-opt-${o.value}`,
|
|
@@ -139,8 +189,11 @@ const Select = (props) => {
|
|
|
139
189
|
selected: isSelected(o.value),
|
|
140
190
|
disabled: disabled2 || void 0,
|
|
141
191
|
"data-menu-open": multiple ? "" : void 0,
|
|
142
|
-
onSelect: () => choose(o),
|
|
143
|
-
children:
|
|
192
|
+
onSelect: (e) => choose(o, e),
|
|
193
|
+
children: [
|
|
194
|
+
custom,
|
|
195
|
+
tip && /* @__PURE__ */ jsx(Tooltip, { ...hintOnly ? { follow: true, delay: 700 } : {}, children: tip })
|
|
196
|
+
]
|
|
144
197
|
},
|
|
145
198
|
o.value
|
|
146
199
|
);
|
|
@@ -211,7 +264,7 @@ const Select = (props) => {
|
|
|
211
264
|
readOnly: true,
|
|
212
265
|
dimActions: true,
|
|
213
266
|
disabled,
|
|
214
|
-
leading: icon ? /* @__PURE__ */ jsx(Icon, { shape: icon }) : void 0,
|
|
267
|
+
leading: leading ?? (icon ? /* @__PURE__ */ jsx(Icon, { shape: icon }) : void 0),
|
|
215
268
|
size,
|
|
216
269
|
status,
|
|
217
270
|
statusIcon,
|
|
@@ -246,8 +299,12 @@ const Select = (props) => {
|
|
|
246
299
|
{
|
|
247
300
|
onStateChange: (_e, { next }) => {
|
|
248
301
|
setOpen(next);
|
|
249
|
-
if (!next)
|
|
302
|
+
if (!next) {
|
|
303
|
+
setQuery("");
|
|
304
|
+
setActiveId(null);
|
|
305
|
+
}
|
|
250
306
|
},
|
|
307
|
+
onactivedescendant: (e) => setActiveId((e.nativeEvent ?? e).detail?.id ?? null),
|
|
251
308
|
children: [
|
|
252
309
|
filtering && // `slot="header"` pins the field in the Menu's fixed header region (above
|
|
253
310
|
// the scrolling options); `data-menu-search` puts the menu in combobox
|
|
@@ -261,6 +318,7 @@ const Select = (props) => {
|
|
|
261
318
|
placeholder: "Filter\u2026",
|
|
262
319
|
"aria-label": "Filter options",
|
|
263
320
|
"aria-autocomplete": "list",
|
|
321
|
+
"aria-activedescendant": open && activeId ? activeId : void 0,
|
|
264
322
|
onInput: (e) => setQuery(e.currentTarget.value)
|
|
265
323
|
}
|
|
266
324
|
) }),
|
|
@@ -279,12 +337,15 @@ const Select = (props) => {
|
|
|
279
337
|
),
|
|
280
338
|
/* @__PURE__ */ jsx(MenuSeparator, {})
|
|
281
339
|
] }),
|
|
282
|
-
visibleLeaves.length === 0 ? renderEmpty?.({ query: q }) : flattening ? renderFlat() : renderTree(options, false)
|
|
340
|
+
visibleLeaves.length === 0 ? renderEmpty?.({ query: q }) : flattening ? renderFlat() : renderTree(options, false),
|
|
341
|
+
clearable && selectedValues.length > 0 && // Pinned in the footer so it never scrolls away in a long / filtered list.
|
|
342
|
+
/* @__PURE__ */ jsx("div", { slot: "footer", className: styles.footer, children: /* @__PURE__ */ jsx(MenuItem, { icon: "x", label: clearLabel, "data-menu-open": "", onSelect: clear }) })
|
|
283
343
|
]
|
|
284
344
|
}
|
|
285
345
|
)
|
|
286
346
|
] });
|
|
287
347
|
};
|
|
288
348
|
export {
|
|
289
|
-
Select
|
|
349
|
+
Select,
|
|
350
|
+
optionsWithSelection
|
|
290
351
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.chevron{margin-inline-end:7px;transition:transform .15s ease}.chevron.open{transform:rotate(180deg)}.filter{padding:4px 4px 0}
|
|
1
|
+
.chevron{margin-inline-end:7px;transition:transform .15s ease}.chevron.open{transform:rotate(180deg)}.filter{padding:4px 4px 0}.footer{padding:var(--menu-padding, 4px);border-top:1px solid var(--border-4)}
|