@tensor_1/react 2.34.0 → 2.36.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.
Files changed (48) hide show
  1. package/README.md +164 -15
  2. package/dist/{SchedulingCalendar-DSLQ6AY5.d.ts → SchedulingCalendar-B6TakO2h.d.ts} +13 -1
  3. package/dist/components/Avatar.d.ts +26 -4
  4. package/dist/components/Avatar.js +26 -3
  5. package/dist/components/CommandPalette.d.ts +6 -1
  6. package/dist/components/CommandPalette.js +9 -3
  7. package/dist/components/DateFieldParts.d.ts +61 -0
  8. package/dist/components/DateFieldParts.js +195 -0
  9. package/dist/components/DatePicker.d.ts +129 -0
  10. package/dist/components/DatePicker.js +340 -0
  11. package/dist/components/DateRangePicker.d.ts +34 -3
  12. package/dist/components/DateRangePicker.js +120 -17
  13. package/dist/components/EntityPicker.d.ts +29 -4
  14. package/dist/components/EntityPicker.js +20 -8
  15. package/dist/components/InlineEdit.d.ts +74 -0
  16. package/dist/components/InlineEdit.js +176 -0
  17. package/dist/components/List.d.ts +8 -1
  18. package/dist/components/List.js +3 -1
  19. package/dist/components/ListRoving.d.ts +12 -0
  20. package/dist/components/ListRoving.js +51 -0
  21. package/dist/components/Radio.d.ts +23 -2
  22. package/dist/components/Radio.js +18 -3
  23. package/dist/components/SchedulingAgenda.d.ts +1 -1
  24. package/dist/components/SchedulingAgenda.js +101 -4
  25. package/dist/components/SchedulingCalendar.d.ts +1 -1
  26. package/dist/components/SchedulingCalendar.js +89 -9
  27. package/dist/components/SchedulingTimeGrid.d.ts +1 -1
  28. package/dist/components/SchedulingTimeGrid.js +101 -4
  29. package/dist/components/SchedulingTimeline.d.ts +130 -15
  30. package/dist/components/SchedulingTimeline.js +765 -141
  31. package/dist/components/SearchSuggest.d.ts +12 -1
  32. package/dist/components/SearchSuggest.js +7 -2
  33. package/dist/components/Tree.d.ts +30 -2
  34. package/dist/components/Tree.js +135 -10
  35. package/dist/date-field-model.d.ts +40 -0
  36. package/dist/date-field-model.js +79 -0
  37. package/dist/index.cjs +4179 -2056
  38. package/dist/index.d.cts +829 -264
  39. package/dist/index.d.ts +10 -7
  40. package/dist/index.js +40 -4
  41. package/dist/labels-context.d.ts +15 -2
  42. package/dist/roving.d.ts +15 -0
  43. package/dist/roving.js +17 -0
  44. package/dist/scheduling-timeline-model.d.ts +61 -11
  45. package/dist/scheduling-timeline-model.js +85 -26
  46. package/dist/scheduling-timeline-rows.d.ts +40 -0
  47. package/dist/scheduling-timeline-rows.js +42 -0
  48. package/package.json +5 -3
@@ -1,5 +1,7 @@
1
1
  import * as react from 'react';
2
- import { DateRangeValue } from '../calendar-model.js';
2
+ import { AriaAttributes } from 'react';
3
+ import { DateRangeValue, CalendarWeekday } from '../calendar-model.js';
4
+ import { Placement } from '../overlay-position.js';
3
5
 
4
6
  /**
5
7
  * Every word the picker renders or announces (TENSOR RX-125, UIX-13). `{start}` and
@@ -16,6 +18,12 @@ interface DateRangePickerLabels {
16
18
  rangeStart: string;
17
19
  rangeEnd: string;
18
20
  inRange: string;
21
+ /** Field mode: what the trigger shows while no range is set. */
22
+ placeholder: string;
23
+ /** Field mode: the trigger's text for a start without an end; `{start}`. */
24
+ fieldStart: string;
25
+ /** Field mode: the trigger's text for a range; `{start}`, `{end}`. */
26
+ fieldRange: string;
19
27
  }
20
28
  declare const DEFAULT_DATE_RANGE_PICKER_LABELS: DateRangePickerLabels;
