@tensor_1/react 2.34.0 → 2.35.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 CHANGED
@@ -437,6 +437,29 @@ const spanLayout = layoutMonthSpans(spansInSalienceOrder, grid, { timeZone, week
437
437
  - **A narrow month** (`monthDensity="counts"`): a cell is the date, the consumer's `days[date].count` and its markers.
438
438
  No entry is drawn and none is counted: a row's "+N" is for windows over `windowLaneCap` only. The setting does not
439
439
  touch the week.
440
+ - **Keyboard (HAR-1527).** The month grid, the time grid and the agenda are each **one tab stop**; no item is a
441
+ tab stop of its own.
442
+ - Month: arrows move between days, Home/End go to the first/last day shown. **Enter** on a day with items moves
443
+ into them (ArrowUp/ArrowDown, Home, End between them; Enter calls `onSelectEntry`; **Esc** returns to the day).
444
+ **Space** or a click on the day number calls `onSelectDate`; so does Enter on a day with no items.
445
+ - Week/Day time grid: the tab stop is a day head (ArrowLeft/ArrowRight between days). Enter goes into the
446
+ day's items and windows. On an item, Enter selects it, or confirms a pending move (`onProposeMove`); Esc drops
447
+ a pending move, otherwise returns to the day head.
448
+ - Agenda (grouped or flat): ArrowUp/ArrowDown, Home and End walk rows, window notes and "open day", also
449
+ through the rows a long agenda has not mounted yet.
450
+ - The keys act on the calendar's own items only: a field or link you render inside a cell or an item keeps its
451
+ keys. A row's "+N" is reached from the day it opens. An item reached with the pointer takes the tab stop to
452
+ its day. Esc on an item returns to its day and is not passed on; a second Esc reaches a surrounding dialog.
453
+ - A day button is named with the date and `days[date].label` (the count and the highest signal), through
454
+ `labels.dayName` (`'{date}, {label}'`). If your labels already say the date, set `dayName: '{label}'`.
455
+ - To put focus back on an item after closing your own panel: `querySelector('[data-item-id="…"]')?.focus()`.
456
+ - **Emphasis.** `entry.emphasis: 'highlight' | 'dim'` sets `data-highlight` / `data-dim` on that item in every
457
+ view. Highlight is a heavier edge in the text colour and a heavier weight, dim is quieter text;
458
+ neither changes a hue, the line style of the state, or the band's leading edge. With forced colours a
459
+ highlighted title is underlined and dimmed text is grey.
460
+ - **`<List roving>`**: the items are one tab stop (ArrowUp/ArrowDown, Home, End; Enter or Space activates the
461
+ focused item's `onClick`). A control inside an item keeps its own tab stop and keys. Items may be wrapped or
462
+ rendered later by a child component.
440
463
  - **Colour.** Every calendar colour is named once at the top of `scheduling-calendar.css` (`--calendar-*`).
441
464
  - **Deprecated, still working in 2.x:** `entry.state` / `SchedulingEntryState`, `overlay.kind` /
442
465
  `SchedulingOverlayKind` and the `previous` / `next` labels. An entry with `state` is no longer tinted: the two
@@ -97,6 +97,12 @@ interface SchedulingCalendarEntry {
97
97
  allDay?: boolean;
98
98
  /** `false` keeps this entry where it is when the grid allows moves (`canMove`). */
99
99
  movable?: boolean;
100
+ /**
101
+ * Draws the item with more or less weight than its neighbours, for example the partners of
102
+ * the item the user picked (`'highlight'`) against everything else (`'dim'`). It is a line
103
+ * weight and a quieter text colour, never a hue.
104
+ */
105
+ emphasis?: 'highlight' | 'dim';
100
106
  }
101
107
  interface SchedulingCalendarOverlay {
102
108
  id: string;
@@ -123,7 +129,11 @@ interface SchedulingCalendarDay {
123
129
  count: number;
124
130
  /** The "+N" of the cell. 0 shows none. */
125
131
  overflowCount: number;
126
- /** The accessible text of the count, e.g. "7 items, 2 need review". */
132
+ /**
133
+ * The accessible text of the count, e.g. "7 items, 2 need review". The day's button is named
134
+ * with the date and this text (`labels.dayName`), so a screen reader hears the date, the count
135
+ * and the highest signal in one go.
136
+ */
127
137
  label?: string;
128
138
  markers?: SchedulingMarker[];
129
139
  }
@@ -193,6 +203,8 @@ interface SchedulingCalendarLabels {
193
203
  moreSpans?: string;
194
204
  /** Accessible name of that control. `{count}` and `{date}` (the first day with a hidden span). */
195
205
  moreSpansLabel?: string;
206
+ /** The name of a day's button when `days[date].label` is given. `{date}` and `{label}` placeholders; use `'{label}'` if your labels already say the date. */
207
+ dayName?: string;
196
208
  /** The accessible text of a day's count when `days[date].label` is not given. `{count}` placeholder. */
197
209
  dayCount?: string;
198
210
  states: Record<SchedulingEntryState, string>;
@@ -3,9 +3,16 @@ import { HTMLAttributes, ReactNode } from 'react';
3
3
 
4
4
  interface ListProps extends HTMLAttributes<HTMLDivElement> {
5
5
  children?: ReactNode;
6
+ /**
7
+ * Makes the list one tab stop: ArrowUp and ArrowDown move between its `ListItem`s, Home and
8
+ * End go to the first and last, Enter and Space activate the focused item (its `onClick`).
9
+ * Focus stays on the same item across re-renders with the same keys. A roving list is a
10
+ * client component; without `roving` the list stays server-renderable.
11
+ */
12
+ roving?: boolean;
6
13
  }
7
14
  /** Bordered, rounded list container over `.uix-list`. */
8
- declare function List({ children, className, ...props }: ListProps): react.JSX.Element;
15
+ declare function List({ children, className, roving, ...props }: ListProps): react.JSX.Element;
9
16
  interface ListItemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
10
17
  title?: ReactNode;
11
18
  meta?: ReactNode;
@@ -1,6 +1,8 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
- function List({ children, className, ...props }) {
3
+ import { RovingList } from "./ListRoving.js";
4
+ function List({ children, className, roving, ...props }) {
5
+ if (roving) return /* @__PURE__ */ jsx(RovingList, { className, ...props, children });
4
6
  return /* @__PURE__ */ jsx("div", { className: cx("uix-list", className), ...props, children });
5
7
  }
6
8
  function ListItem({ title, meta, trail, leading, children, className, ...props }) {
@@ -0,0 +1,12 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes } from 'react';
3
+
4
+ /**
5
+ * `List` with `roving`: the items of the list are one tab stop. ArrowUp and ArrowDown move
6
+ * between them, Home and End go to the first and last, Enter and Space activate the focused
7
+ * item (its `onClick`). A control inside an item keeps its own tab stop and its own keys.
8
+ * Internal: rendered by `List`, which stays a server component without it.
9
+ */
10
+ declare function RovingList({ children, className, onKeyDown, onFocus, ...props }: HTMLAttributes<HTMLDivElement>): react.JSX.Element;
11
+
12
+ export { RovingList };
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { syncRovingStop } from "../roving.js";
6
+ function RovingList({ children, className, onKeyDown, onFocus, ...props }) {
7
+ const ref = useRef(null);
8
+ const stop = useRef({ node: null, index: 0 });
9
+ const items = () => Array.from(ref.current?.querySelectorAll(".uix-list__item") ?? []).filter((element) => element.closest(".uix-list") === ref.current);
10
+ const sync = (next) => {
11
+ const all = items();
12
+ for (const element of all) if (!element.hasAttribute("role")) element.setAttribute("role", "listitem");
13
+ stop.current = syncRovingStop(all, stop.current, next);
14
+ };
15
+ useEffect(() => {
16
+ sync();
17
+ });
18
+ useEffect(() => {
19
+ const list = ref.current;
20
+ if (!list || typeof MutationObserver === "undefined") return;
21
+ const observer = new MutationObserver(() => sync());
22
+ observer.observe(list, { childList: true, subtree: true });
23
+ return () => observer.disconnect();
24
+ }, []);
25
+ const handleKeyDown = (event) => {
26
+ onKeyDown?.(event);
27
+ if (event.defaultPrevented) return;
28
+ const all = items();
29
+ const index = all.indexOf(event.target);
30
+ if (index === -1) return;
31
+ const target = event.key === "ArrowDown" ? index + 1 : event.key === "ArrowUp" ? index - 1 : event.key === "Home" ? 0 : event.key === "End" ? all.length - 1 : null;
32
+ if (target !== null) {
33
+ event.preventDefault();
34
+ const next = all[Math.max(0, Math.min(all.length - 1, target))];
35
+ sync(next);
36
+ next.focus();
37
+ } else if (event.key === "Enter" || event.key === " ") {
38
+ event.preventDefault();
39
+ all[index].click();
40
+ }
41
+ };
42
+ const handleFocus = (event) => {
43
+ onFocus?.(event);
44
+ const item = items().find((element) => element === event.target);
45
+ if (item) sync(item);
46
+ };
47
+ return /* @__PURE__ */ jsx("div", { ref, role: "list", ...props, className: cx("uix-list", "uix-list--roving", className), onKeyDown: handleKeyDown, onFocus: handleFocus, children });
48
+ }
49
+ export {
50
+ RovingList
51
+ };
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { S as SchedulingAgendaGroup, e as SchedulingCalendarLabels, i as SchedulingDatePart, d as SchedulingCalendarEntry, l as SchedulingMarker, f as SchedulingCalendarOverlay } from '../SchedulingCalendar-DSLQ6AY5.js';
3
+ import { S as SchedulingAgendaGroup, e as SchedulingCalendarLabels, i as SchedulingDatePart, d as SchedulingCalendarEntry, l as SchedulingMarker, f as SchedulingCalendarOverlay } from '../SchedulingCalendar-B6TakO2h.js';
4
4
  import '../calendar-model.js';
5
5
  import '../scheduling-calendar-model.js';
6
6
 
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { createElement, useLayoutEffect, useRef, useState } from "react";
3
+ import { createElement, useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  import { addCalendarDays, zonedDateKey, zonedTimeOfDay } from "../calendar-model.js";
5
5
  import { itemDaySpan } from "../scheduling-calendar-model.js";
6
6
  import { useVirtualRows } from "../hooks/useVirtualRows.js";
7
7
  import { fillLabel } from "../fill-label.js";
8
+ import { syncRovingStop } from "../roving.js";
8
9
  const AGENDA_VIRTUAL_ROW = 44;
9
10
  const AGENDA_NARROW_WIDTH = 576;
10
11
  const AGENDA_NARROW_ROW = 64;
@@ -81,6 +82,102 @@ function AgendaBody({
81
82
  box.querySelectorAll(":scope > section > .uix-scheduling-calendar__agenda-head").forEach((head) => observer.observe(head));
82
83
  return () => observer.disconnect();
83
84
  }, [groups]);
85
+ const stop = useRef({ node: null, index: 0 });
86
+ const container = () => flat ? scroller.current : grouped.current;
87
+ const controls = () => Array.from(container()?.querySelectorAll("button") ?? []);
88
+ const focusStop = (target) => {
89
+ if (!target) return;
90
+ stop.current = syncRovingStop(controls(), stop.current, target);
91
+ target.focus();
92
+ };
93
+ const holdsControl = (row2) => row2 !== void 0 && (row2.kind === "entry" || row2.kind === "note" || row2.kind === "hidden" && onShowMore !== void 0);
94
+ const controlAt = (index) => scroller.current?.querySelector(`[data-flat-index="${index}"] button`) ?? null;
95
+ const focusIn = (box) => {
96
+ const tree = box.getRootNode();
97
+ const active = tree.activeElement ?? null;
98
+ return active === document.body ? null : active;
99
+ };
100
+ const focusIsElsewhere = (box) => {
101
+ const active = focusIn(box);
102
+ if (active) return !box.contains(active);
103
+ const outer = document.activeElement;
104
+ return box.getRootNode() !== document && outer !== null && outer !== document.body && !outer.contains(box);
105
+ };
106
+ const rowHeight = narrow ? AGENDA_NARROW_ROW : AGENDA_VIRTUAL_ROW;
107
+ const scrollToRow = (box, index) => {
108
+ box.scrollTop = Math.max(0, index * rowHeight - (box.clientHeight - rowHeight) / 2);
109
+ };
110
+ const landing = useRef(null);
111
+ const land = () => {
112
+ const wanted = landing.current;
113
+ if (!wanted) return;
114
+ const box = container();
115
+ const index = flatRows[wanted.index]?.key === wanted.key ? wanted.index : flatRows.findIndex((row2) => row2.key === wanted.key);
116
+ if (!box || index === -1 || !holdsControl(flatRows[index]) || focusIsElsewhere(box)) {
117
+ landing.current = null;
118
+ return;
119
+ }
120
+ const target = controlAt(index);
121
+ if (target) {
122
+ landing.current = null;
123
+ focusStop(target);
124
+ return;
125
+ }
126
+ if (index !== wanted.index) {
127
+ landing.current = { key: wanted.key, index };
128
+ scrollToRow(box, index);
129
+ }
130
+ };
131
+ useEffect(() => {
132
+ const all = controls();
133
+ stop.current = syncRovingStop(all, stop.current);
134
+ const box = flat ? scroller.current : null;
135
+ if (box) {
136
+ if (all.length === 0 || focusIn(box) === box) box.tabIndex = 0;
137
+ else box.removeAttribute("tabindex");
138
+ }
139
+ land();
140
+ });
141
+ const onFocus = (event) => {
142
+ stop.current = syncRovingStop(controls(), stop.current, event.target);
143
+ };
144
+ const onBlur = (event) => {
145
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) landing.current = null;
146
+ if (event.target === event.currentTarget && controls().length > 0) event.currentTarget.removeAttribute("tabindex");
147
+ };
148
+ const dropLanding = () => {
149
+ landing.current = null;
150
+ };
151
+ const onKeyDown = (event) => {
152
+ const from = event.target;
153
+ if (event.defaultPrevented || !["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key) || !controls().includes(from)) return;
154
+ event.preventDefault();
155
+ const box = flat ? scroller.current : null;
156
+ if (!box) {
157
+ const all = controls();
158
+ const to = event.key === "Home" ? 0 : event.key === "End" ? all.length - 1 : all.indexOf(from) + (event.key === "ArrowDown" ? 1 : -1);
159
+ focusStop(all[Math.max(0, Math.min(all.length - 1, to))]);
160
+ return;
161
+ }
162
+ const at = Number(from.closest("[data-flat-index]")?.getAttribute("data-flat-index"));
163
+ let target = -1;
164
+ if (event.key === "Home") target = flatRows.findIndex(holdsControl);
165
+ else if (event.key === "End") {
166
+ for (let index = flatRows.length - 1; index >= 0 && target === -1; index--) if (holdsControl(flatRows[index])) target = index;
167
+ } else if (Number.isFinite(at)) {
168
+ const step = event.key === "ArrowDown" ? 1 : -1;
169
+ for (let index = at + step; index >= 0 && index < flatRows.length && target === -1; index += step) if (holdsControl(flatRows[index])) target = index;
170
+ }
171
+ if (target === -1) return;
172
+ const mounted = controlAt(target);
173
+ if (mounted) {
174
+ landing.current = null;
175
+ focusStop(mounted);
176
+ return;
177
+ }
178
+ landing.current = { key: flatRows[target].key, index: target };
179
+ scrollToRow(box, target);
180
+ };
84
181
  const note = (overlay) => /* @__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, onClick: () => onSelectOverlay?.(overlay), "aria-label": overlayName(overlay), children: /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__window-text", children: [
85
182
  overlay.kindLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-kind", children: overlay.kindLabel }),
86
183
  /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-name", children: overlay.label }),
@@ -125,7 +222,7 @@ function AgendaBody({
125
222
  const row = (entry, date) => {
126
223
  const markers = entryMarkers(entry);
127
224
  const name = entry.accessibleName ?? (entry.meta ? `${entryName(entry)}, ${entry.meta}` : entryName(entry));
128
- return /* @__PURE__ */ jsxs("button", { type: "button", className: "uix-scheduling-calendar__agenda-row", "data-item-id": entry.id, "data-band": entry.band ?? "none", "data-status": entryStatus(entry), "data-state": entry.state, onClick: () => onSelectEntry?.(entry), "aria-label": name, children: [
225
+ return /* @__PURE__ */ jsxs("button", { type: "button", className: "uix-scheduling-calendar__agenda-row", "data-item-id": entry.id, "data-highlight": entry.emphasis === "highlight" || void 0, "data-dim": entry.emphasis === "dim" || void 0, "data-band": entry.band ?? "none", "data-status": entryStatus(entry), "data-state": entry.state, onClick: () => onSelectEntry?.(entry), "aria-label": name, children: [
129
226
  /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__swatch", "aria-hidden": "true", "data-band": entry.band ?? "none", "data-status": entryStatus(entry) }),
130
227
  /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__agenda-time", children: timeText(entry, date) }),
131
228
  /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__agenda-main", children: [
@@ -143,9 +240,9 @@ function AgendaBody({
143
240
  const continues = (group) => /* @__PURE__ */ jsx("p", { className: "uix-scheduling-calendar__agenda-continues", children: fillLabel(labels.continuesInDay, { count: group.continuesCount ?? 0 }) });
144
241
  if (groups.length === 0) return /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__agenda", role: "region", "aria-label": labels.agenda, children: /* @__PURE__ */ jsx("p", { children: labels.agendaEmpty }) });
145
242
  if (flat) {
146
- return /* @__PURE__ */ jsx("div", { ref: virtual.containerRef, className: "uix-scheduling-calendar__agenda uix-scheduling-calendar__agenda--virtual", "data-narrow": narrow || void 0, role: "region", "aria-label": labels.agenda, children: /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__agenda-window", style: { paddingTop: virtual.padTop, paddingBottom: virtual.padBottom }, children: virtual.rows.map((item) => /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__agenda-vrow", "data-kind": item.kind, children: item.kind === "heading" ? heading(item.group, false) : item.kind === "note" ? note(item.overlay) : item.kind === "entry" ? row(item.entry, item.group.date) : item.kind === "hidden" ? hidden(item.group) : continues(item.group) }, item.key)) }) });
243
+ return /* @__PURE__ */ jsx("div", { ref: virtual.containerRef, className: "uix-scheduling-calendar__agenda uix-scheduling-calendar__agenda--virtual", "data-narrow": narrow || void 0, role: "region", "aria-label": labels.agenda, onKeyDown, onFocus, onBlur, onWheel: dropLanding, onPointerDown: dropLanding, children: /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__agenda-window", style: { paddingTop: virtual.padTop, paddingBottom: virtual.padBottom }, children: virtual.rows.map((item, at) => /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__agenda-vrow", "data-kind": item.kind, "data-flat-index": virtual.startIndex + at, children: item.kind === "heading" ? heading(item.group, false) : item.kind === "note" ? note(item.overlay) : item.kind === "entry" ? row(item.entry, item.group.date) : item.kind === "hidden" ? hidden(item.group) : continues(item.group) }, item.key)) }) });
147
244
  }
148
- return /* @__PURE__ */ jsx("div", { ref: grouped, className: "uix-scheduling-calendar__agenda", role: "region", "aria-label": labels.agenda, children: groups.map((group, index) => {
245
+ return /* @__PURE__ */ jsx("div", { ref: grouped, className: "uix-scheduling-calendar__agenda", role: "region", "aria-label": labels.agenda, onKeyDown, onFocus, children: groups.map((group, index) => {
149
246
  const rowKeys = uniqueKeys(group.rows.map((entry) => entry.id));
150
247
  return /* @__PURE__ */ jsxs("section", { className: "uix-scheduling-calendar__agenda-group", "data-date": group.date, children: [
151
248
  heading(group, true),
@@ -1,4 +1,4 @@
1
1
  import 'react';
2
2
  import '../calendar-model.js';
3
3
  import '../scheduling-calendar-model.js';
4
- export { D as DEFAULT_SCHEDULING_CALENDAR_LABELS, S as SchedulingAgendaGroup, a as SchedulingBand, b as SchedulingCalendar, c as SchedulingCalendarDay, d as SchedulingCalendarEntry, e as SchedulingCalendarLabels, f as SchedulingCalendarOverlay, g as SchedulingCalendarProps, h as SchedulingCalendarView, i as SchedulingDatePart, j as SchedulingEntryState, k as SchedulingLegendItem, l as SchedulingMarker, m as SchedulingMarkerEmphasis, n as SchedulingOverlayKind, o as SchedulingOverlayPattern, p as SchedulingStatus } from '../SchedulingCalendar-DSLQ6AY5.js';
4
+ export { D as DEFAULT_SCHEDULING_CALENDAR_LABELS, S as SchedulingAgendaGroup, a as SchedulingBand, b as SchedulingCalendar, c as SchedulingCalendarDay, d as SchedulingCalendarEntry, e as SchedulingCalendarLabels, f as SchedulingCalendarOverlay, g as SchedulingCalendarProps, h as SchedulingCalendarView, i as SchedulingDatePart, j as SchedulingEntryState, k as SchedulingLegendItem, l as SchedulingMarker, m as SchedulingMarkerEmphasis, n as SchedulingOverlayKind, o as SchedulingOverlayPattern, p as SchedulingStatus } from '../SchedulingCalendar-B6TakO2h.js';
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { rovingStep, syncRovingStop } from "../roving.js";
4
5
  import { addCalendarDays, addCalendarMonths, buildMonthGrid, cachedDateTimeFormat, startOfMonth, zonedDateKey, zonedTimeOfDay } from "../calendar-model.js";
5
6
  import { TOP_LANE_CROSS_MIDNIGHT_MINUTES, itemDaySpan, layoutMonthSpans } from "../scheduling-calendar-model.js";
6
7
  import { SchedulingAgenda } from "./SchedulingAgenda.js";
@@ -37,6 +38,7 @@ const DEFAULT_SCHEDULING_CALENDAR_LABELS = {
37
38
  fewerEntries: "Show fewer",
38
39
  moreSpans: "+{count}",
39
40
  moreSpansLabel: "{count} more spanning this week, from {date}",
41
+ dayName: "{date}, {label}",
40
42
  dayCount: "{count} entries",
41
43
  states: { scheduled: "Scheduled", conflicted: "Conflicted", "in-progress": "In progress", "blackout-violation": "Blackout violation" },
42
44
  statuses: { tentative: "Tentative", committed: "Scheduled", live: "In progress", done: "Done", dead: "Cancelled" },
@@ -222,6 +224,31 @@ function SchedulingCalendar({
222
224
  setActiveDate(date);
223
225
  requestAnimationFrame(() => gridRef.current?.querySelector(`[data-calendar-date="${date}"]`)?.focus());
224
226
  };
227
+ const enteredFrom = useRef(null);
228
+ const cellOf = (date) => gridRef.current?.querySelector(`[data-calendar-date="${date}"]`)?.closest(".uix-scheduling-calendar__day") ?? null;
229
+ const dayRing = (date) => {
230
+ const cell = cellOf(date);
231
+ const week = cell?.parentElement;
232
+ if (!cell || !week) return [];
233
+ const column = Array.from(week.children).indexOf(cell) + 1;
234
+ const covers = (element) => {
235
+ const [from, to] = element.style.gridColumn.split("/").map((part) => Number(part.trim()));
236
+ return from !== void 0 && to !== void 0 && from <= column && column < to;
237
+ };
238
+ const direct = Array.from(week.children);
239
+ return [
240
+ ...direct.filter((element) => element.matches("button.uix-scheduling-calendar__window, button.uix-scheduling-calendar__span") && covers(element)),
241
+ ...Array.from(cell.querySelectorAll(".uix-scheduling-calendar__entries button")),
242
+ // The row's "+N" opens one day; it is reached from that day.
243
+ ...direct.filter((element) => element.matches("button.uix-scheduling-calendar__rowmore") && element.getAttribute("data-more-date") === date)
244
+ ];
245
+ };
246
+ const dayOfItem = (item) => {
247
+ if (enteredFrom.current && dayRing(enteredFrom.current).includes(item)) return enteredFrom.current;
248
+ const week = item.closest(".uix-scheduling-calendar__week");
249
+ const cell = item.closest(".uix-scheduling-calendar__day") ?? (week ? Array.from(week.children)[Number(item.style.gridColumn.split("/")[0]) - 1] ?? null : null);
250
+ return item.getAttribute("data-more-date") ?? cell?.querySelector("[data-calendar-date]")?.getAttribute("data-calendar-date") ?? null;
251
+ };
225
252
  const onDayKeyDown = (event, date) => {
226
253
  const offset = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[event.key];
227
254
  if (offset) {
@@ -233,7 +260,56 @@ function SchedulingCalendar({
233
260
  } else if (event.key === "End") {
234
261
  event.preventDefault();
235
262
  focusDate(days[days.length - 1]);
263
+ } else if (event.key === "Enter") {
264
+ const [first] = dayRing(date);
265
+ if (!first) return;
266
+ event.preventDefault();
267
+ enteredFrom.current = date;
268
+ first.focus();
269
+ }
270
+ };
271
+ const onGridKeyDown = (event) => {
272
+ const item = event.target;
273
+ if (event.defaultPrevented || item.hasAttribute("data-calendar-date") || !["ArrowDown", "ArrowUp", "Home", "End", "Escape"].includes(event.key)) return;
274
+ const date = dayOfItem(item);
275
+ if (!date) return;
276
+ const ring = dayRing(date);
277
+ const index = ring.indexOf(item);
278
+ if (index === -1) return;
279
+ event.preventDefault();
280
+ if (event.key === "Escape") {
281
+ event.stopPropagation();
282
+ enteredFrom.current = null;
283
+ setActiveDate(date);
284
+ gridRef.current?.querySelector(`[data-calendar-date="${date}"]`)?.focus();
285
+ return;
236
286
  }
287
+ enteredFrom.current = date;
288
+ const next = event.key === "Home" ? 0 : event.key === "End" ? ring.length - 1 : index + (event.key === "ArrowDown" ? 1 : -1);
289
+ ring[Math.max(0, Math.min(ring.length - 1, next))]?.focus();
290
+ };
291
+ const onGridFocus = (event) => {
292
+ const item = event.target;
293
+ if (item.hasAttribute("data-calendar-date")) return;
294
+ const date = dayOfItem(item);
295
+ if (date && dayRing(date).includes(item)) setActiveDate(date);
296
+ };
297
+ const flatAgendaRef = useRef(null);
298
+ const flatAgendaStop = useRef({ node: null, index: 0 });
299
+ const flatAgendaRows = () => Array.from(flatAgendaRef.current?.querySelectorAll(":scope > ol > li > button") ?? []);
300
+ useEffect(() => {
301
+ flatAgendaStop.current = syncRovingStop(flatAgendaRows(), flatAgendaStop.current);
302
+ });
303
+ const onFlatAgendaFocus = (event) => {
304
+ flatAgendaStop.current = syncRovingStop(flatAgendaRows(), flatAgendaStop.current, event.target);
305
+ };
306
+ const onFlatAgendaKeyDown = (event) => {
307
+ if (event.defaultPrevented) return;
308
+ const next = rovingStep(flatAgendaRows(), event.target, event.key);
309
+ if (!next) return;
310
+ event.preventDefault();
311
+ flatAgendaStop.current = syncRovingStop(flatAgendaRows(), flatAgendaStop.current, next);
312
+ next.focus();
237
313
  };
238
314
  const entriesOn = (date) => coverage.filter((covered) => date >= covered.start && date < covered.end).map((covered) => covered.entry);
239
315
  const toggleMore = (date, all) => {
@@ -256,12 +332,16 @@ function SchedulingCalendar({
256
332
  const stateText = (entry) => entry.state ? stateLabel[entry.state] : entry.status ? labels.statuses[entry.status] : stateLabel.scheduled;
257
333
  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(", ");
258
334
  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) });
335
+ const emphasisOf = (entry) => ({ "data-highlight": entry.emphasis === "highlight" || void 0, "data-dim": entry.emphasis === "dim" || void 0 });
259
336
  const entryProps = (entry) => ({
260
337
  type: "button",
338
+ // Reached from its day with Enter, never with Tab.
339
+ tabIndex: -1,
261
340
  "data-item-id": entry.id,
262
341
  "data-band": entry.band ?? "none",
263
342
  "data-status": statusOf(entry),
264
343
  "data-state": entry.state,
344
+ ...emphasisOf(entry),
265
345
  onClick: () => onSelectEntry?.(entry),
266
346
  "aria-label": entryName(entry)
267
347
  });
@@ -294,13 +374,13 @@ function SchedulingCalendar({
294
374
  const onDay = picksGiven ? all : entriesOn(date);
295
375
  const badge = renderDayBadge?.(date, onDay);
296
376
  const hasBadge = badge != null && badge !== false;
297
- 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)) });
377
+ 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": info?.label ? fillLabel(labels.dayName, { date: dateText(date, "day"), label: info.label }) : dateText(date, "day"), children: Number(date.slice(-2)) });
298
378
  const moreText = fillLabel(labels.moreEntries, { count: hiddenCount });
299
379
  const moreName = fillLabel(labels.moreEntriesLabel, { count: hiddenCount, date: dateText(date, "day") });
300
380
  return /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__day", "data-outside": view === "month" && !monthDays[index].inMonth || void 0, children: [
301
381
  hasBadge || info ? /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__dayhead", children: [
302
382
  hasBadge && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__badge", children: badge }),
303
- info && /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__count", children: [
383
+ info && /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__count", "aria-hidden": info.label ? true : void 0, children: [
304
384
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: info.count }),
305
385
  /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: info.label ?? fillLabel(labels.dayCount, { count: info.count }) })
306
386
  ] }),
@@ -309,10 +389,10 @@ function SchedulingCalendar({
309
389
  ] }) : dateButton,
310
390
  /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-calendar__entries", children: [
311
391
  shown.map((entry) => /* @__PURE__ */ jsx("button", { className: "uix-scheduling-calendar__entry", ...entryProps(entry), children: entryContent(entry) }, entry.id)),
312
- countsOnly ? null : 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: [
392
+ countsOnly ? null : controlled ? hiddenCount > 0 && (onShowMore ? /* @__PURE__ */ jsx("button", { type: "button", tabIndex: -1, className: "uix-scheduling-calendar__more", "aria-label": moreName, onClick: () => onShowMore(date, onDay), children: moreText }) : /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__more", children: [
313
393
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: moreText }),
314
394
  /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: moreName })
315
- ] })) : 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 })
395
+ ] })) : overflowing && /* @__PURE__ */ jsx("button", { type: "button", tabIndex: -1, 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 })
316
396
  ] })
