@tensor_1/react 2.31.0 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +350 -0
- package/dist/attachment-labels.d.ts +16 -0
- package/dist/attachment-labels.js +9 -0
- package/dist/calendar-model.d.ts +141 -1
- package/dist/calendar-model.js +200 -11
- package/dist/chart-preset.cjs +127 -7
- package/dist/chart-preset.d.cts +2 -2
- package/dist/chart-preset.d.ts +1 -0
- package/dist/chart-preset.js +6 -1
- package/dist/chart-theme-Cl0IrkKQ.d.cts +112 -0
- package/dist/chart-theme.d.ts +51 -0
- package/dist/chart-theme.js +99 -0
- package/dist/chart.cjs +127 -7
- package/dist/chart.d.cts +2 -2
- package/dist/chart.d.ts +1 -0
- package/dist/chart.js +6 -2
- package/dist/components/Alert.d.ts +17 -1
- package/dist/components/Alert.js +9 -4
- package/dist/components/AlertDismiss.d.ts +14 -0
- package/dist/components/AlertDismiss.js +11 -0
- package/dist/components/Attachment.d.ts +15 -12
- package/dist/components/Attachment.js +17 -17
- package/dist/components/AttachmentParts.d.ts +24 -0
- package/dist/components/AttachmentParts.js +20 -0
- package/dist/components/Chart.d.ts +1 -0
- package/dist/components/Chart.js +6 -1
- package/dist/components/ChartCore.d.ts +6 -1
- package/dist/components/ChartCore.js +23 -5
- package/dist/components/Chip.d.ts +17 -2
- package/dist/components/Chip.js +35 -11
- package/dist/components/ChipRemove.d.ts +17 -0
- package/dist/components/ChipRemove.js +13 -0
- package/dist/components/CollapsibleParts.d.ts +29 -0
- package/dist/components/CollapsibleParts.js +20 -0
- package/dist/components/CollapsibleSection.d.ts +30 -3
- package/dist/components/CollapsibleSection.js +7 -11
- package/dist/components/CollapsibleSectionState.d.ts +1 -1
- package/dist/components/CollapsibleSectionState.js +54 -27
- package/dist/components/DashboardGrid.d.ts +60 -0
- package/dist/components/DashboardGrid.js +40 -0
- package/dist/components/DateRangePicker.d.ts +7 -1
- package/dist/components/DateRangePicker.js +5 -1
- package/dist/components/DescriptionList.d.ts +12 -1
- package/dist/components/DescriptionList.js +5 -5
- package/dist/components/Dialogs.d.ts +24 -3
- package/dist/components/Dialogs.js +74 -16
- package/dist/components/FileUpload.d.ts +10 -2
- package/dist/components/FileUpload.js +10 -4
- package/dist/components/FilterEditor.d.ts +4 -0
- package/dist/components/FilterEditor.js +13 -2
- package/dist/components/Heartbeat.d.ts +31 -0
- package/dist/components/Heartbeat.js +28 -0
- package/dist/components/Menu.d.ts +53 -5
- package/dist/components/Menu.js +104 -4
- package/dist/components/Meter.d.ts +7 -3
- package/dist/components/Meter.js +2 -1
- package/dist/components/Popover.d.ts +21 -2
- package/dist/components/Popover.js +94 -17
- package/dist/components/Prose.d.ts +6 -2
- package/dist/components/Prose.js +1 -1
- package/dist/components/RuleBuilder.d.ts +52 -4
- package/dist/components/RuleBuilder.js +109 -24
- package/dist/components/SchedulingCalendar.d.ts +142 -11
- package/dist/components/SchedulingCalendar.js +234 -57
- package/dist/components/SchedulingTimeline.d.ts +1 -0
- package/dist/components/Segmented.d.ts +17 -3
- package/dist/components/Segmented.js +59 -5
- package/dist/components/Select.d.ts +66 -2
- package/dist/components/Select.js +800 -10
- package/dist/components/Spinner.d.ts +2 -1
- package/dist/components/Spinner.js +1 -1
- package/dist/components/StatusPill.d.ts +8 -2
- package/dist/components/StatusPill.js +2 -2
- package/dist/components/Steps.d.ts +28 -4
- package/dist/components/Steps.js +46 -16
- package/dist/components/TableControls.js +4 -3
- package/dist/components/Tabs.d.ts +7 -2
- package/dist/components/Tabs.js +14 -1
- package/dist/components/TextDiff.d.ts +49 -0
- package/dist/components/TextDiff.js +124 -0
- package/dist/{emoji-image-6mPZ15Zy.d.cts → emoji-image-DjCKpdwE.d.cts} +1 -1
- package/dist/emoji.cjs +169 -27
- package/dist/emoji.d.cts +3 -3
- package/dist/file-model.d.ts +22 -3
- package/dist/file-model.js +6 -4
- package/dist/filter-model.d.ts +26 -2
- package/dist/filter-model.js +13 -1
- package/dist/hooks/useAnchoredPosition.d.ts +24 -1
- package/dist/hooks/useAnchoredPosition.js +58 -8
- package/dist/hooks/useVirtualRows.d.ts +17 -2
- package/dist/hooks/useVirtualRows.js +6 -5
- package/dist/icons/Icon.d.ts +14 -14
- package/dist/index.cjs +4179 -1678
- package/dist/index.d.cts +1706 -660
- package/dist/index.d.ts +16 -9
- package/dist/index.js +53 -4
- package/dist/labels-context.d.ts +28 -0
- package/dist/{overlay-position-Cjv5E7mn.d.cts → overlay-position-2Afke51l.d.cts} +20 -0
- package/dist/overlay-position.d.ts +20 -0
- package/dist/overlay-position.js +17 -2
- package/dist/rich-text.cjs +169 -27
- package/dist/rich-text.d.cts +3 -3
- package/dist/rule-builder-model.d.ts +34 -2
- package/dist/rule-builder-model.js +48 -12
- package/dist/scheduling-calendar-model.d.ts +66 -0
- package/dist/scheduling-calendar-model.js +82 -0
- package/dist/select-model.d.ts +80 -0
- package/dist/select-model.js +109 -0
- package/dist/table-engine.d.ts +7 -0
- package/dist/table-engine.js +2 -1
- package/dist/text-diff-model.d.ts +71 -0
- package/dist/text-diff-model.js +261 -0
- package/package.json +1 -1
- package/dist/ChartCore-DzWe10bN.d.cts +0 -59
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { addCalendarDays, addCalendarMonths, buildMonthGrid, startOfMonth,
|
|
4
|
+
import { addCalendarDays, addCalendarMonths, buildMonthGrid, cachedDateTimeFormat, startOfMonth, zonedDateKey, zonedTimeOfDay } from "../calendar-model.js";
|
|
5
|
+
import { itemDaySpan, layoutMonthSpans } from "../scheduling-calendar-model.js";
|
|
5
6
|
import { cx } from "../cx.js";
|
|
6
7
|
import { useUixLabels } from "../labels-context.js";
|
|
7
8
|
import { StatusPill } from "./StatusPill.js";
|
|
@@ -10,6 +11,10 @@ const DEFAULT_SCHEDULING_CALENDAR_LABELS = {
|
|
|
10
11
|
region: "Scheduling calendar in {timeZone}",
|
|
11
12
|
previous: "Previous",
|
|
12
13
|
next: "Next",
|
|
14
|
+
previousMonth: "Previous month",
|
|
15
|
+
nextMonth: "Next month",
|
|
16
|
+
previousWeek: "Previous week",
|
|
17
|
+
nextWeek: "Next week",
|
|
13
18
|
viewGroup: "Calendar view",
|
|
14
19
|
viewMonth: "Month",
|
|
15
20
|
viewWeek: "Week",
|
|
@@ -20,11 +25,16 @@ const DEFAULT_SCHEDULING_CALENDAR_LABELS = {
|
|
|
20
25
|
agendaEmpty: "No scheduled entries match the current filters.",
|
|
21
26
|
grid: "{view} schedule",
|
|
22
27
|
entry: "{title}, {state}, {start} to {end}",
|
|
28
|
+
overlay: "{title}, {start} to {end}",
|
|
23
29
|
legend: "Schedule state legend",
|
|
24
30
|
moreEntries: "+{count} more",
|
|
25
31
|
moreEntriesLabel: "{count} more entries on {date}",
|
|
26
32
|
fewerEntries: "Show fewer",
|
|
27
|
-
|
|
33
|
+
moreSpans: "+{count}",
|
|
34
|
+
moreSpansLabel: "{count} more spanning this week, from {date}",
|
|
35
|
+
dayCount: "{count} entries",
|
|
36
|
+
states: { scheduled: "Scheduled", conflicted: "Conflicted", "in-progress": "In progress", "blackout-violation": "Blackout violation" },
|
|
37
|
+
statuses: { tentative: "Tentative", committed: "Scheduled", live: "In progress", done: "Done", dead: "Cancelled" }
|
|
28
38
|
};
|
|
29
39
|
const stateTone = (state) => state === "scheduled" ? "info" : state === "in-progress" ? "success" : "danger";
|
|
30
40
|
const utcDate = (date) => /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
@@ -33,8 +43,24 @@ const INTL_PARTS = {
|
|
|
33
43
|
weekday: { weekday: "short", timeZone: "UTC" },
|
|
34
44
|
month: { month: "long", year: "numeric", timeZone: "UTC" }
|
|
35
45
|
};
|
|
36
|
-
const intlDate = (date, part, locale) => new Intl.DateTimeFormat(locale, INTL_PARTS[part]).format(utcDate(date));
|
|
37
46
|
const EMPTY_OVERLAYS = [];
|
|
47
|
+
const EMPTY_ENTRIES = [];
|
|
48
|
+
const DEFAULT_LANE_CAP = 2;
|
|
49
|
+
const DEFAULT_CHIP_ROWS = 3;
|
|
50
|
+
function coveredDays(start, end, timeZone) {
|
|
51
|
+
try {
|
|
52
|
+
return itemDaySpan(start, end, timeZone);
|
|
53
|
+
} catch {
|
|
54
|
+
return itemDaySpan(start, start, timeZone);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function Marker({ marker }) {
|
|
58
|
+
const emphasis = marker.emphasis ?? "neutral";
|
|
59
|
+
return /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__marker", "data-emphasis": emphasis, "data-glyph": marker.icon == null ? emphasis : void 0, children: [
|
|
60
|
+
marker.icon != null && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__marker-icon", "aria-hidden": "true", children: marker.icon }),
|
|
61
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: marker.label })
|
|
62
|
+
] });
|
|
63
|
+
}
|
|
38
64
|
function SchedulingCalendar({
|
|
39
65
|
entries,
|
|
40
66
|
anchorDate,
|
|
@@ -44,6 +70,7 @@ function SchedulingCalendar({
|
|
|
44
70
|
onAnchorDateChange,
|
|
45
71
|
onSelectEntry,
|
|
46
72
|
overlays = EMPTY_OVERLAYS,
|
|
73
|
+
onSelectOverlay,
|
|
47
74
|
filter,
|
|
48
75
|
renderEntry,
|
|
49
76
|
locale,
|
|
@@ -53,6 +80,17 @@ function SchedulingCalendar({
|
|
|
53
80
|
maxEntriesPerDay,
|
|
54
81
|
onShowMore,
|
|
55
82
|
renderDayBadge,
|
|
83
|
+
days: dayInfo,
|
|
84
|
+
dayEntries,
|
|
85
|
+
spanLayout,
|
|
86
|
+
windowLaneCap,
|
|
87
|
+
spanLaneCap,
|
|
88
|
+
legend,
|
|
89
|
+
legendCaption,
|
|
90
|
+
showHeader = true,
|
|
91
|
+
notice,
|
|
92
|
+
emptyNote,
|
|
93
|
+
onSelectDate,
|
|
56
94
|
loading,
|
|
57
95
|
error,
|
|
58
96
|
onRetry,
|
|
@@ -60,11 +98,12 @@ function SchedulingCalendar({
|
|
|
60
98
|
labels: labelOverrides
|
|
61
99
|
}) {
|
|
62
100
|
const uixLabels = useUixLabels();
|
|
101
|
+
const overrides = { ...uixLabels.schedulingCalendar, ...labelOverrides };
|
|
63
102
|
const labels = {
|
|
64
103
|
...DEFAULT_SCHEDULING_CALENDAR_LABELS,
|
|
65
|
-
...
|
|
66
|
-
...labelOverrides,
|
|
67
|
-
|
|
104
|
+
...overrides,
|
|
105
|
+
states: { ...DEFAULT_SCHEDULING_CALENDAR_LABELS.states, ...uixLabels.schedulingCalendar?.states, ...labelOverrides?.states },
|
|
106
|
+
statuses: { ...DEFAULT_SCHEDULING_CALENDAR_LABELS.statuses, ...uixLabels.schedulingCalendar?.statuses, ...labelOverrides?.statuses }
|
|
68
107
|
};
|
|
69
108
|
const stateLabel = labels.states;
|
|
70
109
|
const viewLabel = { month: labels.viewMonth, week: labels.viewWeek, agenda: labels.viewAgenda };
|
|
@@ -72,16 +111,66 @@ function SchedulingCalendar({
|
|
|
72
111
|
const view = controlledView ?? internalView;
|
|
73
112
|
const [activeDate, setActiveDate] = useState(anchorDate);
|
|
74
113
|
const [expandedDates, setExpandedDates] = useState(() => /* @__PURE__ */ new Set());
|
|
75
|
-
const dateText = (date, part) => formatDate?.(date, part) ??
|
|
114
|
+
const dateText = (date, part) => formatDate?.(date, part) ?? cachedDateTimeFormat(locale, INTL_PARTS[part]).format(utcDate(date));
|
|
115
|
+
const formatInstant = (instant) => formatInstantProp?.(instant) ?? `${dateText(zonedDateKey(instant, timeZone), "day")} ${zonedTimeOfDay(instant, timeZone)}`;
|
|
76
116
|
const gridRef = useRef(null);
|
|
117
|
+
const picksGiven = dayEntries !== void 0;
|
|
118
|
+
const controlled = picksGiven || dayInfo !== void 0;
|
|
119
|
+
const fixed = controlled || maxEntriesPerDay !== void 0 && onShowMore !== void 0;
|
|
120
|
+
const windowCap = Math.max(0, windowLaneCap ?? (fixed ? DEFAULT_LANE_CAP : Infinity));
|
|
121
|
+
const spanCap = Math.max(0, spanLaneCap ?? (fixed ? DEFAULT_LANE_CAP : Infinity));
|
|
77
122
|
const visibleEntries = useMemo(() => entries.filter((entry) => filter?.(entry) ?? true), [entries, filter]);
|
|
78
|
-
const entrySpans = useMemo(() => new Map(visibleEntries.map((entry) => [entry.id, zonedDateSpan(entry.start, entry.end, timeZone)])), [visibleEntries, timeZone]);
|
|
79
|
-
const overlaySpans = useMemo(() => new Map(overlays.map((overlay) => [overlay.id, { start: overlay.start.slice(0, 10), end: overlay.end.slice(0, 10) }])), [overlays]);
|
|
80
123
|
const monthDays = useMemo(() => buildMonthGrid(startOfMonth(anchorDate), weekStartsOn), [anchorDate, weekStartsOn]);
|
|
81
124
|
const weekday = (utcDate(anchorDate).getUTCDay() - weekStartsOn + 7) % 7;
|
|
82
125
|
const weekStart = addCalendarDays(anchorDate, -weekday);
|
|
83
126
|
const weekDays = useMemo(() => Array.from({ length: 7 }, (_, index) => addCalendarDays(weekStart, index)), [weekStart]);
|
|
84
127
|
const days = useMemo(() => view === "month" ? monthDays.map((day) => day.date) : weekDays, [view, monthDays, weekDays]);
|
|
128
|
+
const { singles, multiDay, coverage } = useMemo(() => {
|
|
129
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
130
|
+
const spans = [];
|
|
131
|
+
const every = [];
|
|
132
|
+
for (const entry of visibleEntries) {
|
|
133
|
+
const covered = coveredDays(entry.start, entry.end, timeZone);
|
|
134
|
+
every.push({ entry, ...covered });
|
|
135
|
+
if (addCalendarDays(covered.start, 1) === covered.end) {
|
|
136
|
+
const list = byDay.get(covered.start);
|
|
137
|
+
if (list) list.push(entry);
|
|
138
|
+
else byDay.set(covered.start, [entry]);
|
|
139
|
+
} else spans.push({ entry, ...covered });
|
|
140
|
+
}
|
|
141
|
+
return { singles: byDay, multiDay: spans, coverage: every };
|
|
142
|
+
}, [visibleEntries, timeZone]);
|
|
143
|
+
const entryById = useMemo(() => new Map([...multiDay].reverse().map(({ entry }) => [entry.id, entry])), [multiDay]);
|
|
144
|
+
const overlayById = useMemo(() => new Map([...overlays].reverse().map((overlay) => [overlay.id, overlay])), [overlays]);
|
|
145
|
+
const layout = useMemo(() => {
|
|
146
|
+
if (spanLayout) return spanLayout;
|
|
147
|
+
if (view === "agenda") return { placed: [], hiddenByRow: {}, firstHiddenDayByRow: {} };
|
|
148
|
+
const inputs = [];
|
|
149
|
+
const windowIds = /* @__PURE__ */ new Set();
|
|
150
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
151
|
+
for (const overlay of overlays) {
|
|
152
|
+
if (windowIds.has(overlay.id) || !(new Date(overlay.end).getTime() >= new Date(overlay.start).getTime())) continue;
|
|
153
|
+
windowIds.add(overlay.id);
|
|
154
|
+
inputs.push({ id: overlay.id, start: overlay.start, end: overlay.end, group: "window" });
|
|
155
|
+
}
|
|
156
|
+
for (const { entry } of multiDay) {
|
|
157
|
+
if (itemIds.has(entry.id)) continue;
|
|
158
|
+
itemIds.add(entry.id);
|
|
159
|
+
inputs.push({ id: entry.id, start: entry.start, end: entry.end, group: "item" });
|
|
160
|
+
}
|
|
161
|
+
return layoutMonthSpans(inputs, days, { timeZone, laneCap: { window: windowCap, item: spanCap } });
|
|
162
|
+
}, [spanLayout, view, overlays, multiDay, days, timeZone, windowCap, spanCap]);
|
|
163
|
+
const placedByRow = useMemo(() => {
|
|
164
|
+
const rows = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const placed of layout.placed) {
|
|
166
|
+
const row = rows.get(placed.weekRow);
|
|
167
|
+
if (row) row.push(placed);
|
|
168
|
+
else rows.set(placed.weekRow, [placed]);
|
|
169
|
+
}
|
|
170
|
+
return rows;
|
|
171
|
+
}, [layout]);
|
|
172
|
+
const lanesUsed = (row, group) => (placedByRow.get(row) ?? []).reduce((max, placed) => placed.group === group ? Math.max(max, placed.lane + 1) : max, 0);
|
|
173
|
+
const lanesUsedAnywhere = (group) => layout.placed.reduce((max, placed) => placed.group === group ? Math.max(max, placed.lane + 1) : max, 0);
|
|
85
174
|
useEffect(() => {
|
|
86
175
|
if (view !== "agenda" && !days.includes(activeDate)) setActiveDate(days[0]);
|
|
87
176
|
}, [activeDate, days, view]);
|
|
@@ -90,6 +179,10 @@ function SchedulingCalendar({
|
|
|
90
179
|
onViewChange?.(next);
|
|
91
180
|
};
|
|
92
181
|
const movePeriod = (direction) => onAnchorDateChange?.(view === "month" ? addCalendarMonths(anchorDate, direction) : addCalendarDays(anchorDate, direction * 7));
|
|
182
|
+
const periodLabel = (direction) => {
|
|
183
|
+
const key = `${direction}${view === "month" ? "Month" : "Week"}`;
|
|
184
|
+
return overrides[key] ?? overrides[direction] ?? labels[key];
|
|
185
|
+
};
|
|
93
186
|
const focusDate = (date) => {
|
|
94
187
|
setActiveDate(date);
|
|
95
188
|
requestAnimationFrame(() => gridRef.current?.querySelector(`[data-calendar-date="${date}"]`)?.focus());
|
|
@@ -107,18 +200,10 @@ function SchedulingCalendar({
|
|
|
107
200
|
focusDate(days[days.length - 1]);
|
|
108
201
|
}
|
|
109
202
|
};
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
return date >= span.start && date <= span.end;
|
|
113
|
-
});
|
|
114
|
-
const overlaysFor = (date) => overlays.filter((overlay) => {
|
|
115
|
-
const span = overlaySpans.get(overlay.id);
|
|
116
|
-
return date >= span.start && date <= span.end;
|
|
117
|
-
});
|
|
118
|
-
const formatInstant = (instant) => formatInstantProp?.(instant) ?? new Intl.DateTimeFormat(locale, { timeZone, dateStyle: "medium", timeStyle: "short" }).format(new Date(instant));
|
|
119
|
-
const toggleMore = (date, dayEntries) => {
|
|
203
|
+
const entriesOn = (date) => coverage.filter((covered) => date >= covered.start && date < covered.end).map((covered) => covered.entry);
|
|
204
|
+
const toggleMore = (date, all) => {
|
|
120
205
|
if (onShowMore) {
|
|
121
|
-
onShowMore(date,
|
|
206
|
+
onShowMore(date, all);
|
|
122
207
|
return;
|
|
123
208
|
}
|
|
124
209
|
setExpandedDates((current) => {
|
|
@@ -128,21 +213,129 @@ function SchedulingCalendar({
|
|
|
128
213
|
return next;
|
|
129
214
|
});
|
|
130
215
|
};
|
|
216
|
+
const statusOf = (entry) => entry.status ?? (entry.state === "in-progress" ? "live" : "committed");
|
|
217
|
+
const markersOf = (entry) => {
|
|
218
|
+
const flagged = entry.state === "conflicted" || entry.state === "blackout-violation";
|
|
219
|
+
return flagged ? [...entry.markers ?? [], { id: "state", label: stateLabel[entry.state], emphasis: "warning" }] : entry.markers ?? [];
|
|
220
|
+
};
|
|
221
|
+
const stateText = (entry) => entry.state ? stateLabel[entry.state] : entry.status ? labels.statuses[entry.status] : stateLabel.scheduled;
|
|
222
|
+
const entryName = (entry) => entry.accessibleName ?? [fillLabel(labels.entry, { title: entry.title, state: stateText(entry), start: formatInstant(entry.start), end: formatInstant(entry.end) }), ...(entry.markers ?? []).map((marker) => marker.label)].join(", ");
|
|
223
|
+
const overlayName = (overlay) => overlay.accessibleName ?? fillLabel(labels.overlay, { title: [overlay.kindLabel, overlay.label, overlay.scopeLabel].filter(Boolean).join(", "), start: formatInstant(overlay.start), end: formatInstant(overlay.end) });
|
|
224
|
+
const entryProps = (entry) => ({
|
|
225
|
+
type: "button",
|
|
226
|
+
"data-item-id": entry.id,
|
|
227
|
+
"data-band": entry.band ?? "none",
|
|
228
|
+
"data-status": statusOf(entry),
|
|
229
|
+
"data-state": entry.state,
|
|
230
|
+
onClick: () => onSelectEntry?.(entry),
|
|
231
|
+
"aria-label": entryName(entry)
|
|
232
|
+
});
|
|
233
|
+
const entryContent = (entry, withTime = true) => {
|
|
234
|
+
const markers = markersOf(entry);
|
|
235
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
236
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__entry-text", children: renderEntry?.(entry) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
237
|
+
withTime && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
238
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__time", children: zonedTimeOfDay(entry.start, timeZone) }),
|
|
239
|
+
" "
|
|
240
|
+
] }),
|
|
241
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__title", children: entry.title })
|
|
242
|
+
] }) }),
|
|
243
|
+
markers.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__markers", children: markers.map((marker) => /* @__PURE__ */ jsx(Marker, { marker }, marker.id)) })
|
|
244
|
+
] });
|
|
245
|
+
};
|
|
246
|
+
const laneStyle = (placed, windowLanes) => ({
|
|
247
|
+
gridColumn: `${placed.startCol + 1} / ${placed.endCol + 2}`,
|
|
248
|
+
["--uix-scheduling-calendar-lane"]: placed.group === "window" ? placed.lane : windowLanes + placed.lane
|
|
249
|
+
});
|
|
250
|
+
const nothingToShow = visibleEntries.length === 0 && !Object.values(dayEntries ?? {}).some((list) => list.length > 0) && !Object.values(dayInfo ?? {}).some((day) => day.count > 0);
|
|
251
|
+
const chipRows = Math.max(0, maxEntriesPerDay ?? DEFAULT_CHIP_ROWS, ...Object.values(dayEntries ?? {}).map((list) => list.length));
|
|
252
|
+
const renderDay = (date, index) => {
|
|
253
|
+
const info = dayInfo?.[date];
|
|
254
|
+
const all = picksGiven ? dayEntries[date] ?? EMPTY_ENTRIES : singles.get(date) ?? EMPTY_ENTRIES;
|
|
255
|
+
const overflowing = !picksGiven && maxEntriesPerDay !== void 0 && all.length > maxEntriesPerDay;
|
|
256
|
+
const collapsed = overflowing && (controlled || onShowMore !== void 0 || !expandedDates.has(date));
|
|
257
|
+
const shown = collapsed ? all.slice(0, Math.max(0, maxEntriesPerDay)) : all;
|
|
258
|
+
const hiddenCount = info ? info.overflowCount : picksGiven ? 0 : all.length - shown.length;
|
|
259
|
+
const onDay = picksGiven ? all : entriesOn(date);
|
|
260
|
+
const badge = renderDayBadge?.(date, onDay);
|
|
261
|
+
const hasBadge = badge != null && badge !== false;
|
|
262
|
+
const dateButton = /* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__date", "data-calendar-date": date, tabIndex: activeDate === date ? 0 : -1, onFocus: () => setActiveDate(date), onKeyDown: (event) => onDayKeyDown(event, date), onClick: onSelectDate ? () => onSelectDate(date) : void 0, "aria-label": dateText(date, "day"), children: Number(date.slice(-2)) });
|
|
263
|
+
const moreText = fillLabel(labels.moreEntries, { count: hiddenCount });
|
|
264
|
+
const moreName = fillLabel(labels.moreEntriesLabel, { count: hiddenCount, date: dateText(date, "day") });
|
|
265
|
+
return /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__day", "data-outside": view === "month" && !monthDays[index].inMonth || void 0, children: [
|
|
266
|
+
hasBadge || info ? /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__dayhead", children: [
|
|
267
|
+
hasBadge && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__badge", children: badge }),
|
|
268
|
+
info && /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__count", children: [
|
|
269
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: info.count }),
|
|
270
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: info.label ?? fillLabel(labels.dayCount, { count: info.count }) })
|
|
271
|
+
] }),
|
|
272
|
+
info?.markers && info.markers.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__markers", children: info.markers.map((marker) => /* @__PURE__ */ jsx(Marker, { marker }, marker.id)) }),
|
|
273
|
+
dateButton
|
|
274
|
+
] }) : dateButton,
|
|
275
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__entries", children: [
|
|
276
|
+
shown.map((entry) => /* @__PURE__ */ jsx("button", { className: "uix-scheduling-calendar__entry", ...entryProps(entry), children: entryContent(entry) }, entry.id)),
|
|
277
|
+
controlled ? hiddenCount > 0 && (onShowMore ? /* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__more", "aria-label": moreName, onClick: () => onShowMore(date, onDay), children: moreText }) : /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__more", children: [
|
|
278
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: moreText }),
|
|
279
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: moreName })
|
|
280
|
+
] })) : overflowing && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__more", "aria-expanded": onShowMore ? void 0 : !collapsed, "aria-label": collapsed ? moreName : void 0, onClick: () => toggleMore(date, onDay), children: collapsed ? moreText : labels.fewerEntries })
|
|
281
|
+
] })
|
|
282
|
+
] }, date);
|
|
283
|
+
};
|
|
284
|
+
const renderWeek = (row) => {
|
|
285
|
+
const rowDays = days.slice(row * 7, row * 7 + 7);
|
|
286
|
+
const windowLanes = lanesUsed(row, "window");
|
|
287
|
+
const spanLanes = lanesUsed(row, "item");
|
|
288
|
+
const hidden = layout.hiddenByRow?.[row]?.length ?? 0;
|
|
289
|
+
const firstHiddenDay = layout.firstHiddenDayByRow?.[row] ?? rowDays[0];
|
|
290
|
+
const moreText = fillLabel(labels.moreSpans, { count: hidden });
|
|
291
|
+
const moreName = fillLabel(labels.moreSpansLabel, { count: hidden, date: dateText(firstHiddenDay, "day") });
|
|
292
|
+
const style = { ["--uix-scheduling-calendar-lanes"]: Math.max(windowLanes + spanLanes, hidden > 0 ? 1 : 0) };
|
|
293
|
+
return /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__week", style, children: [
|
|
294
|
+
rowDays.map((date, column) => renderDay(date, row * 7 + column)),
|
|
295
|
+
(placedByRow.get(row) ?? []).map((placed) => {
|
|
296
|
+
const continues = { "data-continues-before": placed.continuesBefore || void 0, "data-continues-after": placed.continuesAfter || void 0 };
|
|
297
|
+
if (placed.group === "window") {
|
|
298
|
+
const overlay = overlayById.get(placed.id);
|
|
299
|
+
if (!overlay) return null;
|
|
300
|
+
return /* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__window", "data-overlay-id": overlay.id, "data-pattern": overlay.pattern ?? "solid", "data-global": overlay.global || void 0, "data-kind": overlay.kind, ...continues, style: laneStyle(placed, windowLanes), onClick: () => onSelectOverlay?.(overlay), "aria-label": overlayName(overlay), children: /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__window-text", children: [
|
|
301
|
+
overlay.kindLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-kind", children: overlay.kindLabel }),
|
|
302
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-name", children: overlay.label }),
|
|
303
|
+
overlay.scopeLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-scope", children: overlay.scopeLabel })
|
|
304
|
+
] }) }, `window-${placed.id}`);
|
|
305
|
+
}
|
|
306
|
+
const entry = entryById.get(placed.id);
|
|
307
|
+
if (!entry) return null;
|
|
308
|
+
return /* @__PURE__ */ jsx("button", { className: "uix-scheduling-calendar__entry uix-scheduling-calendar__span", ...entryProps(entry), ...continues, style: laneStyle(placed, windowLanes), children: entryContent(entry, !placed.continuesBefore) }, `span-${placed.id}`);
|
|
309
|
+
}),
|
|
310
|
+
hidden > 0 && (onShowMore ? /* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__rowmore", "aria-label": moreName, onClick: () => onShowMore(firstHiddenDay, picksGiven ? dayEntries[firstHiddenDay] ?? EMPTY_ENTRIES : entriesOn(firstHiddenDay)), children: moreText }) : /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__rowmore", children: [
|
|
311
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: moreText }),
|
|
312
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: moreName })
|
|
313
|
+
] }))
|
|
314
|
+
] }, rowDays[0]);
|
|
315
|
+
};
|
|
316
|
+
const gridStyle = fixed ? {
|
|
317
|
+
["--uix-scheduling-calendar-chips"]: chipRows,
|
|
318
|
+
["--uix-scheduling-calendar-lane-cap"]: Math.max(
|
|
319
|
+
Math.max(Number.isFinite(windowCap) ? windowCap : 0, lanesUsedAnywhere("window")) + Math.max(Number.isFinite(spanCap) ? spanCap : 0, lanesUsedAnywhere("item")),
|
|
320
|
+
Object.keys(layout.hiddenByRow ?? {}).length > 0 ? 1 : 0
|
|
321
|
+
)
|
|
322
|
+
} : void 0;
|
|
131
323
|
return /* @__PURE__ */ jsxs("section", { className: cx("uix-scheduling-calendar", className), "aria-label": fillLabel(labels.region, { timeZone }), children: [
|
|
132
|
-
/* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__header", children: [
|
|
324
|
+
showHeader && /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__header", children: [
|
|
133
325
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
134
|
-
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: () => movePeriod(-1), disabled: !onAnchorDateChange, children:
|
|
135
|
-
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: () => movePeriod(1), disabled: !onAnchorDateChange, children:
|
|
326
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: () => movePeriod(-1), disabled: !onAnchorDateChange, children: periodLabel("previous") }),
|
|
327
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: () => movePeriod(1), disabled: !onAnchorDateChange, children: periodLabel("next") })
|
|
136
328
|
] }),
|
|
137
329
|
/* @__PURE__ */ jsx("strong", { children: view === "month" ? dateText(startOfMonth(anchorDate), "month") : `${dateText(weekStart, "day")} \u2013 ${dateText(weekDays[weekDays.length - 1], "day")}` }),
|
|
138
330
|
/* @__PURE__ */ jsx("div", { className: "uix-segmented", role: "group", "aria-label": labels.viewGroup, children: ["month", "week", "agenda"].map((option) => /* @__PURE__ */ jsx("button", { type: "button", className: "uix-segmented__option", "aria-pressed": view === option, "data-selected": view === option || void 0, onClick: () => setView(option), children: viewLabel[option] }, option)) })
|
|
139
331
|
] }),
|
|
332
|
+
notice != null && notice !== false && /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__notice", children: notice }),
|
|
140
333
|
loading ? /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__state", role: "status", children: labels.loading }) : error ? /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__state", role: "alert", children: [
|
|
141
334
|
/* @__PURE__ */ jsx("p", { children: error }),
|
|
142
335
|
onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: labels.retry })
|
|
143
336
|
] }) : view === "agenda" ? /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__agenda", role: "region", "aria-label": labels.agenda, children: visibleEntries.length === 0 ? /* @__PURE__ */ jsx("p", { children: labels.agendaEmpty }) : /* @__PURE__ */ jsx("ol", { children: [...visibleEntries].sort((a, b) => a.start.localeCompare(b.start)).map((entry) => {
|
|
144
|
-
const
|
|
145
|
-
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => onSelectEntry?.(entry), children: [
|
|
337
|
+
const markers = markersOf(entry);
|
|
338
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", { type: "button", "data-item-id": entry.id, "data-band": entry.band ?? "none", "data-status": statusOf(entry), "data-state": entry.state, onClick: () => onSelectEntry?.(entry), children: [
|
|
146
339
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
147
340
|
/* @__PURE__ */ jsx("strong", { children: renderEntry?.(entry) ?? entry.title }),
|
|
148
341
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
@@ -152,40 +345,24 @@ function SchedulingCalendar({
|
|
|
152
345
|
] }),
|
|
153
346
|
entry.meta && /* @__PURE__ */ jsx("span", { children: entry.meta })
|
|
154
347
|
] }),
|
|
155
|
-
/* @__PURE__ */ jsx(
|
|
348
|
+
markers.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__markers", children: markers.map((marker) => /* @__PURE__ */ jsx(Marker, { marker }, marker.id)) }),
|
|
349
|
+
entry.state ? /* @__PURE__ */ jsx(StatusPill, { tone: stateTone(entry.state), children: stateLabel[entry.state] }) : /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__status", children: stateText(entry) })
|
|
156
350
|
] }) }, entry.id);
|
|
157
|
-
}) }) }) : /* @__PURE__ */ jsxs("div", { ref: gridRef, className: cx("uix-scheduling-calendar__grid", view === "week" && "uix-scheduling-calendar__grid--week"), role: "group", "aria-label": fillLabel(labels.grid, { view: viewLabel[view] }), children: [
|
|
351
|
+
}) }) }) : /* @__PURE__ */ jsxs("div", { ref: gridRef, className: cx("uix-scheduling-calendar__grid", view === "week" && "uix-scheduling-calendar__grid--week"), "data-fixed": fixed || void 0, style: gridStyle, role: "group", "aria-label": fillLabel(labels.grid, { view: viewLabel[view] }), children: [
|
|
158
352
|
days.slice(0, 7).map((date) => /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__weekday", "aria-hidden": "true", children: dateText(date, "weekday") }, `weekday-${date}`)),
|
|
159
|
-
days.
|
|
160
|
-
|
|
161
|
-
const dayOverlays = overlaysFor(date);
|
|
162
|
-
const overflowing = maxEntriesPerDay !== void 0 && dayEntries.length > maxEntriesPerDay;
|
|
163
|
-
const collapsed = overflowing && (onShowMore !== void 0 || !expandedDates.has(date));
|
|
164
|
-
const shownEntries = collapsed ? dayEntries.slice(0, Math.max(0, maxEntriesPerDay)) : dayEntries;
|
|
165
|
-
const hiddenCount = dayEntries.length - shownEntries.length;
|
|
166
|
-
const badge = renderDayBadge?.(date, dayEntries);
|
|
167
|
-
const dateButton = /* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__date", "data-calendar-date": date, tabIndex: activeDate === date ? 0 : -1, onFocus: () => setActiveDate(date), onKeyDown: (event) => onDayKeyDown(event, date), "aria-label": dateText(date, "day"), children: Number(date.slice(-2)) });
|
|
168
|
-
return /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__day", "data-outside": view === "month" && !monthDays[index].inMonth || void 0, children: [
|
|
169
|
-
badge == null || badge === false ? dateButton : /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__dayhead", children: [
|
|
170
|
-
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__badge", children: badge }),
|
|
171
|
-
dateButton
|
|
172
|
-
] }),
|
|
173
|
-
dayOverlays.map((overlay) => /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__overlay", "data-kind": overlay.kind, children: overlay.label }, overlay.id)),
|
|
174
|
-
/* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__entries", children: [
|
|
175
|
-
shownEntries.map((entry) => {
|
|
176
|
-
const span = entrySpans.get(entry.id);
|
|
177
|
-
const state = entry.state ?? "scheduled";
|
|
178
|
-
return /* @__PURE__ */ jsxs("button", { type: "button", className: "uix-scheduling-calendar__entry", "data-state": state, "data-range-start": date === span.start || void 0, "data-range-end": date === span.end || void 0, onClick: () => onSelectEntry?.(entry), "aria-label": fillLabel(labels.entry, { title: entry.title, state: stateLabel[state], start: formatInstant(entry.start), end: formatInstant(entry.end) }), children: [
|
|
179
|
-
/* @__PURE__ */ jsx("span", { children: renderEntry?.(entry) ?? entry.title }),
|
|
180
|
-
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: stateLabel[state] })
|
|
181
|
-
] }, entry.id);
|
|
182
|
-
}),
|
|
183
|
-
overflowing && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__more", "aria-expanded": onShowMore ? void 0 : !collapsed, "aria-label": collapsed ? fillLabel(labels.moreEntriesLabel, { count: hiddenCount, date: dateText(date, "day") }) : void 0, onClick: () => toggleMore(date, dayEntries), children: collapsed ? fillLabel(labels.moreEntries, { count: hiddenCount }) : labels.fewerEntries })
|
|
184
|
-
] })
|
|
185
|
-
] }, date);
|
|
186
|
-
})
|
|
353
|
+
Array.from({ length: days.length / 7 }, (_, row) => renderWeek(row)),
|
|
354
|
+
emptyNote != null && emptyNote !== false && nothingToShow && /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__empty", children: emptyNote })
|
|
187
355
|
] }),
|
|
188
|
-
/* @__PURE__ */
|
|
356
|
+
legend ? (legend.length > 0 || legendCaption != null && legendCaption !== false) && /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__legend", role: "group", "aria-label": labels.legend, children: [
|
|
357
|
+
legend.length > 0 && /* @__PURE__ */ jsx("ul", { className: "uix-scheduling-calendar__legend-list", children: legend.map((item) => /* @__PURE__ */ jsxs("li", { "data-legend-id": item.id, children: [
|
|
358
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__swatch", "aria-hidden": "true", "data-band": item.swatch.band, "data-status": item.swatch.status, "data-pattern": item.swatch.pattern, children: item.swatch.icon }),
|
|
359
|
+
item.label
|
|
360
|
+
] }, item.id)) }),
|
|
361
|
+
legendCaption != null && legendCaption !== false && /* @__PURE__ */ jsx("p", { className: "uix-scheduling-calendar__legend-caption", children: legendCaption })
|
|
362
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__legend", role: "group", "aria-label": labels.legend, children: [
|
|
363
|
+
["scheduled", "conflicted", "in-progress", "blackout-violation"].map((state) => /* @__PURE__ */ jsx("span", { "data-state": state, children: stateLabel[state] }, state)),
|
|
364
|
+
legendCaption != null && legendCaption !== false && /* @__PURE__ */ jsx("p", { className: "uix-scheduling-calendar__legend-caption", children: legendCaption })
|
|
365
|
+
] })
|
|
189
366
|
] });
|
|
190
367
|
}
|
|
191
368
|
export {
|
|
@@ -3,6 +3,7 @@ import { ReactNode } from 'react';
|
|
|
3
3
|
import { TimelineRange, TimelineScale } from '../scheduling-timeline-model.js';
|
|
4
4
|
import { SchedulingEntryState } from './SchedulingCalendar.js';
|
|
5
5
|
import '../calendar-model.js';
|
|
6
|
+
import '../scheduling-calendar-model.js';
|
|
6
7
|
|
|
7
8
|
interface SchedulingTimelineLane {
|
|
8
9
|
id: string;
|
|
@@ -6,6 +6,14 @@ interface SegmentedProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'
|
|
|
6
6
|
value?: string;
|
|
7
7
|
/** Fires with the option's value when a different option is chosen. */
|
|
8
8
|
onChange?: (value: string) => void;
|
|
9
|
+
/**
|
|
10
|
+
* How the choice is exposed to assistive technology (HAR-1604).
|
|
11
|
+
* `'toggle'` (default): `role="group"` of buttons with `aria-pressed`, for a view or filter
|
|
12
|
+
* toggle bar. `'radio'`: `role="radiogroup"` of `role="radio"` with `aria-checked`, for a
|
|
13
|
+
* setting with exactly one value (theme, density, language). Name the group with
|
|
14
|
+
* `aria-label` or `aria-labelledby` in either mode. The keyboard model is the same in both.
|
|
15
|
+
*/
|
|
16
|
+
selection?: 'toggle' | 'radio';
|
|
9
17
|
children?: ReactNode;
|
|
10
18
|
}
|
|
11
19
|
/**
|
|
@@ -13,16 +21,22 @@ interface SegmentedProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'
|
|
|
13
21
|
* (density, audience, view mode …) that reads as one pill with a lifted active
|
|
14
22
|
* segment. Selection state is shared with `<SegmentedOption>` via context, so
|
|
15
23
|
* the caller only wires `value` + `onChange` once.
|
|
24
|
+
*
|
|
25
|
+
* Keyboard (HAR-1604): the group is ONE tab stop, on the selected option (the first enabled
|
|
26
|
+
* one when nothing is selected). Arrow Right/Down and Left/Up move to the next / previous
|
|
27
|
+
* enabled option and select it, wrapping at the ends; Home and End jump; Left/Right swap in
|
|
28
|
+
* a right-to-left group. Before 2.32.0 every option was its own tab stop.
|
|
16
29
|
*/
|
|
17
|
-
declare function Segmented({ value, onChange, children, className, ...props }: SegmentedProps): react.JSX.Element;
|
|
30
|
+
declare function Segmented({ value, onChange, selection, children, className, onKeyDown, ...props }: SegmentedProps): react.JSX.Element;
|
|
18
31
|
interface SegmentedOptionProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> {
|
|
19
32
|
value: string;
|
|
20
33
|
children?: ReactNode;
|
|
21
34
|
}
|
|
22
35
|
/**
|
|
23
36
|
* One option in a `<Segmented>`. Active state is derived from the parent's
|
|
24
|
-
* `value`; the
|
|
25
|
-
* `.uix-segmented__option[aria-pressed="true"]
|
|
37
|
+
* `value`; the selected segment lifts to the surface via
|
|
38
|
+
* `.uix-segmented__option[aria-pressed="true"]` (or `[aria-checked="true"]` in a
|
|
39
|
+
* `selection="radio"` group).
|
|
26
40
|
*/
|
|
27
41
|
declare function SegmentedOption({ value, children, className, onClick, ...props }: SegmentedOptionProps): react.JSX.Element;
|
|
28
42
|
|
|
@@ -1,11 +1,61 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { createContext, useContext, useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
const SegmentedCtx = createContext({});
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const OPTION = ".uix-segmented__option";
|
|
7
|
+
function Segmented({ value, onChange, selection = "toggle", children, className, onKeyDown, ...props }) {
|
|
8
|
+
const ref = useRef(null);
|
|
9
|
+
const contextValue = useMemo(() => ({ value, onChange, selection }), [value, onChange, selection]);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const group = ref.current;
|
|
12
|
+
if (!group) return;
|
|
13
|
+
const all = Array.from(group.querySelectorAll(OPTION));
|
|
14
|
+
const enabled = all.filter((o) => !o.disabled);
|
|
15
|
+
const current = enabled.find((o) => o.getAttribute("aria-pressed") === "true" || o.getAttribute("aria-checked") === "true") ?? enabled[0];
|
|
16
|
+
for (const o of all) o.tabIndex = o === current ? 0 : -1;
|
|
17
|
+
});
|
|
18
|
+
const handleKeyDown = (event) => {
|
|
19
|
+
onKeyDown?.(event);
|
|
20
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
21
|
+
const group = ref.current;
|
|
22
|
+
const from = event.target.closest(OPTION);
|
|
23
|
+
if (!group || !from || !group.contains(from)) return;
|
|
24
|
+
const options = Array.from(group.querySelectorAll(`${OPTION}:not(:disabled)`));
|
|
25
|
+
const index = options.indexOf(from);
|
|
26
|
+
if (index < 0) return;
|
|
27
|
+
const rtl = getComputedStyle(group).direction === "rtl";
|
|
28
|
+
let next;
|
|
29
|
+
switch (event.key) {
|
|
30
|
+
case "ArrowDown":
|
|
31
|
+
next = index + 1;
|
|
32
|
+
break;
|
|
33
|
+
case "ArrowUp":
|
|
34
|
+
next = index - 1;
|
|
35
|
+
break;
|
|
36
|
+
case "ArrowRight":
|
|
37
|
+
next = index + (rtl ? -1 : 1);
|
|
38
|
+
break;
|
|
39
|
+
case "ArrowLeft":
|
|
40
|
+
next = index + (rtl ? 1 : -1);
|
|
41
|
+
break;
|
|
42
|
+
case "Home":
|
|
43
|
+
next = 0;
|
|
44
|
+
break;
|
|
45
|
+
case "End":
|
|
46
|
+
next = options.length - 1;
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
const target = options[(next + options.length) % options.length];
|
|
54
|
+
if (!target || target === from) return;
|
|
55
|
+
target.focus();
|
|
56
|
+
target.click();
|
|
57
|
+
};
|
|
58
|
+
return /* @__PURE__ */ jsx(SegmentedCtx.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { ref, role: selection === "radio" ? "radiogroup" : "group", className: cx("uix-segmented", className), onKeyDown: handleKeyDown, ...props, children }) });
|
|
9
59
|
}
|
|
10
60
|
function SegmentedOption({
|
|
11
61
|
value,
|
|
@@ -16,12 +66,16 @@ function SegmentedOption({
|
|
|
16
66
|
}) {
|
|
17
67
|
const ctx = useContext(SegmentedCtx);
|
|
18
68
|
const selected = ctx.value === value;
|
|
69
|
+
const radio = ctx.selection === "radio";
|
|
19
70
|
return /* @__PURE__ */ jsx(
|
|
20
71
|
"button",
|
|
21
72
|
{
|
|
22
73
|
type: "button",
|
|
74
|
+
role: radio ? "radio" : void 0,
|
|
23
75
|
className: cx("uix-segmented__option", className),
|
|
24
|
-
"aria-pressed": selected,
|
|
76
|
+
"aria-pressed": radio ? void 0 : selected,
|
|
77
|
+
"aria-checked": radio ? selected : void 0,
|
|
78
|
+
tabIndex: ctx.selection ? selected ? 0 : -1 : void 0,
|
|
25
79
|
onClick: (e) => {
|
|
26
80
|
ctx.onChange?.(value);
|
|
27
81
|
onClick?.(e);
|
|
@@ -1,9 +1,73 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { SelectHTMLAttributes } from 'react';
|
|
2
|
+
import { SelectHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
import { Placement } from '../overlay-position.js';
|
|
4
|
+
import { SelectOption, SelectGroup } from '../select-model.js';
|
|
3
5
|
|
|
6
|
+
/** Chrome strings of `Select`; `{count}` is replaced where noted. */
|
|
7
|
+
interface SelectLabels {
|
|
8
|
+
/** Accessible name of the filter field when the Select has no label of its own. */
|
|
9
|
+
search: string;
|
|
10
|
+
/** Placeholder of the filter field. */
|
|
11
|
+
searchPlaceholder: string;
|
|
12
|
+
loading: string;
|
|
13
|
+
/** No options at all. */
|
|
14
|
+
empty: string;
|
|
15
|
+
/** A filter matched nothing. */
|
|
16
|
+
noMatches: string;
|
|
17
|
+
/** Async `loadOptions` failed. */
|
|
18
|
+
error: string;
|
|
19
|
+
retry: string;
|
|
20
|
+
/** Multi-select: clears every value. */
|
|
21
|
+
clear: string;
|
|
22
|
+
/** Multi-select on a phone: closes the sheet. */
|
|
23
|
+
done: string;
|
|
24
|
+
/** Multi-select trigger, spoken after the "+N". `{count}` = how many are selected. */
|
|
25
|
+
selectedCount: string;
|
|
26
|
+
/** Announced after filtering. `{count}`. */
|
|
27
|
+
results: string;
|
|
28
|
+
}
|
|
4
29
|
interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
|
|
5
30
|
size?: 'sm' | 'md';
|
|
31
|
+
/** The options as data, instead of `<option>` / `<optgroup>` children. */
|
|
32
|
+
options?: readonly (SelectOption | SelectGroup)[];
|
|
33
|
+
/**
|
|
34
|
+
* Called with the new value: a string, or every selected value with `multiple`. `option` is the
|
|
35
|
+
* option that was chosen or toggled. `onChange(e)` still fires as on a native select.
|
|
36
|
+
*/
|
|
37
|
+
onValueChange?: (value: string | string[], option?: SelectOption) => void;
|
|
38
|
+
/** Shown while nothing is selected. Also makes "nothing" the starting value (and `required` checks it). */
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
/** Marks the control invalid (`aria-invalid`), e.g. from your own validation. */
|
|
41
|
+
invalid?: boolean;
|
|
42
|
+
/** Shows and submits the value, but does not open. */
|
|
43
|
+
readOnly?: boolean;
|
|
44
|
+
/** A filter field at the top of the list. `'auto'` adds it for more than 12 options. Default `false`. */
|
|
45
|
+
searchable?: boolean | 'auto';
|
|
46
|
+
/**
|
|
47
|
+
* Load the options when the list opens (and, when `searchable`, as the filter changes). The
|
|
48
|
+
* previous request's `signal` is aborted when a newer one starts or the list closes. Pass the
|
|
49
|
+
* selected option in `options` so its label shows before the first load.
|
|
50
|
+
*/
|
|
51
|
+
loadOptions?: (query: string, signal: AbortSignal) => Promise<readonly (SelectOption | SelectGroup)[]>;
|
|
52
|
+
/** Draw an option's content (the check mark for the selected state is drawn for you). */
|
|
53
|
+
renderOption?: (option: SelectOption, state: {
|
|
54
|
+
selected: boolean;
|
|
55
|
+
active: boolean;
|
|
56
|
+
}) => ReactNode;
|
|
57
|
+
/** Draw the trigger's content for the selected option(s). */
|
|
58
|
+
renderValue?: (selected: readonly SelectOption[]) => ReactNode;
|
|
59
|
+
/** Preferred side of the list. Default `'bottom-start'`; it flips when there is no room. */
|
|
60
|
+
placement?: Placement;
|
|
61
|
+
labels?: Partial<SelectLabels>;
|
|
6
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* A select that draws its own list (HAR-1572): the WAI-ARIA APG select-only combobox — focus
|
|
65
|
+
* stays on the trigger, `aria-activedescendant` marks the active option — in a top-layer popover
|
|
66
|
+
* placed with `useAnchoredPosition`, or a bottom sheet on a phone. A visually hidden native
|
|
67
|
+
* `<select>` carries `name`, `required`, `form`, `multiple` and the value, so `<option>` children,
|
|
68
|
+
* `onChange(e)` with `e.target.value`, FormData, `form.reset()`, `ref` and react-hook-form keep
|
|
69
|
+
* working; `ref` points at that select.
|
|
70
|
+
*/
|
|
7
71
|
declare const Select: react.ForwardRefExoticComponent<SelectProps & react.RefAttributes<HTMLSelectElement>>;
|
|
8
72
|
|
|
9
|
-
export { Select, type SelectProps };
|
|
73
|
+
export { Select, SelectGroup, type SelectLabels, SelectOption, type SelectProps };
|