21
29
  interface DateRangePickerProps {
@@ -41,8 +49,31 @@ interface DateRangePickerProps {
41
49
  * clock and zone may differ from the viewer's), or `null` for no today marker.
42
50
  */
43
51
  today?: string | null;
52
+ /** First column of each month, in `Date.getUTCDay()` numbering (0 = Sunday). Default 1, Monday. */
53
+ weekStartsOn?: CalendarWeekday;
54
+ /**
55
+ * `'inline'` (default) draws the months in the page. `'field'` draws a form control that
56
+ * shows the range and opens the same months in a popover (HAR-1383): for forms and filter
57
+ * bars. It closes once the range has both ends; Escape closes it and returns focus.
58
+ */
59
+ mode?: 'inline' | 'field';
60
+ /** Field mode: how the trigger writes a date. Default: the locale's numeric date. */
61
+ formatDate?: (date: string) => string;
62
+ /** Field mode: lands on the trigger, so a `<label htmlFor>` or `<Field>` names it. */
63
+ id?: string;
64
+ /** Field mode. */
65
+ size?: 'sm' | 'md';
66
+ 'aria-describedby'?: string;
67
+ 'aria-invalid'?: AriaAttributes['aria-invalid'];
68
+ /** Field mode: where the popover opens. Default `'bottom-start'`. */
69
+ placement?: Placement;
70
+ /** Field mode. */
71
+ onOpenChange?: (open: boolean) => void;
44
72
  }
45
- /** Controlled, two-month range picker using the UIx calendar language. */
46
- declare function DateRangePicker({ value, onChange, visibleMonth: controlledMonth, onVisibleMonthChange, months, min, max, isDateDisabled, locale, label, invalid, invalidMessage, disabled, className, labels: labelOverrides, today, }: DateRangePickerProps): react.JSX.Element;
73
+ /**
74
+ * Controlled date-range picker using the UIx calendar language: two months inline, or with
75
+ * `mode="field"` a form control that opens them in a popover.
76
+ */
77
+ declare function DateRangePicker({ labels: labelOverrides, ...props }: DateRangePickerProps): react.JSX.Element;
47
78
 
48
79
  export { DEFAULT_DATE_RANGE_PICKER_LABELS, DateRangePicker, type DateRangePickerLabels, type DateRangePickerProps };
@@ -3,7 +3,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useId, useMemo, useRef, useState } from "react";
4
4
  import { addCalendarDays, addCalendarMonths, buildMonthGrid, isDateInRange, isDateUnavailable, selectRangeDate, startOfMonth } from "../calendar-model.js";
5
5
  import { cx } from "../cx.js";
6
+ import { formatDateKey } from "../date-field-model.js";
7
+ import { CalendarIcon } from "../icons/components.js";
6
8
  import { useUixLabels } from "../labels-context.js";
