@antadesign/anta 0.3.4 → 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/components/Calendar.d.ts +3 -2
- package/dist/components/Calendar.js +18 -7
- package/dist/components/InputDate.d.ts +3 -2
- package/dist/components/InputDate.js +36 -91
- 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 +6 -1
- package/dist/components/Menu.js +2 -0
- package/dist/components/Select.d.ts +24 -6
- package/dist/components/Select.js +12 -2
- 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/Tabs.d.ts +6 -0
- package/dist/components/Tabs.js +3 -1
- package/dist/elements/a-button.css +1 -1
- package/dist/elements/a-calendar.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 +7 -1
- package/dist/elements/a-menu-item.css +1 -1
- package/dist/elements/a-menu.js +11 -1
- package/dist/elements/a-tab.css +1 -1
- package/dist/elements/a-tabs.css +1 -1
- package/dist/elements/a-tooltip.js +1 -1
- package/dist/elements/index.d.ts +1 -0
- package/dist/elements/index.js +3 -0
- package/dist/general_types.d.ts +52 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- 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
|
@@ -61,8 +61,9 @@ export interface CalendarProps extends Omit<BaseProps, "children" | "onChange">
|
|
|
61
61
|
* `<Calendar>` — a single-date month grid you pick a day from, **composed from
|
|
62
62
|
* Anta components in light DOM** (nothing is hidden in a shadow root): days and
|
|
63
63
|
* the prev/next chevrons are `<Button>`s, so they inherit the design system's
|
|
64
|
-
* states for free — a selected day is literally a `
|
|
65
|
-
* `selected` state
|
|
64
|
+
* states for free — a selected day is literally a `tertiary` Button in its
|
|
65
|
+
* `selected` state, toned `brand` so the active date reads in the brand color.
|
|
66
|
+
* All date math runs on the Temporal engine
|
|
66
67
|
* (`@antadesign/anta` exports `buildMonth` & friends), and the value is an ISO
|
|
67
68
|
* `YYYY-MM-DD` string.
|
|
68
69
|
*
|
|
@@ -38,11 +38,21 @@ const Calendar = ({
|
|
|
38
38
|
const [cursor, setCursor] = useState(
|
|
39
39
|
() => clampDate(parseISODate(value ?? defaultValue) ?? today, minD, maxD)
|
|
40
40
|
);
|
|
41
|
+
const [lastSyncedValue, setLastSyncedValue] = useState(value);
|
|
42
|
+
let effectiveCursor = cursor;
|
|
43
|
+
if (value !== lastSyncedValue) {
|
|
44
|
+
setLastSyncedValue(value);
|
|
45
|
+
const d = parseISODate(value);
|
|
46
|
+
if (d) {
|
|
47
|
+
effectiveCursor = clampDate(d, minD, maxD);
|
|
48
|
+
setCursor(effectiveCursor);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
41
51
|
const [focusReq, setFocusReq] = useState(null);
|
|
42
52
|
const [jumpOpen, setJumpOpen] = useState(false);
|
|
43
53
|
const headingId = useId();
|
|
44
|
-
const month = buildMonth({ anchor:
|
|
45
|
-
const cursorIso =
|
|
54
|
+
const month = buildMonth({ anchor: effectiveCursor, locale: resolvedLocale, min: minD, max: maxD, selected, today });
|
|
55
|
+
const cursorIso = effectiveCursor.toString();
|
|
46
56
|
const [lastFocusSignal, setLastFocusSignal] = useState(focusSignal);
|
|
47
57
|
if (focusSignal !== lastFocusSignal) {
|
|
48
58
|
setLastFocusSignal(focusSignal);
|
|
@@ -73,24 +83,25 @@ const Calendar = ({
|
|
|
73
83
|
const jumpItems = useMemo(() => {
|
|
74
84
|
const monthItem = (y, i) => {
|
|
75
85
|
const m = i + 1;
|
|
86
|
+
const isCurrent = y === cursor.year && m === cursor.month;
|
|
76
87
|
return /* @__PURE__ */ jsx(
|
|
77
88
|
MenuItem,
|
|
78
89
|
{
|
|
79
90
|
label: monthNames[i],
|
|
80
|
-
|
|
81
|
-
selected: y === cursor.year && m === cursor.month,
|
|
91
|
+
selected: isCurrent,
|
|
82
92
|
disabled: monthOutOfRange(y, m) || void 0,
|
|
83
93
|
"data-menu-open": "",
|
|
84
94
|
onSelect: () => {
|
|
85
95
|
pickMonth(y, m);
|
|
86
96
|
setJumpOpen(false);
|
|
87
|
-
}
|
|
97
|
+
},
|
|
98
|
+
children: isCurrent && /* @__PURE__ */ jsx("span", { className: "jump-dot", "aria-hidden": "true" })
|
|
88
99
|
},
|
|
89
100
|
`${y}-${m}`
|
|
90
101
|
);
|
|
91
102
|
};
|
|
92
103
|
return years.length === 1 ? monthNames.map((_, i) => monthItem(years[0], i)) : years.map((y) => /* @__PURE__ */ jsxs(MenuItem, { submenu: true, label: String(y), selected: y === cursor.year, children: [
|
|
93
|
-
y === cursor.year && /* @__PURE__ */ jsx("span", { className: "
|
|
104
|
+
y === cursor.year && /* @__PURE__ */ jsx("span", { className: "jump-dot", "aria-hidden": "true" }),
|
|
94
105
|
/* @__PURE__ */ jsx(Menu, { children: monthNames.map((_, i) => monthItem(y, i)) })
|
|
95
106
|
] }, y));
|
|
96
107
|
}, [monthNames, cursor.year, cursor.month, minYear, maxYear, min, max, disabled]);
|
|
@@ -192,7 +203,7 @@ const Calendar = ({
|
|
|
192
203
|
`wd${i}`
|
|
193
204
|
)),
|
|
194
205
|
month.weeks.flat().map((d) => {
|
|
195
|
-
const variant = d.today && !d.selected ? { priority: "secondary" } : { priority: "tertiary", selected: d.selected };
|
|
206
|
+
const variant = d.today && !d.selected ? { priority: "secondary" } : { priority: "tertiary", selected: d.selected, tone: d.selected ? "brand" : void 0 };
|
|
196
207
|
return /* @__PURE__ */ jsxs(
|
|
197
208
|
Button,
|
|
198
209
|
{
|
|
@@ -45,9 +45,10 @@ 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 — the calendar affordance.
|
|
48
|
+
/** Leading icon at the start of the field — the calendar affordance. Pass
|
|
49
|
+
* another shape to change it, or `false` to drop it.
|
|
49
50
|
* @defaultValue calendar-days */
|
|
50
|
-
icon?: IconShape;
|
|
51
|
+
icon?: IconShape | false;
|
|
51
52
|
/** Include a time. The value becomes ISO `YYYY-MM-DDTHH:mm`, the field parses a
|
|
52
53
|
* trailing time after a space (`06/15/2026 14:30`, `… 2:30pm`), and the menu
|
|
53
54
|
* shows a time row (hours : minutes, an AM/PM toggle in 12-hour locales, then a
|
|
@@ -17,7 +17,7 @@ import { Menu } from "./Menu";
|
|
|
17
17
|
import { Calendar } from "./Calendar";
|
|
18
18
|
import { Button } from "./Button";
|
|
19
19
|
import { Icon } from "./Icon";
|
|
20
|
-
import {
|
|
20
|
+
import { InputTime } from "./InputTime";
|
|
21
21
|
import styles from "./InputDate.module.css";
|
|
22
22
|
const InputDate = ({
|
|
23
23
|
value,
|
|
@@ -67,47 +67,21 @@ const InputDate = ({
|
|
|
67
67
|
const hasTime = !!current && current.length >= 16 && current[10] === "T";
|
|
68
68
|
const h24 = hasTime ? parseInt(current.slice(11, 13), 10) || 0 : 0;
|
|
69
69
|
const curM = hasTime ? current.slice(14, 16) : "00";
|
|
70
|
-
const curMer = h24 < 12 ? "AM" : "PM";
|
|
71
|
-
const curHourShown = twelveHour ? String((h24 + 11) % 12 + 1).padStart(2, "0") : String(h24).padStart(2, "0");
|
|
72
70
|
const [text, setText] = useState(() => fmt(current));
|
|
73
71
|
const [open, setOpen] = useState(false);
|
|
74
72
|
const [invalid, setInvalid] = useState(false);
|
|
75
73
|
const [focusNonce, setFocusNonce] = useState(0);
|
|
76
|
-
const [hourText, setHourText] = useState(curHourShown);
|
|
77
|
-
const [minuteText, setMinuteText] = useState(curM);
|
|
78
|
-
const meridiem = curMer;
|
|
79
74
|
const [lastValue, setLastValue] = useState(current);
|
|
80
75
|
if (current !== lastValue) {
|
|
81
76
|
setLastValue(current);
|
|
82
77
|
setText(fmt(current));
|
|
83
78
|
setInvalid(false);
|
|
84
79
|
}
|
|
85
|
-
const [lastTime, setLastTime] = useState(`${h24}:${curM}`);
|
|
86
|
-
if (time && `${h24}:${curM}` !== lastTime) {
|
|
87
|
-
setLastTime(`${h24}:${curM}`);
|
|
88
|
-
setHourText(curHourShown);
|
|
89
|
-
setMinuteText(curM);
|
|
90
|
-
}
|
|
91
80
|
const commit = (iso) => {
|
|
92
81
|
if (!controlled) setInternal(iso || void 0);
|
|
93
82
|
onValueChange?.(iso, { value: iso, name });
|
|
94
83
|
};
|
|
95
|
-
const
|
|
96
|
-
const resolveHour = (raw, mer) => {
|
|
97
|
-
let n = parseInt(String(raw || "0"), 10) || 0;
|
|
98
|
-
if (!twelveHour) {
|
|
99
|
-
const h = Math.min(Math.max(n, 0), 23);
|
|
100
|
-
return { h24: h, shown: String(h).padStart(2, "0"), mer };
|
|
101
|
-
}
|
|
102
|
-
if (n === 0) return { h24: 0, shown: "12", mer: "AM" };
|
|
103
|
-
if (n > 23) n = 23;
|
|
104
|
-
if (n > 12) return { h24: n, shown: String(n - 12).padStart(2, "0"), mer: "PM" };
|
|
105
|
-
return { h24: mer === "AM" ? n % 12 : n % 12 + 12, shown: String(n).padStart(2, "0"), mer };
|
|
106
|
-
};
|
|
107
|
-
const withTime = (d) => {
|
|
108
|
-
const { h24: h } = resolveHour(hourText, meridiem);
|
|
109
|
-
return `${d}T${String(h).padStart(2, "0")}:${clampPad(minuteText, 59)}`;
|
|
110
|
-
};
|
|
84
|
+
const withTime = (d) => `${d}T${String(h24).padStart(2, "0")}:${curM}`;
|
|
111
85
|
const apply = (iso) => {
|
|
112
86
|
setInvalid(false);
|
|
113
87
|
if (iso === (current ?? "")) setText(fmt(iso));
|
|
@@ -119,24 +93,25 @@ const InputDate = ({
|
|
|
119
93
|
setInvalid(false);
|
|
120
94
|
if (current) commit("");
|
|
121
95
|
else setText("");
|
|
122
|
-
return;
|
|
96
|
+
return true;
|
|
123
97
|
}
|
|
124
98
|
const parsed = time ? parseDateTimeInput(t, resolvedLocale, { min: minD, max: maxD }) : parseDateInput(t, resolvedLocale, { min: minD, max: maxD });
|
|
125
99
|
if (!parsed) {
|
|
126
100
|
setInvalid(true);
|
|
127
|
-
return;
|
|
101
|
+
return false;
|
|
128
102
|
}
|
|
129
103
|
const iso = time ? parsed.toString({ smallestUnit: "minute" }) : parsed.toString();
|
|
130
104
|
apply(iso);
|
|
105
|
+
return true;
|
|
131
106
|
};
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
107
|
+
const parsedPreview = useMemo(() => {
|
|
108
|
+
const t = text.trim();
|
|
109
|
+
if (!t) return dateISO;
|
|
110
|
+
const parsed = time ? parseDateTimeInput(t, resolvedLocale, { min: minD, max: maxD }) : parseDateInput(t, resolvedLocale, { min: minD, max: maxD });
|
|
111
|
+
return parsed ? parsed.toString().slice(0, 10) : null;
|
|
112
|
+
}, [text, time, resolvedLocale, dateISO, min, max]);
|
|
113
|
+
const [previewISO, setPreviewISO] = useState(dateISO);
|
|
114
|
+
if (parsedPreview !== null && parsedPreview !== previewISO) setPreviewISO(parsedPreview);
|
|
140
115
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
141
116
|
/* @__PURE__ */ jsx(
|
|
142
117
|
Input,
|
|
@@ -148,7 +123,7 @@ const InputDate = ({
|
|
|
148
123
|
round,
|
|
149
124
|
disabled,
|
|
150
125
|
clearable,
|
|
151
|
-
leading: /* @__PURE__ */ jsx(Icon, { shape: icon ?? "calendar-days" }),
|
|
126
|
+
leading: icon === false ? void 0 : /* @__PURE__ */ jsx(Icon, { shape: icon ?? "calendar-days" }),
|
|
152
127
|
value: text,
|
|
153
128
|
placeholder: pattern,
|
|
154
129
|
inputMode: time ? "text" : "numeric",
|
|
@@ -166,6 +141,10 @@ const InputDate = ({
|
|
|
166
141
|
if (!open) e.currentTarget.click();
|
|
167
142
|
setFocusNonce((n) => n + 1);
|
|
168
143
|
}
|
|
144
|
+
if (e.key === "Enter" && !disabled) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
if (resolve(e.currentTarget.value)) setOpen(false);
|
|
147
|
+
}
|
|
169
148
|
},
|
|
170
149
|
className: cn(styles.dateField, className),
|
|
171
150
|
style,
|
|
@@ -177,13 +156,14 @@ const InputDate = ({
|
|
|
177
156
|
{
|
|
178
157
|
open,
|
|
179
158
|
placement: "bottom-start",
|
|
159
|
+
autoWidth: true,
|
|
180
160
|
className: styles.calendarMenu,
|
|
181
161
|
onStateChange: (_e, { next }) => setOpen(next),
|
|
182
162
|
children: /* @__PURE__ */ jsxs("div", { "data-menu-open": "", children: [
|
|
183
163
|
/* @__PURE__ */ jsx(
|
|
184
164
|
Calendar,
|
|
185
165
|
{
|
|
186
|
-
value:
|
|
166
|
+
value: previewISO,
|
|
187
167
|
min,
|
|
188
168
|
max,
|
|
189
169
|
locale,
|
|
@@ -199,58 +179,23 @@ const InputDate = ({
|
|
|
199
179
|
}
|
|
200
180
|
),
|
|
201
181
|
time && /* @__PURE__ */ jsxs("div", { className: styles.timeRow, children: [
|
|
202
|
-
/* @__PURE__ */
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
182
|
+
/* @__PURE__ */ jsx(
|
|
183
|
+
InputTime,
|
|
184
|
+
{
|
|
185
|
+
size,
|
|
186
|
+
locale,
|
|
187
|
+
hour12,
|
|
188
|
+
disabled,
|
|
189
|
+
"aria-label": "Time",
|
|
190
|
+
value: hasTime ? `${String(h24).padStart(2, "0")}:${curM}` : "",
|
|
191
|
+
onValueChange: (e, { value: t }) => {
|
|
192
|
+
const ev = "nativeEvent" in e ? e.nativeEvent : e;
|
|
193
|
+
if (ev?.type !== "change") return;
|
|
194
|
+
const base = dateISO || Temporal.Now.plainDateISO().toString();
|
|
195
|
+
commit(t ? `${base}T${t}` : base);
|
|
251
196
|
}
|
|
252
|
-
|
|
253
|
-
|
|
197
|
+
}
|
|
198
|
+
),
|
|
254
199
|
/* @__PURE__ */ jsx(
|
|
255
200
|
Button,
|
|
256
201
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.calendarMenu{--menu-padding: 8px}.timeRow{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:6px;padding-top:8px;border-top:1px solid var(--border-3)}.
|
|
1
|
+
.calendarMenu{--menu-padding: 8px}.timeRow{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:6px;padding-top:8px;border-top:1px solid var(--border-3)}.dateField::part(input){text-overflow:ellipsis}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { BaseProps, DOMEventHandlers } from '../general_types';
|
|
2
|
+
import type { IconShape } from '../elements/a-icon.shapes';
|
|
3
|
+
/** Convenience snapshot passed as the 2nd argument to `onValueChange`. */
|
|
4
|
+
export interface InputTimeChangeAttrs {
|
|
5
|
+
/** Current value — 24-hour `"HH:mm"`, `''` when incomplete. */
|
|
6
|
+
value: string;
|
|
7
|
+
/** The field's `name`, for keyed updates: `s => ({ ...s, [name]: value })`. */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** `true` when the value is empty / incomplete. */
|
|
10
|
+
empty: boolean;
|
|
11
|
+
/** Whether the field currently passes validation. `undefined` where
|
|
12
|
+
* `ElementInternals` is unsupported. */
|
|
13
|
+
valid?: boolean;
|
|
14
|
+
/** Current validation message (`''` when valid). */
|
|
15
|
+
validationMessage: string;
|
|
16
|
+
}
|
|
17
|
+
export interface InputTimeProps extends BaseProps, DOMEventHandlers {
|
|
18
|
+
/** Extra content rendered under the field, above the hint (a no-box child like
|
|
19
|
+
* a `<Tooltip>` takes no space and just anchors to the field). */
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
/** Field label, shown above the control and used as the segment group's
|
|
22
|
+
* accessible name. */
|
|
23
|
+
label?: React.ReactNode;
|
|
24
|
+
/** Message below the field. `status` recolors it and prefixes a glyph. */
|
|
25
|
+
hint?: React.ReactNode;
|
|
26
|
+
/** Validation / feedback tone. Only `critical` marks the field invalid; the
|
|
27
|
+
* others are advisory. Omit (or `neutral`) for a plain field. */
|
|
28
|
+
status?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical';
|
|
29
|
+
/** Glyph before the `hint` when `status` is set (per-status default; pass a
|
|
30
|
+
* shape to override, or `false` to drop it). */
|
|
31
|
+
statusIcon?: IconShape | (string & {}) | false;
|
|
32
|
+
/** Custom accent colour — any literal CSS colour tints the resting + hover
|
|
33
|
+
* border (focus ring stays `--focus-ring`); `status` overrides for validation. */
|
|
34
|
+
tone?: string;
|
|
35
|
+
/** Size variant. small=24px, medium=28px, large=32px tall.
|
|
36
|
+
* @defaultValue medium */
|
|
37
|
+
size?: 'small' | 'medium' | 'large';
|
|
38
|
+
/** Controlled value — 24-hour `"HH:mm"`. Pair with `onValueChange`. */
|
|
39
|
+
value?: string;
|
|
40
|
+
/** Initial value for the uncontrolled case (24-hour `"HH:mm"`). */
|
|
41
|
+
defaultValue?: string;
|
|
42
|
+
/** BCP-47 locale driving the clock (12h vs 24h), segment order, separator, and
|
|
43
|
+
* the AM/PM text.
|
|
44
|
+
* @defaultValue navigator.language */
|
|
45
|
+
locale?: string;
|
|
46
|
+
/** Force the clock: `true` = 12-hour (AM/PM), `false` = 24-hour. Omit to follow
|
|
47
|
+
* the locale. */
|
|
48
|
+
hour12?: boolean;
|
|
49
|
+
/** Earliest allowed time, 24-hour `"HH:mm"`. A complete value below it is
|
|
50
|
+
* clamped up (on step / blur) and flagged `rangeUnderflow` for form validity. */
|
|
51
|
+
min?: string;
|
|
52
|
+
/** Latest allowed time, 24-hour `"HH:mm"`. A complete value above it is clamped
|
|
53
|
+
* down (on step / blur) and flagged `rangeOverflow`. */
|
|
54
|
+
max?: string;
|
|
55
|
+
/** Form field name — the 24-hour value submits under this key. */
|
|
56
|
+
name?: string;
|
|
57
|
+
/** Disable the field. */
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
/** Mark the field required (drives native validity). */
|
|
60
|
+
required?: boolean;
|
|
61
|
+
/** Leading icon at the start of the field — the clock affordance. Pass another
|
|
62
|
+
* shape to change it, or `false` to drop it.
|
|
63
|
+
* @defaultValue clock */
|
|
64
|
+
icon?: IconShape | false;
|
|
65
|
+
/** Show a clear button once the field has a value. */
|
|
66
|
+
clearable?: boolean;
|
|
67
|
+
/** Dim the trailing adornments at rest; they brighten on hover / focus. */
|
|
68
|
+
dimActions?: boolean;
|
|
69
|
+
/** Content pinned to the end of the field (after the clear button). */
|
|
70
|
+
trailing?: React.ReactNode;
|
|
71
|
+
/** Fully-round the field, or a custom radius (`number` px / CSS length). */
|
|
72
|
+
round?: boolean | number | string;
|
|
73
|
+
/** Fires on every edit (`input`), with the native event + an `attrs` snapshot
|
|
74
|
+
* (`value`, `name`, `empty`, `valid`, `validationMessage`). Also fires on
|
|
75
|
+
* `change` (blur) and on clear. */
|
|
76
|
+
onValueChange?: (event: any, attrs: InputTimeChangeAttrs) => void;
|
|
77
|
+
/** Fires after the built-in clear button has cleared the field. */
|
|
78
|
+
onClearInput?: (e: CustomEvent) => void;
|
|
79
|
+
/** Fires when the field gains focus. */
|
|
80
|
+
onFocus?: (e: any) => void;
|
|
81
|
+
/** Fires when the field loses focus. */
|
|
82
|
+
onBlur?: (e: any) => void;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `<InputTime>` — a segmented wall-clock time field. One boxed input (matching
|
|
86
|
+
* the other inputs) with separate hour / minute / (12-hour) AM-PM sections that
|
|
87
|
+
* behave as one control: each is a `role="spinbutton"` — ↑/↓ steps with wrap,
|
|
88
|
+
* ←/→ moves between them, typing digits auto-advances, and AM/PM toggles with
|
|
89
|
+
* ↑/↓ or `a`/`p`. Renders an `<a-input-time>` web component that owns the
|
|
90
|
+
* segments, keyboard, and form value (via `ElementInternals`).
|
|
91
|
+
*
|
|
92
|
+
* The clock and layout follow the locale (`en-US` → 12-hour, most others 24),
|
|
93
|
+
* overridable with `hour12`. The value is a 24-hour `"HH:mm"` string; controlled
|
|
94
|
+
* (`value` + `onValueChange`) or uncontrolled (`defaultValue`), submitting under
|
|
95
|
+
* `name`.
|
|
96
|
+
*
|
|
97
|
+
* Requires `@antadesign/anta/elements` (client-side only). Reads
|
|
98
|
+
* `navigator.language` at render, so render client-side (not SSR-safe).
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```tsx
|
|
102
|
+
* <InputTime label="Start" defaultValue="09:30" onValueChange={(_, { value }) => save(value)} />
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare const InputTime: ({ label, hint, status, statusIcon, tone, size, round, value, defaultValue, locale, hour12, min, max, name, disabled, required, icon, clearable, dimActions, trailing, onValueChange, onClearInput, children, className, style, ...rest }: InputTimeProps) => any;
|
|
@@ -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,
|
|
@@ -83,4 +88,4 @@ export interface MenuProps extends BaseProps {
|
|
|
83
88
|
* </Menu>
|
|
84
89
|
* ```
|
|
85
90
|
*/
|
|
86
|
-
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);
|
|
@@ -97,8 +97,10 @@ export interface TriggerState {
|
|
|
97
97
|
}
|
|
98
98
|
/** Snapshot passed as the 2nd argument to `onValueChange` — describes *what*
|
|
99
99
|
* changed, alongside the new full value in the 1st argument. A discriminated
|
|
100
|
-
* union: a row toggle carries `value` + `option`; the "Select all"
|
|
101
|
-
*
|
|
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. */
|
|
102
104
|
export type SelectChangeAttrs = {
|
|
103
105
|
/** The option value that changed — the chosen value (single) or the toggled
|
|
104
106
|
* row (multiple). */
|
|
@@ -108,9 +110,11 @@ export type SelectChangeAttrs = {
|
|
|
108
110
|
/** Multiple only: whether the change turned selection **on** (true) or off. */
|
|
109
111
|
selected?: boolean;
|
|
110
112
|
} | {
|
|
111
|
-
/** 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). */
|
|
112
115
|
all: true;
|
|
113
|
-
/** Whether
|
|
116
|
+
/** Whether the bulk change turned everything **on** (true) or cleared it
|
|
117
|
+
* (false — always false for a "Clear"). */
|
|
114
118
|
selected: boolean;
|
|
115
119
|
};
|
|
116
120
|
/** Props shared by both selection modes, intersected into `SelectProps`. Exported
|
|
@@ -139,6 +143,11 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
|
|
|
139
143
|
* `leading` slot). With a custom `renderTrigger`, it's passed through as
|
|
140
144
|
* `state.icon` instead — the consumer places it. */
|
|
141
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;
|
|
142
151
|
/** Field label, above the trigger (Input's `label`). */
|
|
143
152
|
label?: string;
|
|
144
153
|
/** Helper text under the field (Input's `hint`). */
|
|
@@ -174,6 +183,13 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
|
|
|
174
183
|
/** Label for the `selectAll` row.
|
|
175
184
|
* @defaultValue Select all */
|
|
176
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;
|
|
177
193
|
/** Render the **content** of each option row yourself, replacing the built-in
|
|
178
194
|
* `label`/`hint`/`icon` layout. Select still supplies the row box, click, ARIA,
|
|
179
195
|
* and the selection indicator — you return only what goes *inside*. Read extra
|
|
@@ -194,8 +210,10 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
|
|
|
194
210
|
* the menu anchors to it (its own previous DOM sibling) and opens it on click,
|
|
195
211
|
* so a fragment, multiple siblings, or a non-focusable wrapper will misanchor
|
|
196
212
|
* (the menu warns in the console if the trigger has no focusable element). Give
|
|
197
|
-
* the element `aria-haspopup="menu"` plus `aria-expanded={state.open}`. The
|
|
198
|
-
* 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. */
|
|
199
217
|
renderTrigger?: (state: TriggerState) => React.ReactNode;
|
|
200
218
|
/** Render content in the menu body when the (filtered) option list is empty —
|
|
201
219
|
* a "no results" message, a loading indicator (gated on your own external
|