@tensor_1/react 2.31.0 → 2.33.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 +367 -0
- package/dist/SchedulingCalendar-C4XVbqZr.d.ts +339 -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 +4 -110
- package/dist/components/SchedulingCalendar.js +299 -63
- package/dist/components/SchedulingTimeGrid.d.ts +4 -0
- package/dist/components/SchedulingTimeGrid.js +521 -0
- package/dist/components/SchedulingTimeline.d.ts +2 -1
- 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 +4888 -1714
- package/dist/index.d.cts +1871 -658
- package/dist/index.d.ts +16 -9
- package/dist/index.js +67 -4
- package/dist/labels-context.d.ts +29 -1
- 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 +169 -0
- package/dist/scheduling-calendar-model.js +176 -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
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { zonedDateKey, zonedHourSlots, zonedTimeOfDay } from "../calendar-model.js";
|
|
5
|
+
import { layoutDaySpans, layoutTimeGridDay, placesInTopLane, proposeMove } from "../scheduling-calendar-model.js";
|
|
6
|
+
import { fillLabel } from "../fill-label.js";
|
|
7
|
+
const HOUR_MS = 36e5;
|
|
8
|
+
const MINUTES_PER_HOUR = 60;
|
|
9
|
+
const LINES_PER_HOUR = 3;
|
|
10
|
+
const DRAG_THRESHOLD = 4;
|
|
11
|
+
const DEFAULT_STEP = 15;
|
|
12
|
+
const vars = (values) => values;
|
|
13
|
+
const linesFor = (segment) => Math.max(0, Math.floor(segment.lengthHours * LINES_PER_HOUR - 0.25));
|
|
14
|
+
const slotSignature = (slots) => slots.map((slot) => `${slot.label}${slot.offsetLabel ?? ""}`).join(" ");
|
|
15
|
+
function uniqueById(items) {
|
|
16
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17
|
+
return items.filter((item) => seen.has(item.id) ? false : (seen.add(item.id), true));
|
|
18
|
+
}
|
|
19
|
+
const GridItem = memo(function GridItem2({ entry, segment, column, alone, name, status, movable, hintId, timeZone, continuesFrom, markers, renderEntry, renderMarker, handlers }) {
|
|
20
|
+
const lines = linesFor(segment);
|
|
21
|
+
const continuation = segment.part === "continuation";
|
|
22
|
+
const style = vars({
|
|
23
|
+
"--uix-scheduling-calendar-offset": segment.offsetHours,
|
|
24
|
+
"--uix-scheduling-calendar-length": segment.lengthHours,
|
|
25
|
+
"--uix-scheduling-calendar-lane": segment.lane,
|
|
26
|
+
"--uix-scheduling-calendar-lanes": segment.laneCount
|
|
27
|
+
});
|
|
28
|
+
const text = renderEntry?.(entry, { availableLines: lines }) ?? (lines > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
29
|
+
!continuation && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
30
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__time", children: zonedTimeOfDay(entry.start, timeZone) }),
|
|
31
|
+
" "
|
|
32
|
+
] }),
|
|
33
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__title", children: entry.title })
|
|
34
|
+
] }) : null);
|
|
35
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
36
|
+
continuation && lines > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__tg-from", children: continuesFrom }),
|
|
37
|
+
(!continuation || alone) && text != null && text !== false && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__entry-text", children: text }),
|
|
38
|
+
markers.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__markers", children: markers.map((marker) => /* @__PURE__ */ jsx("span", { children: renderMarker(marker) }, marker.id)) })
|
|
39
|
+
] });
|
|
40
|
+
const shared = {
|
|
41
|
+
className: "uix-scheduling-calendar__entry uix-scheduling-calendar__tg-item",
|
|
42
|
+
"data-band": entry.band ?? "none",
|
|
43
|
+
"data-status": status,
|
|
44
|
+
"data-state": entry.state,
|
|
45
|
+
"data-part": segment.part,
|
|
46
|
+
"data-lines": Math.min(lines, 3),
|
|
47
|
+
"data-movable": movable || void 0,
|
|
48
|
+
style,
|
|
49
|
+
onClick: () => handlers.click(entry),
|
|
50
|
+
onPointerDown: movable ? (event) => handlers.pointerDown(event, entry, column, false) : void 0
|
|
51
|
+
};
|
|
52
|
+
if (continuation && !alone) return /* @__PURE__ */ jsx("div", { ...shared, "data-continuation-of": entry.id, "aria-hidden": "true", children: body });
|
|
53
|
+
return /* @__PURE__ */ jsx(
|
|
54
|
+
"button",
|
|
55
|
+
{
|
|
56
|
+
type: "button",
|
|
57
|
+
...shared,
|
|
58
|
+
"data-item-id": entry.id,
|
|
59
|
+
"aria-label": name,
|
|
60
|
+
"aria-describedby": movable ? hintId : void 0,
|
|
61
|
+
onKeyDown: movable ? (event) => handlers.keyDown(event, entry) : void 0,
|
|
62
|
+
onBlur: movable ? () => handlers.blur(entry) : void 0,
|
|
63
|
+
children: body
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}, (previous, next) => previous.entry === next.entry && previous.column === next.column && previous.alone === next.alone && previous.name === next.name && previous.status === next.status && previous.movable === next.movable && previous.hintId === next.hintId && previous.timeZone === next.timeZone && previous.continuesFrom === next.continuesFrom && previous.renderEntry === next.renderEntry && previous.segment.part === next.segment.part && previous.segment.offsetHours === next.segment.offsetHours && previous.segment.lengthHours === next.segment.lengthHours && previous.segment.lane === next.segment.lane && previous.segment.laneCount === next.segment.laneCount);
|
|
67
|
+
function SchedulingTimeGrid({
|
|
68
|
+
view,
|
|
69
|
+
days,
|
|
70
|
+
timeZone,
|
|
71
|
+
entries: givenEntries,
|
|
72
|
+
overlays: givenOverlays,
|
|
73
|
+
dayInfo,
|
|
74
|
+
labels,
|
|
75
|
+
gridLabel,
|
|
76
|
+
dateText,
|
|
77
|
+
formatInstant,
|
|
78
|
+
entryName,
|
|
79
|
+
entryStatus,
|
|
80
|
+
entryMarkers,
|
|
81
|
+
overlayName,
|
|
82
|
+
renderEntry,
|
|
83
|
+
renderMarker,
|
|
84
|
+
onSelectEntry,
|
|
85
|
+
onSelectOverlay,
|
|
86
|
+
onSelectDate,
|
|
87
|
+
onShowMore,
|
|
88
|
+
now,
|
|
89
|
+
canMove,
|
|
90
|
+
step: givenStep,
|
|
91
|
+
onProposeMove,
|
|
92
|
+
topLaneCrossMidnightMinutes,
|
|
93
|
+
topLaneCap,
|
|
94
|
+
windowLaneCap,
|
|
95
|
+
maxLanes,
|
|
96
|
+
emptyNote,
|
|
97
|
+
nothingToShow
|
|
98
|
+
}) {
|
|
99
|
+
const rootRef = useRef(null);
|
|
100
|
+
const hintId = useId();
|
|
101
|
+
const [widthCap, setWidthCap] = useState(Infinity);
|
|
102
|
+
const [ghost, setGhost] = useState(null);
|
|
103
|
+
const [announcement, setAnnouncement] = useState("");
|
|
104
|
+
const moving = canMove && onProposeMove !== void 0;
|
|
105
|
+
const step = Number.isFinite(givenStep) && givenStep > 0 ? givenStep : DEFAULT_STEP;
|
|
106
|
+
const laneOptions = useMemo(() => ({ crossMidnightMinutes: topLaneCrossMidnightMinutes }), [topLaneCrossMidnightMinutes]);
|
|
107
|
+
const entries = useMemo(() => uniqueById(givenEntries), [givenEntries]);
|
|
108
|
+
const overlays = useMemo(() => uniqueById(givenOverlays), [givenOverlays]);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const root = rootRef.current;
|
|
111
|
+
if (!root || typeof ResizeObserver === "undefined") return void 0;
|
|
112
|
+
const column = root.querySelector("[data-tg-column]");
|
|
113
|
+
const probe = root.querySelector(".uix-scheduling-calendar__tg-probe");
|
|
114
|
+
if (!column || !probe) return void 0;
|
|
115
|
+
let frame = 0;
|
|
116
|
+
const measure = () => {
|
|
117
|
+
frame = 0;
|
|
118
|
+
if (!column.isConnected) return;
|
|
119
|
+
const lane = probe.getBoundingClientRect().width;
|
|
120
|
+
const width = column.getBoundingClientRect().width;
|
|
121
|
+
setWidthCap(lane > 0 && width > 0 ? Math.max(1, Math.floor(width / lane)) : Infinity);
|
|
122
|
+
};
|
|
123
|
+
const observer = new ResizeObserver(() => {
|
|
124
|
+
if (!frame) frame = requestAnimationFrame(measure);
|
|
125
|
+
});
|
|
126
|
+
observer.observe(column);
|
|
127
|
+
measure();
|
|
128
|
+
return () => {
|
|
129
|
+
observer.disconnect();
|
|
130
|
+
if (frame) cancelAnimationFrame(frame);
|
|
131
|
+
};
|
|
132
|
+
}, [days]);
|
|
133
|
+
const lanes = Math.max(1, Math.min(maxLanes, widthCap));
|
|
134
|
+
const entryById = useMemo(() => new Map(entries.map((entry) => [entry.id, entry])), [entries]);
|
|
135
|
+
const overlayById = useMemo(() => new Map(overlays.map((overlay) => [overlay.id, overlay])), [overlays]);
|
|
136
|
+
const columns = useMemo(() => days.map((date) => layoutTimeGridDay(entries, date, timeZone, { ...laneOptions, maxLanes: lanes })), [days, entries, timeZone, laneOptions, lanes]);
|
|
137
|
+
const slots = useMemo(() => days.map((date) => zonedHourSlots(date, timeZone)), [days, timeZone]);
|
|
138
|
+
const top = useMemo(() => layoutDaySpans(entries.filter((entry) => placesInTopLane(entry, timeZone, laneOptions)), days, { timeZone, laneCap: Math.max(0, topLaneCap) }), [entries, days, timeZone, laneOptions, topLaneCap]);
|
|
139
|
+
const drawable = useMemo(() => overlays.filter((overlay) => new Date(overlay.end).getTime() >= new Date(overlay.start).getTime()), [overlays]);
|
|
140
|
+
const strip = useMemo(() => layoutDaySpans(drawable, days, { timeZone, laneCap: Math.max(0, windowLaneCap) }), [drawable, days, timeZone, windowLaneCap]);
|
|
141
|
+
const shades = useMemo(() => layoutDaySpans(drawable.filter((overlay) => overlay.global), days, { timeZone }).placed, [drawable, days, timeZone]);
|
|
142
|
+
const lanesIn = (placed) => placed.reduce((max, span) => Math.max(max, span.lane + 1), 0);
|
|
143
|
+
const maxHours = Math.max(...columns.map((column) => column.dayHours));
|
|
144
|
+
const gutterIndex = useMemo(() => {
|
|
145
|
+
const signatures = slots.map(slotSignature);
|
|
146
|
+
const count = /* @__PURE__ */ new Map();
|
|
147
|
+
for (const signature of signatures) count.set(signature, (count.get(signature) ?? 0) + 1);
|
|
148
|
+
let best = 0;
|
|
149
|
+
signatures.forEach((signature, index) => {
|
|
150
|
+
if ((count.get(signature) ?? 0) > (count.get(signatures[best]) ?? 0)) best = index;
|
|
151
|
+
});
|
|
152
|
+
return best;
|
|
153
|
+
}, [slots]);
|
|
154
|
+
const gutterSignature = slotSignature(slots[gutterIndex]);
|
|
155
|
+
const hourLabels = (index, inline) => slots[index].map((slot) => /* @__PURE__ */ jsxs(
|
|
156
|
+
"div",
|
|
157
|
+
{
|
|
158
|
+
className: inline ? "uix-scheduling-calendar__tg-hour uix-scheduling-calendar__tg-hour--inline" : "uix-scheduling-calendar__tg-hour",
|
|
159
|
+
"data-hour": slot.label,
|
|
160
|
+
style: vars({ "--uix-scheduling-calendar-offset": (slot.instant.getTime() - columns[index].dayStart) / HOUR_MS }),
|
|
161
|
+
children: [
|
|
162
|
+
slot.label,
|
|
163
|
+
slot.offsetLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__tg-offset", children: slot.offsetLabel })
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
slot.instant.getTime()
|
|
167
|
+
));
|
|
168
|
+
const movable = (entry) => moving && entry.movable !== false;
|
|
169
|
+
const live = useRef({ ghost, entryById, timeZone, step, onProposeMove, onSelectEntry, labels, formatInstant, days, columns, movable });
|
|
170
|
+
live.current = { ghost, entryById, timeZone, step, onProposeMove, onSelectEntry, labels, formatInstant, days, columns, movable };
|
|
171
|
+
const dragRef = useRef(null);
|
|
172
|
+
const suppressClick = useRef(false);
|
|
173
|
+
const refocus = useRef(null);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (ghost && !entryById.has(ghost.id)) setGhost(null);
|
|
176
|
+
}, [ghost, entryById]);
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const id = refocus.current;
|
|
179
|
+
const root = rootRef.current;
|
|
180
|
+
if (!id || !root) return;
|
|
181
|
+
const active = document.activeElement;
|
|
182
|
+
const lost = !active || active === document.body;
|
|
183
|
+
if (!lost) {
|
|
184
|
+
if (active.getAttribute("data-item-id") !== id) refocus.current = null;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (const candidate of Array.from(root.querySelectorAll("[data-item-id]"))) {
|
|
188
|
+
if (candidate.getAttribute("data-item-id") === id) {
|
|
189
|
+
candidate.focus();
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
const send = useCallback((id, delta) => {
|
|
195
|
+
const state = live.current;
|
|
196
|
+
const entry = state.entryById.get(id);
|
|
197
|
+
if (!entry || !state.onProposeMove || !state.movable(entry) || delta.days === 0 && delta.minutes === 0) {
|
|
198
|
+
setGhost(null);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const proposal = proposeMove(entry, delta, state.timeZone);
|
|
202
|
+
const result = state.onProposeMove(id, proposal);
|
|
203
|
+
if (result && typeof result.then === "function") {
|
|
204
|
+
setGhost({ id, delta, source: "sent", proposal });
|
|
205
|
+
const clear = () => setGhost((current) => current && current.source === "sent" && current.proposal === proposal ? null : current);
|
|
206
|
+
result.then(clear, clear);
|
|
207
|
+
} else setGhost(null);
|
|
208
|
+
}, []);
|
|
209
|
+
const handlers = useMemo(() => ({
|
|
210
|
+
click: (entry) => {
|
|
211
|
+
live.current.onSelectEntry?.(entry);
|
|
212
|
+
},
|
|
213
|
+
keyDown: (event, entry) => {
|
|
214
|
+
const state = live.current;
|
|
215
|
+
const pending = state.ghost && state.ghost.id === entry.id && state.ghost.source === "keys" ? state.ghost.delta : null;
|
|
216
|
+
if (event.key === "Enter" && pending) {
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
refocus.current = entry.id;
|
|
219
|
+
send(entry.id, pending);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (event.key === "Escape" && pending) {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
event.stopPropagation();
|
|
225
|
+
setGhost(null);
|
|
226
|
+
setAnnouncement(state.labels.moveCancelled);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!event.shiftKey) return;
|
|
230
|
+
const base = pending ?? { days: 0, minutes: 0 };
|
|
231
|
+
const next = event.key === "ArrowUp" ? { ...base, minutes: base.minutes - state.step } : event.key === "ArrowDown" ? { ...base, minutes: base.minutes + state.step } : event.key === "ArrowLeft" ? { ...base, days: base.days - 1 } : event.key === "ArrowRight" ? { ...base, days: base.days + 1 } : null;
|
|
232
|
+
if (!next) return;
|
|
233
|
+
event.preventDefault();
|
|
234
|
+
if (next.days === 0 && next.minutes === 0) {
|
|
235
|
+
setGhost(null);
|
|
236
|
+
setAnnouncement(state.labels.moveCancelled);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const proposal = proposeMove(entry, next, state.timeZone);
|
|
240
|
+
setGhost({ id: entry.id, delta: next, source: "keys", proposal });
|
|
241
|
+
const proposed = fillLabel(state.labels.moveProposed, { start: state.formatInstant(proposal.start), end: state.formatInstant(proposal.end) });
|
|
242
|
+
setAnnouncement(proposal.adjusted ? `${fillLabel(state.labels.gapForward, { time: zonedTimeOfDay(proposal.start, state.timeZone) })} ${proposed}` : proposed);
|
|
243
|
+
},
|
|
244
|
+
blur: (entry) => {
|
|
245
|
+
refocus.current = null;
|
|
246
|
+
setGhost((current) => current && current.id === entry.id && current.source === "keys" ? null : current);
|
|
247
|
+
},
|
|
248
|
+
pointerDown: (event, entry, column, top2) => {
|
|
249
|
+
if (event.button !== 0 || dragRef.current) return;
|
|
250
|
+
const root = rootRef.current;
|
|
251
|
+
if (!root) return;
|
|
252
|
+
const elements = Array.from(root.querySelectorAll("[data-tg-column]"));
|
|
253
|
+
const rects = elements.map((element) => element.getBoundingClientRect());
|
|
254
|
+
const under = rects.findIndex((rect) => event.clientX >= rect.left && event.clientX < rect.right);
|
|
255
|
+
const from = under === -1 ? column : under;
|
|
256
|
+
const hours = live.current.columns[from]?.dayHours ?? 1;
|
|
257
|
+
const finish = (commit) => {
|
|
258
|
+
const drag = dragRef.current;
|
|
259
|
+
if (!drag) return;
|
|
260
|
+
drag.stop();
|
|
261
|
+
dragRef.current = null;
|
|
262
|
+
if (!drag.moved) return;
|
|
263
|
+
suppressClick.current = true;
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
suppressClick.current = false;
|
|
266
|
+
}, 0);
|
|
267
|
+
if (commit && !drag.dropped) send(drag.id, drag.delta);
|
|
268
|
+
else setGhost(null);
|
|
269
|
+
};
|
|
270
|
+
const onMove = (move) => {
|
|
271
|
+
const drag = dragRef.current;
|
|
272
|
+
if (!drag || move.pointerId !== drag.pointerId) return;
|
|
273
|
+
if (move.buttons === 0 && move.pointerType === "mouse") {
|
|
274
|
+
finish(false);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (drag.dropped) return;
|
|
278
|
+
const dx = move.clientX - drag.x;
|
|
279
|
+
const dy = move.clientY - drag.y;
|
|
280
|
+
if (!drag.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
281
|
+
drag.moved = true;
|
|
282
|
+
const target = drag.rects.findIndex((rect) => move.clientX >= rect.left && move.clientX < rect.right);
|
|
283
|
+
const stepMinutes = live.current.step;
|
|
284
|
+
const delta = {
|
|
285
|
+
days: target === -1 ? drag.delta.days : target - drag.column,
|
|
286
|
+
minutes: drag.top || drag.hourPx <= 0 ? 0 : Math.round(dy / drag.hourPx * MINUTES_PER_HOUR / stepMinutes) * stepMinutes
|
|
287
|
+
};
|
|
288
|
+
if (delta.days === drag.delta.days && delta.minutes === drag.delta.minutes) return;
|
|
289
|
+
drag.delta = delta;
|
|
290
|
+
const dragged = live.current.entryById.get(drag.id);
|
|
291
|
+
if (dragged) setGhost({ id: drag.id, delta, source: "pointer", proposal: proposeMove(dragged, delta, live.current.timeZone) });
|
|
292
|
+
};
|
|
293
|
+
const onUp = (up) => {
|
|
294
|
+
if (up.pointerId === dragRef.current?.pointerId) finish(true);
|
|
295
|
+
};
|
|
296
|
+
const onCancel = (cancel) => {
|
|
297
|
+
if (cancel.pointerId === dragRef.current?.pointerId) finish(false);
|
|
298
|
+
};
|
|
299
|
+
const onKey = (key) => {
|
|
300
|
+
const drag = dragRef.current;
|
|
301
|
+
if (key.key !== "Escape" || !drag?.moved || drag.dropped) return;
|
|
302
|
+
key.preventDefault();
|
|
303
|
+
key.stopPropagation();
|
|
304
|
+
drag.dropped = true;
|
|
305
|
+
setGhost(null);
|
|
306
|
+
};
|
|
307
|
+
window.addEventListener("pointermove", onMove);
|
|
308
|
+
window.addEventListener("pointerup", onUp);
|
|
309
|
+
window.addEventListener("pointercancel", onCancel);
|
|
310
|
+
window.addEventListener("keydown", onKey, true);
|
|
311
|
+
dragRef.current = {
|
|
312
|
+
id: entry.id,
|
|
313
|
+
pointerId: event.pointerId,
|
|
314
|
+
x: event.clientX,
|
|
315
|
+
y: event.clientY,
|
|
316
|
+
column: from,
|
|
317
|
+
top: top2,
|
|
318
|
+
moved: false,
|
|
319
|
+
dropped: false,
|
|
320
|
+
rects,
|
|
321
|
+
hourPx: (rects[from]?.height ?? 0) / hours,
|
|
322
|
+
delta: { days: 0, minutes: 0 },
|
|
323
|
+
stop: () => {
|
|
324
|
+
window.removeEventListener("pointermove", onMove);
|
|
325
|
+
window.removeEventListener("pointerup", onUp);
|
|
326
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
327
|
+
window.removeEventListener("keydown", onKey, true);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}), [send]);
|
|
332
|
+
useEffect(() => () => {
|
|
333
|
+
dragRef.current?.stop();
|
|
334
|
+
dragRef.current = null;
|
|
335
|
+
}, []);
|
|
336
|
+
const gridColumns = (span) => `${span.startCol + 2} / ${span.endCol + 3}`;
|
|
337
|
+
const nowAt = now === void 0 ? null : new Date(now).getTime();
|
|
338
|
+
const nowColumn = nowAt === null || Number.isNaN(nowAt) ? -1 : days.indexOf(zonedDateKey(new Date(nowAt), timeZone));
|
|
339
|
+
const topHiddenOn = (date) => top.hidden.filter((id) => {
|
|
340
|
+
const entry = entryById.get(id);
|
|
341
|
+
return entry ? layoutDaySpans([entry], [date], { timeZone }).placed.length > 0 : false;
|
|
342
|
+
});
|
|
343
|
+
const windowLanes = lanesIn(strip.placed);
|
|
344
|
+
const topLanes = lanesIn(top.placed);
|
|
345
|
+
const moreWindows = strip.hidden.length;
|
|
346
|
+
const firstHiddenWindowDay = strip.firstHiddenDay ?? days[0];
|
|
347
|
+
const ghostEntry = ghost ? entryById.get(ghost.id) : void 0;
|
|
348
|
+
const ghostTop = ghostEntry ? placesInTopLane(ghostEntry, timeZone, laneOptions) : false;
|
|
349
|
+
const ghostParts = ghost && ghostEntry && !ghostTop ? days.map((date) => layoutTimeGridDay([{ id: "ghost", start: ghost.proposal.start, end: ghost.proposal.end }], date, timeZone, { crossMidnightMinutes: Infinity }).segments) : null;
|
|
350
|
+
const ghostSpan = ghost && ghostEntry && ghostTop ? layoutDaySpans([{ id: "ghost", start: ghost.proposal.start, end: ghost.proposal.end }], days, { timeZone }).placed[0] : void 0;
|
|
351
|
+
const ghostLane = ghost ? top.placed.find((span) => span.id === ghost.id)?.lane ?? 0 : 0;
|
|
352
|
+
return /* @__PURE__ */ jsxs(
|
|
353
|
+
"div",
|
|
354
|
+
{
|
|
355
|
+
ref: rootRef,
|
|
356
|
+
className: "uix-scheduling-calendar__timegrid",
|
|
357
|
+
"data-view": view,
|
|
358
|
+
role: "group",
|
|
359
|
+
"aria-label": gridLabel,
|
|
360
|
+
style: vars({ "--uix-scheduling-calendar-days": days.length, "--uix-scheduling-calendar-hours": maxHours }),
|
|
361
|
+
onClickCapture: (event) => {
|
|
362
|
+
if (suppressClick.current) {
|
|
363
|
+
suppressClick.current = false;
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
event.stopPropagation();
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
children: [
|
|
369
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__tg-head", children: [
|
|
370
|
+
/* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__tg-zone", children: fillLabel(labels.timeZone, { timeZone }) }),
|
|
371
|
+
days.map((date, index) => {
|
|
372
|
+
const info = dayInfo?.[date];
|
|
373
|
+
const hiddenIds = dayInfo ? [] : [...columns[index].hidden, ...topHiddenOn(date)];
|
|
374
|
+
const more = info ? info.overflowCount : hiddenIds.length;
|
|
375
|
+
const moreText = fillLabel(labels.moreSpans, { count: more });
|
|
376
|
+
const moreName = fillLabel(labels.moreEntriesLabel, { count: more, date: dateText(date, "day") });
|
|
377
|
+
const headName = info ? info.label ?? `${dateText(date, "day")}, ${fillLabel(labels.dayCount, { count: info.count })}` : dateText(date, "day");
|
|
378
|
+
return /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__tg-dayhead", role: "group", "aria-label": headName, "data-date": date, children: [
|
|
379
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__date", "data-calendar-date": date, "aria-label": dateText(date, "day"), onClick: onSelectDate ? () => onSelectDate(date) : void 0, children: dateText(date, "column") }),
|
|
380
|
+
info && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__count", "aria-hidden": "true", children: info.count }),
|
|
381
|
+
info?.markers && info.markers.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__markers", children: info.markers.map((marker) => /* @__PURE__ */ jsx("span", { children: renderMarker(marker, true) }, marker.id)) }),
|
|
382
|
+
more > 0 && (onShowMore ? /* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__more", "aria-label": moreName, onClick: () => onShowMore(date, hiddenIds.map((id) => entryById.get(id)).filter(Boolean)), children: moreText }) : /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__more", children: [
|
|
383
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: moreText }),
|
|
384
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: moreName })
|
|
385
|
+
] }))
|
|
386
|
+
] }, date);
|
|
387
|
+
})
|
|
388
|
+
] }),
|
|
389
|
+
(windowLanes > 0 || moreWindows > 0) && /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__tg-strip", role: "group", "aria-label": labels.windows, style: vars({ "--uix-scheduling-calendar-lanes": Math.max(windowLanes, 1) }), children: [
|
|
390
|
+
strip.placed.map((placed) => {
|
|
391
|
+
const overlay = overlayById.get(placed.id);
|
|
392
|
+
if (!overlay) return null;
|
|
393
|
+
return /* @__PURE__ */ jsx(
|
|
394
|
+
"button",
|
|
395
|
+
{
|
|
396
|
+
type: "button",
|
|
397
|
+
className: "uix-scheduling-calendar__window",
|
|
398
|
+
"data-overlay-id": overlay.id,
|
|
399
|
+
"data-pattern": overlay.pattern ?? "solid",
|
|
400
|
+
"data-global": overlay.global || void 0,
|
|
401
|
+
"data-kind": overlay.kind,
|
|
402
|
+
"data-continues-before": placed.continuesBefore || void 0,
|
|
403
|
+
"data-continues-after": placed.continuesAfter || void 0,
|
|
404
|
+
style: { gridColumn: gridColumns(placed), ...vars({ "--uix-scheduling-calendar-lane": placed.lane }) },
|
|
405
|
+
onClick: () => onSelectOverlay?.(overlay),
|
|
406
|
+
"aria-label": overlayName(overlay),
|
|
407
|
+
children: /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__window-text", children: [
|
|
408
|
+
overlay.kindLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-kind", children: overlay.kindLabel }),
|
|
409
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-name", children: overlay.label }),
|
|
410
|
+
overlay.scopeLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-scope", children: overlay.scopeLabel })
|
|
411
|
+
] })
|
|
412
|
+
},
|
|
413
|
+
placed.id
|
|
414
|
+
);
|
|
415
|
+
}),
|
|
416
|
+
moreWindows > 0 && (onShowMore ? /* @__PURE__ */ jsx("button", { type: "button", className: "uix-scheduling-calendar__rowmore", "aria-label": fillLabel(labels.moreWindowsLabel, { count: moreWindows, date: dateText(firstHiddenWindowDay, "day") }), onClick: () => onShowMore(firstHiddenWindowDay, []), children: fillLabel(labels.moreWindows, { count: moreWindows }) }) : /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__rowmore", children: fillLabel(labels.moreWindows, { count: moreWindows }) }))
|
|
417
|
+
] }),
|
|
418
|
+
(topLanes > 0 || ghostSpan) && /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__tg-strip uix-scheduling-calendar__tg-top", role: "group", "aria-label": labels.topLane, style: vars({ "--uix-scheduling-calendar-lanes": Math.max(topLanes, 1) }), children: [
|
|
419
|
+
top.placed.map((placed) => {
|
|
420
|
+
const entry = entryById.get(placed.id);
|
|
421
|
+
if (!entry) return null;
|
|
422
|
+
const canDrag = movable(entry);
|
|
423
|
+
const markers = entryMarkers(entry);
|
|
424
|
+
return /* @__PURE__ */ jsxs(
|
|
425
|
+
"button",
|
|
426
|
+
{
|
|
427
|
+
type: "button",
|
|
428
|
+
className: "uix-scheduling-calendar__entry uix-scheduling-calendar__tg-span",
|
|
429
|
+
"data-item-id": entry.id,
|
|
430
|
+
"data-band": entry.band ?? "none",
|
|
431
|
+
"data-status": entryStatus(entry),
|
|
432
|
+
"data-state": entry.state,
|
|
433
|
+
"data-movable": canDrag || void 0,
|
|
434
|
+
"data-continues-before": placed.continuesBefore || void 0,
|
|
435
|
+
"data-continues-after": placed.continuesAfter || void 0,
|
|
436
|
+
style: { gridColumn: gridColumns(placed), ...vars({ "--uix-scheduling-calendar-lane": placed.lane }) },
|
|
437
|
+
"aria-label": entryName(entry),
|
|
438
|
+
"aria-describedby": canDrag ? hintId : void 0,
|
|
439
|
+
onClick: () => handlers.click(entry),
|
|
440
|
+
onKeyDown: canDrag ? (event) => handlers.keyDown(event, entry) : void 0,
|
|
441
|
+
onBlur: canDrag ? () => handlers.blur(entry) : void 0,
|
|
442
|
+
onPointerDown: canDrag ? (event) => handlers.pointerDown(event, entry, placed.startCol, true) : void 0,
|
|
443
|
+
children: [
|
|
444
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__entry-text", children: renderEntry?.(entry, { availableLines: 1 }) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
445
|
+
!placed.continuesBefore && !entry.allDay && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
446
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__time", children: zonedTimeOfDay(entry.start, timeZone) }),
|
|
447
|
+
" "
|
|
448
|
+
] }),
|
|
449
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__title", children: entry.title })
|
|
450
|
+
] }) }),
|
|
451
|
+
markers.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__markers", children: markers.map((marker) => /* @__PURE__ */ jsx("span", { children: renderMarker(marker) }, marker.id)) })
|
|
452
|
+
]
|
|
453
|
+
},
|
|
454
|
+
placed.id
|
|
455
|
+
);
|
|
456
|
+
}),
|
|
457
|
+
ghostSpan && /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__tg-ghost", "aria-hidden": "true", "data-sent": ghost?.source === "sent" || void 0, style: { gridColumn: gridColumns(ghostSpan), ...vars({ "--uix-scheduling-calendar-lane": ghostLane }) } })
|
|
458
|
+
] }),
|
|
459
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__tg-body", children: [
|
|
460
|
+
/* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__tg-gutter", "aria-hidden": "true", children: hourLabels(gutterIndex, false) }),
|
|
461
|
+
shades.map((placed) => {
|
|
462
|
+
const overlay = overlayById.get(placed.id);
|
|
463
|
+
const first = columns[placed.startCol];
|
|
464
|
+
const last = columns[placed.endCol];
|
|
465
|
+
const from = placed.continuesBefore ? 0 : Math.max(0, (new Date(overlay.start).getTime() - first.dayStart) / HOUR_MS) / maxHours;
|
|
466
|
+
const to = (placed.continuesAfter ? last.dayHours : Math.min(last.dayHours, (new Date(overlay.end).getTime() - last.dayStart) / HOUR_MS)) / maxHours;
|
|
467
|
+
const width = 100 / (placed.endCol - placed.startCol + 1);
|
|
468
|
+
const percent = (value) => `${Math.round(value * 1e4) / 100}%`;
|
|
469
|
+
const clipPath = placed.startCol === placed.endCol ? `polygon(0 ${percent(from)}, 100% ${percent(from)}, 100% ${percent(to)}, 0 ${percent(to)})` : `polygon(0 ${percent(from)}, ${width}% ${percent(from)}, ${width}% 0, 100% 0, 100% ${percent(to)}, ${100 - width}% ${percent(to)}, ${100 - width}% 100%, 0 100%)`;
|
|
470
|
+
return /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__tg-shade", "aria-hidden": "true", "data-pattern": overlay.pattern ?? "solid", style: { gridColumn: gridColumns(placed), clipPath } }, `shade-${placed.id}`);
|
|
471
|
+
}),
|
|
472
|
+
columns.map((column, index) => /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__tg-column", "data-tg-column": column.date, style: { gridColumn: `${index + 2}`, ...vars({ "--uix-scheduling-calendar-hours": column.dayHours }) }, children: [
|
|
473
|
+
slotSignature(slots[index]) !== gutterSignature && hourLabels(index, true),
|
|
474
|
+
column.segments.map((segment) => {
|
|
475
|
+
const entry = entryById.get(segment.id);
|
|
476
|
+
const alone = segment.part === "continuation" && index === 0;
|
|
477
|
+
return /* @__PURE__ */ jsx(
|
|
478
|
+
GridItem,
|
|
479
|
+
{
|
|
480
|
+
entry,
|
|
481
|
+
segment,
|
|
482
|
+
column: index,
|
|
483
|
+
alone,
|
|
484
|
+
name: entryName(entry),
|
|
485
|
+
status: entryStatus(entry),
|
|
486
|
+
movable: movable(entry),
|
|
487
|
+
hintId,
|
|
488
|
+
timeZone,
|
|
489
|
+
continuesFrom: fillLabel(labels.continuesFrom, { time: zonedTimeOfDay(entry.start, timeZone) }),
|
|
490
|
+
markers: entryMarkers(entry),
|
|
491
|
+
renderEntry,
|
|
492
|
+
renderMarker,
|
|
493
|
+
handlers
|
|
494
|
+
},
|
|
495
|
+
`${segment.id}-${segment.part}`
|
|
496
|
+
);
|
|
497
|
+
}),
|
|
498
|
+
ghostParts?.[index].map((segment) => /* @__PURE__ */ jsx(
|
|
499
|
+
"div",
|
|
500
|
+
{
|
|
501
|
+
className: "uix-scheduling-calendar__tg-ghost",
|
|
502
|
+
"aria-hidden": "true",
|
|
503
|
+
"data-sent": ghost?.source === "sent" || void 0,
|
|
504
|
+
style: vars({ "--uix-scheduling-calendar-offset": segment.offsetHours, "--uix-scheduling-calendar-length": segment.lengthHours })
|
|
505
|
+
},
|
|
506
|
+
`ghost-${segment.part}`
|
|
507
|
+
)),
|
|
508
|
+
index === nowColumn && nowAt !== null && /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__tg-now", "aria-hidden": "true", style: vars({ "--uix-scheduling-calendar-offset": (nowAt - column.dayStart) / HOUR_MS }) })
|
|
509
|
+
] }, column.date))
|
|
510
|
+
] }),
|
|
511
|
+
emptyNote != null && emptyNote !== false && nothingToShow && /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__empty", children: emptyNote }),
|
|
512
|
+
moving && /* @__PURE__ */ jsx("p", { id: hintId, className: "uix-visually-hidden", children: labels.moveHint }),
|
|
513
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__tg-probe", "aria-hidden": "true", children: "00:00 abcdef" }),
|
|
514
|
+
/* @__PURE__ */ jsx("div", { className: "uix-visually-hidden", "aria-live": "polite", children: announcement })
|
|
515
|
+
]
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
export {
|
|
520
|
+
SchedulingTimeGrid
|
|
521
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { TimelineRange, TimelineScale } from '../scheduling-timeline-model.js';
|
|
4
|
-
import { SchedulingEntryState } from '
|
|
4
|
+
import { i as SchedulingEntryState } from '../SchedulingCalendar-C4XVbqZr.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);
|