317
397
  ] }, date);
318
398
  };
@@ -334,7 +414,7 @@ function SchedulingCalendar({
334
414
  if (placed.group === "window") {
335
415
  const overlay = overlayById.get(placed.id);
336
416
  if (!overlay) return null;
337
- 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: [
417
+ return /* @__PURE__ */ jsx("button", { type: "button", tabIndex: -1, 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: [
338
418
  overlay.kindLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-kind", children: overlay.kindLabel }),
339
419
  /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-name", children: overlay.label }),
340
420
  overlay.scopeLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__window-scope", children: overlay.scopeLabel })
@@ -344,7 +424,7 @@ function SchedulingCalendar({
344
424
  if (!entry || countsOnly) return null;
345
425
  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}`);
346
426
  }),
347
- 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: [
427
+ hidden > 0 && (onShowMore ? /* @__PURE__ */ jsx("button", { type: "button", tabIndex: -1, className: "uix-scheduling-calendar__rowmore", "data-more-date": rowDays.includes(firstHiddenDay) ? firstHiddenDay : rowDays[0], "aria-label": moreName, onClick: () => onShowMore(firstHiddenDay, picksGiven ? dayEntries[firstHiddenDay] ?? EMPTY_ENTRIES : entriesOn(firstHiddenDay)), children: moreText }) : /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-calendar__rowmore", children: [
348
428
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: moreText }),
349
429
  /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: moreName })
350
430
  ] }))
@@ -391,9 +471,9 @@ function SchedulingCalendar({
391
471
  onSelectOverlay,
392
472
  onShowMore
393
473
  }
394
- ) : 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) => {
474
+ ) : view === "agenda" ? /* @__PURE__ */ jsx("div", { ref: flatAgendaRef, className: "uix-scheduling-calendar__agenda", role: "region", "aria-label": labels.agenda, onKeyDown: onFlatAgendaKeyDown, onFocus: onFlatAgendaFocus, 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) => {
395
475
  const markers = markersOf(entry);
396
- 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: [
476
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", { type: "button", "data-item-id": entry.id, "data-highlight": entry.emphasis === "highlight" || void 0, "data-dim": entry.emphasis === "dim" || void 0, "data-band": entry.band ?? "none", "data-status": statusOf(entry), "data-state": entry.state, onClick: () => onSelectEntry?.(entry), children: [
397
477
  /* @__PURE__ */ jsxs("span", { children: [
398
478
  /* @__PURE__ */ jsx("strong", { children: renderEntry?.(entry) ?? entry.title }),
399
479
  /* @__PURE__ */ jsxs("span", { children: [
@@ -440,7 +520,7 @@ function SchedulingCalendar({
440
520
  emptyNote,
441
521
  nothingToShow
442
522
  }
443
- ) : /* @__PURE__ */ jsxs("div", { ref: gridRef, className: cx("uix-scheduling-calendar__grid", view === "week" && "uix-scheduling-calendar__grid--week"), "data-fixed": fixed || void 0, "data-density": countsOnly ? "counts" : void 0, style: gridStyle, role: "group", "aria-label": fillLabel(labels.grid, { view: viewLabel[view] }), children: [
523
+ ) : /* @__PURE__ */ jsxs("div", { ref: gridRef, className: cx("uix-scheduling-calendar__grid", view === "week" && "uix-scheduling-calendar__grid--week"), "data-fixed": fixed || void 0, "data-density": countsOnly ? "counts" : void 0, style: gridStyle, onKeyDown: onGridKeyDown, onFocus: onGridFocus, role: "group", "aria-label": fillLabel(labels.grid, { view: viewLabel[view] }), children: [
444
524
  days.slice(0, 7).map((date) => /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__weekday", "aria-hidden": "true", children: dateText(date, "weekday") }, `weekday-${date}`)),
445
525
  Array.from({ length: days.length / 7 }, (_, row) => renderWeek(row)),
446
526
  emptyNote != null && emptyNote !== false && nothingToShow && /* @__PURE__ */ jsx("div", { className: "uix-scheduling-calendar__empty", children: emptyNote })
@@ -1,4 +1,4 @@
1
1
  import 'react';
2
2
  import '../scheduling-calendar-model.js';
3
- export { q as SchedulingTimeGrid, r as SchedulingTimeGridProps, T as TimeGridEntryContext } from '../SchedulingCalendar-DSLQ6AY5.js';
3
+ export { q as SchedulingTimeGrid, r as SchedulingTimeGridProps, T as TimeGridEntryContext } from '../SchedulingCalendar-B6TakO2h.js';
4
4
  import '../calendar-model.js';