9
+ import { localDateKey, useFieldPopover, weekdayNames } from "./DateFieldParts.js";
10
+ import { Popover } from "./Popover.js";
7
11
  const DEFAULT_DATE_RANGE_PICKER_LABELS = {
8
12
  previous: "Previous",
9
13
  previousMonth: "Show previous month",
@@ -14,16 +18,14 @@ const DEFAULT_DATE_RANGE_PICKER_LABELS = {
14
18
  rangeSelected: "{start} to {end} selected.",
15
19
  rangeStart: "start of selected range",
16
20
  rangeEnd: "end of selected range",
17
- inRange: "in selected range"
21
+ inRange: "in selected range",
22
+ placeholder: "Choose dates",
23
+ fieldStart: "{start} \u2013",
24
+ fieldRange: "{start} \u2013 {end}"
18
25
  };
19
- const localDateKey = (now = /* @__PURE__ */ new Date()) => `${now.getFullYear().toString().padStart(4, "0")}-${(now.getMonth() + 1).toString().padStart(2, "0")}-${now.getDate().toString().padStart(2, "0")}`;
20
26
  const monthLabel = (date, locale) => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
21
27
  const dateLabel = (date, locale) => new Intl.DateTimeFormat(locale, { dateStyle: "full", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
22
- const weekdayNames = (locale) => {
23
- const fmt = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" });
24
- return Array.from({ length: 7 }, (_, i) => fmt.format(new Date(Date.UTC(2024, 0, 1 + i))));
25
- };
26
- function DateRangePicker({
28
+ function RangeCalendar({
27
29
  value,
28
30
  onChange,
29
31
  visibleMonth: controlledMonth,
@@ -38,18 +40,18 @@ function DateRangePicker({
38
40
  invalidMessage = "The selected date range is invalid.",
39
41
  disabled,
40
42
  className,
41
- labels: labelOverrides,
42
- today
43
+ words: labels,
44
+ today,
45
+ weekStartsOn = 1,
46
+ autoFocus = false
43
47
  }) {
44
48
  const id = useId();
45
- const uixLabels = useUixLabels();
46
- const labels = { ...DEFAULT_DATE_RANGE_PICKER_LABELS, ...uixLabels.dateRangePicker, ...labelOverrides };
47
- const weekdays = useMemo(() => weekdayNames(locale), [locale]);
49
+ const weekdays = useMemo(() => weekdayNames(locale, weekStartsOn), [locale, weekStartsOn]);
48
50
  const [internalMonth, setInternalMonth] = useState(() => startOfMonth(controlledMonth ?? value.start ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)));
49
51
  const visibleMonth = startOfMonth(controlledMonth ?? internalMonth);
50
52
  const [activeDate, setActiveDate] = useState(value.end ?? value.start ?? visibleMonth);
51
53
  const rootRef = useRef(null);
52
- const pendingFocus = useRef(false);
54
+ const pendingFocus = useRef(autoFocus);
53
55
  const count = Math.max(1, Math.min(3, months));
54
56
  const calendars = useMemo(() => Array.from({ length: count }, (_, index) => {
55
57
  const month = startOfMonth(addCalendarMonths(visibleMonth, index));
@@ -65,7 +67,7 @@ function DateRangePicker({
65
67
  }, [activeDate, calendars]);
66
68
  useEffect(() => {
67
69
  if (!pendingFocus.current) return;
68
- rootRef.current?.querySelector(`[data-date="${activeDate}"]`)?.focus();
70
+ rootRef.current?.querySelector(`[data-date="${activeDate}"]`)?.focus({ preventScroll: autoFocus });
69
71
  pendingFocus.current = false;
70
72
  }, [activeDate, visibleMonth]);
71
73
  const setMonth = (month) => {
@@ -93,8 +95,8 @@ function DateRangePicker({
93
95
  if (event.key === "Home" || event.key === "End") {
94
96
  event.preventDefault();
95
97
  const weekday = (/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay();
96
- const mondayIndex = (weekday + 6) % 7;
97
- focusDate(addCalendarDays(date, event.key === "Home" ? -mondayIndex : 6 - mondayIndex));
98
+ const index = (weekday - weekStartsOn + 7) % 7;
99
+ focusDate(addCalendarDays(date, event.key === "Home" ? -index : 6 - index));
98
100
  } else if (event.key === "PageUp" || event.key === "PageDown") {
99
101
  event.preventDefault();
100
102
  focusDate(addCalendarMonths(date, event.key === "PageUp" ? -1 : 1));
@@ -113,7 +115,7 @@ function DateRangePicker({
113
115
  /* @__PURE__ */ jsx("h4", { children: monthLabel(month, locale) }),
114
116
  /* @__PURE__ */ jsx("div", { className: "uix-date-range-picker__weekdays", "aria-hidden": "true", children: weekdays.map((day, index) => /* @__PURE__ */ jsx("span", { children: day }, index)) }),
115
117
  /* @__PURE__ */ jsxs("div", { className: "uix-date-range-picker__grid", role: "group", "aria-label": monthLabel(month, locale), children: [
116
- Array.from({ length: ((/* @__PURE__ */ new Date(`${month}T00:00:00Z`)).getUTCDay() + 6) % 7 }, (_, index) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }, `blank-${index}`)),
118
+ Array.from({ length: ((/* @__PURE__ */ new Date(`${month}T00:00:00Z`)).getUTCDay() - weekStartsOn + 7) % 7 }, (_, index) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }, `blank-${index}`)),
117
119
  days.map((day) => {
118
120
  const unavailable = disabled || isDateUnavailable(day.date, { min, max, isDisabled: isDateDisabled });
119
121
  const edge = day.date === value.start ? "start" : day.date === value.end ? "end" : void 0;
@@ -143,6 +145,107 @@ function DateRangePicker({
143
145
  rangeInvalid && /* @__PURE__ */ jsx("p", { className: "uix-field__error", role: "alert", children: invalidMessage })
144
146
  ] });
145
147
  }
148
+ function DateRangePicker({ labels: labelOverrides, ...props }) {
149
+ const uixLabels = useUixLabels();
150
+ const words = { ...DEFAULT_DATE_RANGE_PICKER_LABELS, ...uixLabels.dateRangePicker, ...labelOverrides };
151
+ if (props.mode === "field") return /* @__PURE__ */ jsx(DateRangeField, { ...props, words });
152
+ return /* @__PURE__ */ jsx(RangeCalendar, { ...props, words });
153
+ }
154
+ function DateRangeField(props) {
155
+ const {
156
+ value,
157
+ onChange,
158
+ locale,
159
+ label = "Choose date range",
160
+ invalid,
161
+ disabled,
162
+ className,
163
+ words,
164
+ formatDate,
165
+ id: idProp,
166
+ size = "md",
167
+ placement = "bottom-start",
168
+ onOpenChange,
169
+ "aria-describedby": ariaDescribedBy,
170
+ "aria-invalid": ariaInvalid
171
+ } = props;
172
+ const uid = useId();
173
+ const id = idProp ?? `${uid}-trigger`;
174
+ const popoverId = `${uid}-months`;
175
+ const valueId = `${uid}-value`;
176
+ const anchorRef = useRef(null);
177
+ const triggerRef = useRef(null);
178
+ const { open, hide, triggerProps } = useFieldPopover(popoverId, () => triggerRef.current?.focus({ preventScroll: true }), onOpenChange);
179
+ const [labelled, setLabelled] = useState(false);
180
+ useEffect(() => {
181
+ setLabelled((triggerRef.current?.labels?.length ?? 0) > 0);
182
+ }, [id]);
183
+ const format = (date) => formatDate?.(date) ?? formatDateKey(date, locale);
184
+ const text = !value.start ? null : !value.end ? words.fieldStart.replace("{start}", format(value.start)) : words.fieldRange.replace("{start}", format(value.start)).replace("{end}", format(value.end));
185
+ const rangeInvalid = invalid || !!value.start && !!value.end && value.start > value.end;
186
+ const describedBy = [ariaDescribedBy, labelled && text ? valueId : void 0].filter(Boolean).join(" ") || void 0;
187
+ return /* @__PURE__ */ jsxs(
188
+ "div",
189
+ {
190
+ ref: anchorRef,
191
+ className: cx("uix-date-range-field", className),
192
+ onKeyDown: (event) => {
193
+ if (event.key !== "Escape" || !open) return;
194
+ event.stopPropagation();
195
+ event.preventDefault();
196
+ hide();
197
+ },
198
+ children: [
199
+ /* @__PURE__ */ jsxs(
200
+ "button",
201
+ {
202
+ ref: triggerRef,
203
+ type: "button",
204
+ id,
205
+ disabled,
206
+ className: cx("uix-input", size === "sm" && "uix-input--sm", "uix-date-range-field__trigger"),
207
+ "aria-describedby": describedBy,
208
+ "aria-invalid": rangeInvalid || ariaInvalid === true || ariaInvalid === "true" || void 0,
209
+ ...triggerProps,
210
+ children: [
211
+ /* @__PURE__ */ jsx("span", { className: "uix-date-range-field__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(CalendarIcon, { size: "sm" }) }),
212
+ /* @__PURE__ */ jsxs("span", { className: "uix-visually-hidden", children: [
213
+ label,
214
+ ": "
215
+ ] }),
216
+ /* @__PURE__ */ jsx("span", { id: valueId, className: cx("uix-date-range-field__value", !text && "uix-date-range-field__value--empty"), children: text ?? words.placeholder })
217
+ ]
218
+ }
219
+ ),
220
+ /* @__PURE__ */ jsx(
221
+ Popover,
222
+ {
223
+ id: popoverId,
224
+ anchor: anchorRef,
225
+ placement,
226
+ className: "uix-date-range-field__popover",
227
+ role: "dialog",
228
+ "aria-label": label,
229
+ capHeight: true,
230
+ closeWhenAnchorHidden: true,
231
+ children: open && /* @__PURE__ */ jsx(
232
+ RangeCalendar,
233
+ {
234
+ ...props,
235
+ className: "uix-date-range-picker--in-popover",
236
+ autoFocus: true,
237
+ onChange: (next) => {
238
+ onChange(next);
239
+ if (next.start && next.end) hide();
240
+ }
241
+ }
242
+ )
243
+ }
244
+ )
245
+ ]
246
+ }
247
+ );
248
+ }
146
249
  export {
147
250
  DEFAULT_DATE_RANGE_PICKER_LABELS,
148
251
  DateRangePicker
@@ -17,14 +17,35 @@ interface EntityPickerLabels {
17
17
  none: string;
18
18
  /** Polite count after a search. `{count}`. */
19
19
  results: string;
20
+ /**
21
+ * The last line of the list when the search found more than it returned (HAR-1648). Not an
22
+ * option: it cannot be chosen.
23
+ */
24
+ more: string;
25
+ /** Polite count when there are more matches than shown. `{count}` is the number shown. */
26
+ resultsMore: string;
27
+ /** Shown in the open list while the query is shorter than `minQueryLength`. `{count}`. */
28
+ tooShort: string;
29
+ /** The same when `minQueryLength` is 1. */
30
+ typeToSearch: string;
20
31
  }
21
32
  declare const DEFAULT_ENTITY_PICKER_LABELS: EntityPickerLabels;
33
+ /** What `onSearch` may resolve to when it knows its list was cut off. */
34
+ interface EntityPickerSearchResult {
35
+ options: readonly SearchSuggestOption[];
36
+ /** There are more matches than `options` holds: the list ends with the `more` line. */
37
+ hasMore?: boolean;
38
+ }
22
39
  interface EntityPickerProps {
23
40
  /** The chosen record, or null. */
24
41
  value: SearchSuggestOption | null;
25
42
  onValueChange: (next: SearchSuggestOption | null) => void;
26
- /** Finds records for the query; a rejection shows the error state with a Retry row. */
27
- onSearch: (query: string) => Promise<readonly SearchSuggestOption[]>;
43
+ /**
44
+ * Finds records for the query; a rejection shows the error state with a Retry row. Resolve
45
+ * with the options, or with `{ options, hasMore: true }` when the list was cut off (a page of
46
+ * 20 out of many): the list then says that more match.
47
+ */
48
+ onSearch: (query: string) => Promise<readonly SearchSuggestOption[] | EntityPickerSearchResult>;
28
49
  /** The field's name (the visible label usually comes from a surrounding `Field`). */
29
50
  label: string;
30
51
  /** Id of the control, for `Field htmlFor`/`label for`. */
@@ -32,7 +53,10 @@ interface EntityPickerProps {
32
53
  /** Hidden form value: the chosen record's id. */
33
54
  name?: string;
34
55
  placeholder?: string;
35
- /** Search only from this many characters. Default 0 (an empty query lists suggestions). */
56
+ /**
57
+ * Search only from this many characters. Default 0 (an empty query lists suggestions). Below
58
+ * it the open list shows the `tooShort` hint instead of nothing.
59
+ */
36
60
  minQueryLength?: number;
37
61
  /** Debounce before searching, in ms. Default 250. */
38
62
  delay?: number;
@@ -42,6 +66,7 @@ interface EntityPickerProps {
42
66
  'aria-describedby'?: string;
43
67
  /** How the chosen record shows in the field. Default: title, then the last breadcrumb. */
44
68
  renderValue?: (option: SearchSuggestOption) => ReactNode;
69
+ /** Each word defaults to `UixLabelsProvider` `entityPicker`, then to English. */
45
70
  labels?: Partial<EntityPickerLabels>;
46
71
  className?: string;
47
72
  }
@@ -53,4 +78,4 @@ interface EntityPickerProps {
53
78
  */
54
79
  declare function EntityPicker({ value, onValueChange, onSearch, label, id, name, placeholder, minQueryLength, delay, disabled, invalid, 'aria-describedby': describedBy, renderValue, labels: labelOverrides, className, }: EntityPickerProps): react.JSX.Element;
55
80
 
56
- export { DEFAULT_ENTITY_PICKER_LABELS, EntityPicker, type EntityPickerLabels, type EntityPickerProps };
81
+ export { DEFAULT_ENTITY_PICKER_LABELS, EntityPicker, type EntityPickerLabels, type EntityPickerProps, type EntityPickerSearchResult };
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { fillLabel } from "../fill-label.js";
6
+ import { useUixLabels } from "../labels-context.js";
6
7
  import { SearchSuggest } from "./SearchSuggest.js";
7
8
  const DEFAULT_ENTITY_PICKER_LABELS = {
8
9
  search: "Search",
@@ -13,7 +14,11 @@ const DEFAULT_ENTITY_PICKER_LABELS = {
13
14
  clear: "Clear {title}",
14
15
  change: "{label}: {title}. Change",
15
16
  none: "Choose\u2026",
16
- results: "{count} results"
17
+ results: "{count} results",
18
+ more: "More matches. Keep typing to narrow the search.",
19
+ resultsMore: "{count} results shown, more match. Keep typing to narrow the search.",
20
+ tooShort: "Type at least {count} characters.",
21
+ typeToSearch: "Type to search."
17
22
  };
18
23
  function EntityPicker({
19
24
  value,
@@ -32,10 +37,11 @@ function EntityPicker({
32
37
  labels: labelOverrides,
33
38
  className
34
39
  }) {
35
- const labels = { ...DEFAULT_ENTITY_PICKER_LABELS, ...labelOverrides };
40
+ const labels = { ...DEFAULT_ENTITY_PICKER_LABELS, ...useUixLabels().entityPicker, ...labelOverrides };
36
41
  const [editing, setEditing] = useState(value == null);
37
42
  const [query, setQuery] = useState("");
38
43
  const [results, setResults] = useState([]);
44
+ const [hasMore, setHasMore] = useState(false);
39
45
  const [status, setStatus] = useState("idle");
40
46
  const [attempt, setAttempt] = useState(0);
41
47
  const latest = useRef(0);
@@ -52,6 +58,7 @@ function EntityPicker({
52
58
  if (q.length < minQueryLength) {
53
59
  setStatus("idle");
54
60
  setResults([]);
61
+ setHasMore(false);
55
62
  return;
56
63
  }
57
64
  const run = ++latest.current;
@@ -59,14 +66,16 @@ function EntityPicker({
59
66
  setStatus("loading");
60
67
  onSearch(q).then(
61
68
  (found) => {
62
- if (run === latest.current) {
63
- setResults(found);
64
- setStatus("done");
65
- }
69
+ if (run !== latest.current) return;
70
+ const list = "options" in found ? found.options : found;
71
+ setResults(list);
72
+ setHasMore("options" in found && !!found.hasMore && list.length > 0);
73
+ setStatus("done");
66
74
  },
67
75
  () => {
68
76
  if (run === latest.current) {
69
77
  setResults([]);
78
+ setHasMore(false);
70
79
  setStatus("error");
71
80
  }
72
81
  }
@@ -95,6 +104,7 @@ function EntityPicker({
95
104
  setEditing(true);
96
105
  };
97
106
  const hidden = name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: value?.id ?? "" }) : null;
107
+ const tooShort = !disabled && query.trim().length < minQueryLength;
98
108
  if (!editing && value) {
99
109
  const shown = renderValue?.(value) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
100
110
  /* @__PURE__ */ jsx("span", { className: "uix-entity-picker__title", children: value.title }),
@@ -150,10 +160,12 @@ function EntityPicker({
150
160
  placeholder: placeholder ?? labels.none,
151
161
  loading: status === "loading",
152
162
  loadingLabel: labels.loading,
153
- empty: labels.empty,
163
+ empty: tooShort ? void 0 : labels.empty,
164
+ idle: tooShort ? fillLabel(minQueryLength === 1 ? labels.typeToSearch : labels.tooShort, { count: minQueryLength }) : void 0,
165
+ note: status === "done" && hasMore ? labels.more : void 0,
154
166
  error: status === "error" ? labels.error : void 0,
155
167
  footer: status === "error" ? { label: labels.retry, onSelect: () => setAttempt((n) => n + 1) } : void 0,
156
- status: status === "done" ? fillLabel(labels.results, { count: results.length }) : void 0,
168
+ status: status === "done" ? fillLabel(hasMore ? labels.resultsMore : labels.results, { count: results.length }) : void 0,
157
169
  strategy: "fixed"
158
170
  }
159
171
  ),
@@ -0,0 +1,74 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /** Every word `InlineEdit` renders or announces. `{label}` is the field's name. */
5
+ interface InlineEditLabels {
6
+ /** Added to the view button's name after the value: "…, Edit {label}". */
7
+ edit: string;
8
+ save: string;
9
+ cancel: string;
10
+ /** Polite status while `onSave` runs. */
11
+ saving: string;
12
+ /** Polite status after a save. */
13
+ saved: string;
14
+ /** Shown when `onSave` rejects without a message of its own. */
15
+ failed: string;
16
+ /** What the view shows when the value is empty and no `placeholder` was given. */
17
+ empty: string;
18
+ }
19
+ declare const DEFAULT_INLINE_EDIT_LABELS: InlineEditLabels;
20
+ /** What a custom editor receives. Spread the field props onto your control. */
21
+ interface InlineEditEditorProps {
22
+ /** The draft. */
23
+ value: string;
24
+ onChange: (next: string) => void;
25
+ /** Save the draft (what Enter does in the default editor). */
26
+ save: () => void;
27
+ cancel: () => void;
28
+ /** `id`, the accessible name, the error wiring and `disabled` while saving. */
29
+ field: {
30
+ id: string;
31
+ 'aria-label': string;
32
+ 'aria-describedby': string | undefined;
33
+ 'aria-invalid': true | undefined;
34
+ disabled: boolean;
35
+ };
36
+ }
37
+ interface InlineEditProps {
38
+ /** The saved value. */
39
+ value: string;
40
+ /**
41
+ * Save the new value. Return a promise to show the pending state; a rejection keeps the
42
+ * editor open with the error under it (the rejection's message, else `labels.failed`).
43
+ */
44
+ onSave: (next: string) => void | Promise<void>;
45
+ /** The field's name: "Title", "Due date". It names the edit button and the editor. */
46
+ label: string;
47
+ /** How the saved value shows. Default: the text itself. */
48
+ renderView?: (value: string) => ReactNode;
49
+ /**
50
+ * A custom editor: a `Select`, a `Combobox`, a date field. Spread `field` onto the control.
51
+ * Default: an `Input`, or a `Textarea` with `multiline`.
52
+ */
53
+ renderEditor?: (props: InlineEditEditorProps) => ReactNode;
54
+ /** Edit in a textarea: Enter adds a line, Ctrl/⌘+Enter saves. */
55
+ multiline?: boolean;
56
+ /** Return a message to refuse the draft; nothing is saved while it returns one. */
57
+ validate?: (next: string) => ReactNode | undefined;
58
+ /** Shown in the view while the value is empty, e.g. "Add a description". */
59
+ placeholder?: ReactNode;
60
+ /** Read-only: the value shows as plain text, with no edit button. */
61
+ disabled?: boolean;
62
+ /** Each word defaults to `UixLabelsProvider` `inlineEdit`, then to English. */
63
+ labels?: Partial<InlineEditLabels>;
64
+ className?: string;
65
+ }
66
+ /**
67
+ * A value that is edited in place (HAR-1381; TENSOR C16 `editable-record.tsx`): the value is
68
+ * a button that says what it edits; activating it swaps in an editor with Save and Cancel.
69
+ * Enter saves and Escape cancels; while `onSave` runs the editor is busy; a failed save keeps
70
+ * the draft and shows why; afterwards focus is back on the value. Nothing is saved on blur.
71
+ */
72
+ declare function InlineEdit({ value, onSave, label, renderView, renderEditor, multiline, validate, placeholder, disabled, labels: labelOverrides, className, }: InlineEditProps): react.JSX.Element;
73
+
74
+ export { DEFAULT_INLINE_EDIT_LABELS, InlineEdit, type InlineEditEditorProps, type InlineEditLabels, type InlineEditProps };
@@ -0,0 +1,176 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { fillLabel } from "../fill-label.js";
6
+ import { useUixLabels } from "../labels-context.js";
7
+ import { PencilIcon } from "../icons/components.js";
8
+ import { Button } from "./Button.js";
9
+ import { Input } from "./Input.js";
10
+ import { Textarea } from "./Textarea.js";
11
+ const DEFAULT_INLINE_EDIT_LABELS = {
12
+ edit: "Edit {label}",
13
+ save: "Save",
14
+ cancel: "Cancel",
15
+ saving: "Saving\u2026",
16
+ saved: "Saved",
17
+ failed: "Could not save.",
18
+ empty: "Not set"
19
+ };
20
+ function InlineEdit({
21
+ value,
22
+ onSave,
23
+ label,
24
+ renderView,
25
+ renderEditor,
26
+ multiline = false,
27
+ validate,
28
+ placeholder,
29
+ disabled = false,
30
+ labels: labelOverrides,
31
+ className
32
+ }) {
33
+ const labels = { ...DEFAULT_INLINE_EDIT_LABELS, ...useUixLabels().inlineEdit, ...labelOverrides };
34
+ const id = useId();
35
+ const errorId = `${id}-error`;
36
+ const [editing, setEditing] = useState(false);
37
+ const [draft, setDraft] = useState(value);
38
+ const [pending, setPending] = useState(false);
39
+ const [error, setError] = useState();
40
+ const [status, setStatus] = useState("");
41
+ const viewRef = useRef(null);
42
+ const editorRef = useRef(null);
43
+ const focusNext = useRef(null);
44
+ const mounted = useRef(true);
45
+ useEffect(() => {
46
+ mounted.current = true;
47
+ return () => {
48
+ mounted.current = false;
49
+ };
50
+ }, []);
51
+ useEffect(() => {
52
+ const target = focusNext.current;
53
+ if (!target) return;
54
+ focusNext.current = null;
55
+ if (target === "view") {
56
+ viewRef.current?.focus();
57
+ return;
58
+ }
59
+ const control = editorRef.current?.querySelector("input, textarea, select, button, [tabindex]");
60
+ control?.focus();
61
+ if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) control.select();
62
+ }, [editing]);
63
+ const start = () => {
64
+ setDraft(value);
65
+ setError(void 0);
66
+ setStatus("");
67
+ focusNext.current = "editor";
68
+ setEditing(true);
69
+ };
70
+ const close = () => {
71
+ focusNext.current = "view";
72
+ setEditing(false);
73
+ };
74
+ const cancel = () => {
75
+ if (pending) return;
76
+ setError(void 0);
77
+ close();
78
+ };
79
+ const save = () => {
80
+ if (pending) return;
81
+ const issue = validate?.(draft);
82
+ if (issue != null) {
83
+ setError(issue);
84
+ return;
85
+ }
86
+ if (draft === value) {
87
+ close();
88
+ return;
89
+ }
90
+ setError(void 0);
91
+ let result;
92
+ try {
93
+ result = onSave(draft);
94
+ } catch (thrown) {
95
+ setError(thrown instanceof Error && thrown.message ? thrown.message : labels.failed);
96
+ return;
97
+ }
98
+ if (!result || typeof result.then !== "function") {
99
+ setStatus(labels.saved);
100
+ close();
101
+ return;
102
+ }
103
+ setPending(true);
104
+ setStatus(labels.saving);
105
+ result.then(
106
+ () => {
107
+ if (!mounted.current) return;
108
+ setPending(false);
109
+ setStatus(labels.saved);
110
+ close();
111
+ },
112
+ (reason) => {
113
+ if (!mounted.current) return;
114
+ setPending(false);
115
+ setStatus("");
116
+ setError(typeof reason === "string" && reason ? reason : reason instanceof Error && reason.message ? reason.message : labels.failed);
117
+ focusNext.current = null;
118
+ editorRef.current?.querySelector("input, textarea, select")?.focus();
119
+ }
120
+ );
121
+ };
122
+ const onEditorKeyDown = (event) => {
123
+ if (event.key === "Escape") {
124
+ event.preventDefault();
125
+ event.stopPropagation();
126
+ cancel();
127
+ } else if (event.key === "Enter" && !event.defaultPrevented) {
128
+ const inField = event.target.matches("input, textarea");
129
+ if (!inField) return;
130
+ const isTextarea = event.target.tagName === "TEXTAREA";
131
+ if (isTextarea && !(event.ctrlKey || event.metaKey)) return;
132
+ event.preventDefault();
133
+ save();
134
+ }
135
+ };
136
+ const liveRegion = /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: status });
137
+ const shown = value === "" ? /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__placeholder", children: placeholder ?? labels.empty }) : renderView?.(value) ?? value;
138
+ if (disabled) {
139
+ return /* @__PURE__ */ jsx("span", { className: cx("uix-inline-edit", "uix-inline-edit--readonly", className), children: /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__value", children: shown }) });
140
+ }
141
+ if (!editing) {
142
+ return /* @__PURE__ */ jsxs("span", { className: cx("uix-inline-edit", className), children: [
143
+ /* @__PURE__ */ jsxs("button", { ref: viewRef, type: "button", className: "uix-inline-edit__view", onClick: start, children: [
144
+ /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__value", children: shown }),
145
+ /* @__PURE__ */ jsxs("span", { className: "uix-visually-hidden", children: [
146
+ ", ",
147
+ fillLabel(labels.edit, { label })
148
+ ] }),
149
+ /* @__PURE__ */ jsx("span", { className: "uix-inline-edit__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(PencilIcon, { size: "sm" }) })
150
+ ] }),
151
+ liveRegion
152
+ ] });
153
+ }
154
+ const field = {
155
+ id,
156
+ "aria-label": label,
157
+ "aria-describedby": error != null ? errorId : void 0,
158
+ "aria-invalid": error != null ? true : void 0,
159
+ disabled: pending
160
+ };
161
+ return /* @__PURE__ */ jsxs("span", { className: cx("uix-inline-edit", "uix-inline-edit--editing", className), children: [
162
+ /* @__PURE__ */ jsxs("div", { ref: editorRef, className: "uix-inline-edit__editor", role: "group", "aria-label": fillLabel(labels.edit, { label }), "aria-busy": pending || void 0, onKeyDown: onEditorKeyDown, children: [
163
+ /* @__PURE__ */ jsx("div", { className: "uix-inline-edit__control", children: renderEditor ? renderEditor({ value: draft, onChange: setDraft, save, cancel, field }) : multiline ? /* @__PURE__ */ jsx(Textarea, { ...field, rows: 3, value: draft, invalid: error != null, onChange: (event) => setDraft(event.currentTarget.value) }) : /* @__PURE__ */ jsx(Input, { ...field, size: "sm", value: draft, invalid: error != null, onChange: (event) => setDraft(event.currentTarget.value) }) }),
164
+ /* @__PURE__ */ jsxs("span", { className: "uix-inline-edit__actions", children: [
165
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "primary", loading: pending, onClick: save, children: labels.save }),
166
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", disabled: pending, onClick: cancel, children: labels.cancel })
167
+ ] }),
168
+ error != null && /* @__PURE__ */ jsx("div", { id: errorId, className: "uix-field__error uix-inline-edit__error", role: "alert", children: error })
169
+ ] }),
170
+ liveRegion
171
+ ] });
172
+ }
173
+ export {
174
+ DEFAULT_INLINE_EDIT_LABELS,
175
+ InlineEdit
176
+ };
@@ -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;