@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.
- package/README.md +164 -15
- package/dist/{SchedulingCalendar-DSLQ6AY5.d.ts → SchedulingCalendar-B6TakO2h.d.ts} +13 -1
- package/dist/components/Avatar.d.ts +26 -4
- package/dist/components/Avatar.js +26 -3
- package/dist/components/CommandPalette.d.ts +6 -1
- package/dist/components/CommandPalette.js +9 -3
- package/dist/components/DateFieldParts.d.ts +61 -0
- package/dist/components/DateFieldParts.js +195 -0
- package/dist/components/DatePicker.d.ts +129 -0
- package/dist/components/DatePicker.js +340 -0
- package/dist/components/DateRangePicker.d.ts +34 -3
- package/dist/components/DateRangePicker.js +120 -17
- package/dist/components/EntityPicker.d.ts +29 -4
- package/dist/components/EntityPicker.js +20 -8
- package/dist/components/InlineEdit.d.ts +74 -0
- package/dist/components/InlineEdit.js +176 -0
- package/dist/components/List.d.ts +8 -1
- package/dist/components/List.js +3 -1
- package/dist/components/ListRoving.d.ts +12 -0
- package/dist/components/ListRoving.js +51 -0
- package/dist/components/Radio.d.ts +23 -2
- package/dist/components/Radio.js +18 -3
- package/dist/components/SchedulingAgenda.d.ts +1 -1
- package/dist/components/SchedulingAgenda.js +101 -4
- package/dist/components/SchedulingCalendar.d.ts +1 -1
- package/dist/components/SchedulingCalendar.js +89 -9
- package/dist/components/SchedulingTimeGrid.d.ts +1 -1
- package/dist/components/SchedulingTimeGrid.js +101 -4
- package/dist/components/SchedulingTimeline.d.ts +130 -15
- package/dist/components/SchedulingTimeline.js +765 -141
- package/dist/components/SearchSuggest.d.ts +12 -1
- package/dist/components/SearchSuggest.js +7 -2
- package/dist/components/Tree.d.ts +30 -2
- package/dist/components/Tree.js +135 -10
- package/dist/date-field-model.d.ts +40 -0
- package/dist/date-field-model.js +79 -0
- package/dist/index.cjs +4179 -2056
- package/dist/index.d.cts +829 -264
- package/dist/index.d.ts +10 -7
- package/dist/index.js +40 -4
- package/dist/labels-context.d.ts +15 -2
- package/dist/roving.d.ts +15 -0
- package/dist/roving.js +17 -0
- package/dist/scheduling-timeline-model.d.ts +61 -11
- package/dist/scheduling-timeline-model.js +85 -26
- package/dist/scheduling-timeline-rows.d.ts +40 -0
- package/dist/scheduling-timeline-rows.js +42 -0
- package/package.json +5 -3
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { addCalendarDays, addCalendarMonths, buildMonthGrid, isDateUnavailable, startOfMonth } from "../calendar-model.js";
|
|
5
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "../icons/components.js";
|
|
6
|
+
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")}`;
|
|
7
|
+
const utc = (date) => /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
8
|
+
const monthLabel = (date, locale) => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(utc(date));
|
|
9
|
+
const fullDateLabel = (date, locale) => new Intl.DateTimeFormat(locale, { dateStyle: "full", timeZone: "UTC" }).format(utc(date));
|
|
10
|
+
const weekdayNames = (locale, weekStartsOn = 1) => {
|
|
11
|
+
const fmt = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" });
|
|
12
|
+
return Array.from({ length: 7 }, (_, i) => fmt.format(new Date(Date.UTC(2024, 0, 7 + weekStartsOn + i))));
|
|
13
|
+
};
|
|
14
|
+
const isOpen = (el) => {
|
|
15
|
+
try {
|
|
16
|
+
return !!el?.matches(":popover-open");
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function useFieldPopover(popoverId, restoreFocus, onOpenChange) {
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const pointerWasOpen = useRef(null);
|
|
24
|
+
const restoreRef = useRef(restoreFocus);
|
|
25
|
+
restoreRef.current = restoreFocus;
|
|
26
|
+
const onOpenChangeRef = useRef(onOpenChange);
|
|
27
|
+
onOpenChangeRef.current = onOpenChange;
|
|
28
|
+
const skipRestore = useRef(false);
|
|
29
|
+
const popoverEl = () => typeof document === "undefined" ? null : document.getElementById(popoverId);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const el = popoverEl();
|
|
32
|
+
if (!el) return;
|
|
33
|
+
const onToggle = (event) => {
|
|
34
|
+
const { newState } = event;
|
|
35
|
+
const nowOpen = newState ? newState === "open" : isOpen(el);
|
|
36
|
+
setOpen(nowOpen);
|
|
37
|
+
onOpenChangeRef.current?.(nowOpen);
|
|
38
|
+
if (nowOpen) return;
|
|
39
|
+
const active = el.ownerDocument.activeElement;
|
|
40
|
+
if (skipRestore.current) skipRestore.current = false;
|
|
41
|
+
else if (el.contains(active) || active === el.ownerDocument.body) restoreRef.current();
|
|
42
|
+
};
|
|
43
|
+
el.addEventListener("toggle", onToggle);
|
|
44
|
+
return () => el.removeEventListener("toggle", onToggle);
|
|
45
|
+
}, [popoverId]);
|
|
46
|
+
const show = () => {
|
|
47
|
+
const el = popoverEl();
|
|
48
|
+
if (el && !isOpen(el)) {
|
|
49
|
+
try {
|
|
50
|
+
el.showPopover();
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const hide = (restore = true) => {
|
|
56
|
+
const el = popoverEl();
|
|
57
|
+
if (!el || !isOpen(el)) return;
|
|
58
|
+
skipRestore.current = !restore;
|
|
59
|
+
try {
|
|
60
|
+
el.hidePopover();
|
|
61
|
+
} catch {
|
|
62
|
+
skipRestore.current = false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
open,
|
|
67
|
+
show,
|
|
68
|
+
hide,
|
|
69
|
+
/** Spread on the button that opens the calendar. */
|
|
70
|
+
triggerProps: {
|
|
71
|
+
"aria-haspopup": "dialog",
|
|
72
|
+
"aria-expanded": open,
|
|
73
|
+
"aria-controls": popoverId,
|
|
74
|
+
// A pointer press outside an open auto popover already closed it (light dismiss); the
|
|
75
|
+
// click that follows must not open it again.
|
|
76
|
+
onPointerDown: () => {
|
|
77
|
+
pointerWasOpen.current = isOpen(popoverEl());
|
|
78
|
+
},
|
|
79
|
+
// A press that never became a click (dragged off, cancelled) must not decide the next one.
|
|
80
|
+
onPointerLeave: () => {
|
|
81
|
+
pointerWasOpen.current = null;
|
|
82
|
+
},
|
|
83
|
+
onPointerCancel: () => {
|
|
84
|
+
pointerWasOpen.current = null;
|
|
85
|
+
},
|
|
86
|
+
onClick: (event) => {
|
|
87
|
+
if (event.defaultPrevented) return;
|
|
88
|
+
const was = pointerWasOpen.current;
|
|
89
|
+
pointerWasOpen.current = null;
|
|
90
|
+
if (was === true) {
|
|
91
|
+
hide();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (isOpen(popoverEl())) hide();
|
|
95
|
+
else show();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function MonthCalendar({
|
|
101
|
+
value,
|
|
102
|
+
onSelect,
|
|
103
|
+
initialDate,
|
|
104
|
+
min,
|
|
105
|
+
max,
|
|
106
|
+
isUnavailable,
|
|
107
|
+
locale,
|
|
108
|
+
weekStartsOn = 1,
|
|
109
|
+
today,
|
|
110
|
+
labels,
|
|
111
|
+
autoFocus = false
|
|
112
|
+
}) {
|
|
113
|
+
const clamp = (date) => min && date < min ? min : max && date > max ? max : date;
|
|
114
|
+
const [active, setActive] = useState(() => clamp(initialDate));
|
|
115
|
+
const gridRef = useRef(null);
|
|
116
|
+
const pendingFocus = useRef(autoFocus);
|
|
117
|
+
const month = startOfMonth(active);
|
|
118
|
+
const days = useMemo(() => buildMonthGrid(month, weekStartsOn).filter((day) => day.inMonth), [month, weekStartsOn]);
|
|
119
|
+
const blanks = (utc(month).getUTCDay() - weekStartsOn + 7) % 7;
|
|
120
|
+
const weekdays = useMemo(() => weekdayNames(locale, weekStartsOn), [locale, weekStartsOn]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!pendingFocus.current) return;
|
|
123
|
+
pendingFocus.current = false;
|
|
124
|
+
gridRef.current?.querySelector(`[data-date="${active}"]`)?.focus({ preventScroll: true });
|
|
125
|
+
}, [active]);
|
|
126
|
+
const focusDate = (date) => {
|
|
127
|
+
pendingFocus.current = true;
|
|
128
|
+
setActive(clamp(date));
|
|
129
|
+
};
|
|
130
|
+
const showMonth = (amount) => setActive((current) => clamp(addCalendarMonths(current, amount)));
|
|
131
|
+
const unavailable = (date) => isDateUnavailable(date, { min, max, isDisabled: isUnavailable });
|
|
132
|
+
const onDayKeyDown = (event, date) => {
|
|
133
|
+
const rtl = typeof getComputedStyle === "function" && getComputedStyle(event.currentTarget).direction === "rtl";
|
|
134
|
+
const offsets = { ArrowLeft: rtl ? 1 : -1, ArrowRight: rtl ? -1 : 1, ArrowUp: -7, ArrowDown: 7 };
|
|
135
|
+
if (event.key in offsets) {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
focusDate(addCalendarDays(date, offsets[event.key]));
|
|
138
|
+
} else if (event.key === "Home" || event.key === "End") {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
const index = (utc(date).getUTCDay() - weekStartsOn + 7) % 7;
|
|
141
|
+
focusDate(addCalendarDays(date, event.key === "Home" ? -index : 6 - index));
|
|
142
|
+
} else if (event.key === "PageUp" || event.key === "PageDown") {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
focusDate(addCalendarMonths(date, (event.key === "PageUp" ? -1 : 1) * (event.shiftKey ? 12 : 1)));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const atMin = !!min && month <= startOfMonth(min);
|
|
148
|
+
const atMax = !!max && month >= startOfMonth(max);
|
|
149
|
+
return /* @__PURE__ */ jsxs("div", { className: "uix-date-picker__calendar", children: [
|
|
150
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-date-picker__nav", children: [
|
|
151
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-date-picker__nav-btn", "aria-label": labels.previousMonth, "aria-disabled": atMin || void 0, onClick: () => {
|
|
152
|
+
if (!atMin) showMonth(-1);
|
|
153
|
+
}, children: /* @__PURE__ */ jsx(ChevronLeftIcon, { size: "sm" }) }),
|
|
154
|
+
/* @__PURE__ */ jsx("span", { className: "uix-date-picker__month", "aria-live": "polite", children: monthLabel(month, locale) }),
|
|
155
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-date-picker__nav-btn", "aria-label": labels.nextMonth, "aria-disabled": atMax || void 0, onClick: () => {
|
|
156
|
+
if (!atMax) showMonth(1);
|
|
157
|
+
}, children: /* @__PURE__ */ jsx(ChevronRightIcon, { size: "sm" }) })
|
|
158
|
+
] }),
|
|
159
|
+
/* @__PURE__ */ jsx("div", { className: "uix-date-range-picker__weekdays", "aria-hidden": "true", children: weekdays.map((day, index) => /* @__PURE__ */ jsx("span", { children: day }, index)) }),
|
|
160
|
+
/* @__PURE__ */ jsxs("div", { ref: gridRef, className: "uix-date-range-picker__grid uix-date-picker__grid", role: "group", "aria-label": monthLabel(month, locale), children: [
|
|
161
|
+
Array.from({ length: blanks }, (_, index) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }, `blank-${index}`)),
|
|
162
|
+
days.map((day) => {
|
|
163
|
+
const off = unavailable(day.date);
|
|
164
|
+
return /* @__PURE__ */ jsx(
|
|
165
|
+
"button",
|
|
166
|
+
{
|
|
167
|
+
type: "button",
|
|
168
|
+
"data-date": day.date,
|
|
169
|
+
"data-selected": day.date === value || void 0,
|
|
170
|
+
"aria-pressed": day.date === value,
|
|
171
|
+
"aria-label": fullDateLabel(day.date, locale),
|
|
172
|
+
"aria-current": day.date === today ? "date" : void 0,
|
|
173
|
+
"aria-disabled": off || void 0,
|
|
174
|
+
tabIndex: active === day.date ? 0 : -1,
|
|
175
|
+
onFocus: () => setActive(day.date),
|
|
176
|
+
onKeyDown: (event) => onDayKeyDown(event, day.date),
|
|
177
|
+
onClick: () => {
|
|
178
|
+
if (!off) onSelect(day.date);
|
|
179
|
+
},
|
|
180
|
+
children: day.day
|
|
181
|
+
},
|
|
182
|
+
day.date
|
|
183
|
+
);
|
|
184
|
+
})
|
|
185
|
+
] })
|
|
186
|
+
] });
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
MonthCalendar,
|
|
190
|
+
fullDateLabel,
|
|
191
|
+
localDateKey,
|
|
192
|
+
monthLabel,
|
|
193
|
+
useFieldPopover,
|
|
194
|
+
weekdayNames
|
|
195
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { AriaAttributes, CSSProperties } from 'react';
|
|
3
|
+
import { CalendarWeekday } from '../calendar-model.js';
|
|
4
|
+
import { Placement } from '../overlay-position.js';
|
|
5
|
+
|
|
6
|
+
/** Every word `DatePicker` renders or announces. */
|
|
7
|
+
interface DatePickerLabels {
|
|
8
|
+
/** The calendar button's name while no date is set. */
|
|
9
|
+
open: string;
|
|
10
|
+
/** The calendar button's name with a date set; `{date}` is the date in full. */
|
|
11
|
+
openSelected: string;
|
|
12
|
+
/** The calendar popover's name. */
|
|
13
|
+
calendar: string;
|
|
14
|
+
previousMonth: string;
|
|
15
|
+
nextMonth: string;
|
|
16
|
+
/**
|
|
17
|
+
* Shown when the typed text is not a date; `{pattern}` is the expected shape: the
|
|
18
|
+
* `placeholder` when one is given, else the locale's (`DD.MM.YYYY`).
|
|
19
|
+
*/
|
|
20
|
+
invalidDate: string;
|
|
21
|
+
/** The same, when the shape is not known: a custom `parseDate` and no `placeholder`. */
|
|
22
|
+
notADate: string;
|
|
23
|
+
/** Shown when the typed date is outside `min` / `max` or refused by `isUnavailable`. */
|
|
24
|
+
unavailable: string;
|
|
25
|
+
}
|
|
26
|
+
declare const DEFAULT_DATE_PICKER_LABELS: DatePickerLabels;
|
|
27
|
+
/** Why typed text was refused: not a date, or a date that cannot be chosen. */
|
|
28
|
+
type DateInputProblem = 'format' | 'unavailable';
|
|
29
|
+
interface DatePickerProps {
|
|
30
|
+
/** The date as `YYYY-MM-DD`, or `null` for none. */
|
|
31
|
+
value: string | null;
|
|
32
|
+
onValueChange: (value: string | null) => void;
|
|
33
|
+
/** First and last selectable day (`YYYY-MM-DD`). The calendar does not go past them. */
|
|
34
|
+
min?: string;
|
|
35
|
+
max?: string;
|
|
36
|
+
/** Days inside `min` / `max` that cannot be chosen (weekends, a freeze, days already taken). */
|
|
37
|
+
isUnavailable?: (date: string) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* How the field writes a date. Default: the locale's numeric date (22.11.2026, 11/22/2026).
|
|
40
|
+
* Pass `parseDate` with it when your format is not three numbers, so typing still works.
|
|
41
|
+
*/
|
|
42
|
+
formatDate?: (date: string) => string;
|
|
43
|
+
/** Read typed text into `YYYY-MM-DD`, or `null` when it is not a date. Default: ISO or the locale's order. */
|
|
44
|
+
parseDate?: (text: string) => string | null;
|
|
45
|
+
locale?: string;
|
|
46
|
+
/** First column of the calendar, in `Date.getUTCDay()` numbering (0 = Sunday). Default 1, Monday. */
|
|
47
|
+
weekStartsOn?: CalendarWeekday;
|
|
48
|
+
/**
|
|
49
|
+
* The ISO date that gets the today ring. Defaults to the viewer's local date; pass it when
|
|
50
|
+
* the product works in another zone, or `null` for no today marker.
|
|
51
|
+
*/
|
|
52
|
+
today?: string | null;
|
|
53
|
+
/** Default: the locale's date shape, e.g. `DD.MM.YYYY`. */
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
size?: 'sm' | 'md';
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
readOnly?: boolean;
|
|
58
|
+
required?: boolean;
|
|
59
|
+
invalid?: boolean;
|
|
60
|
+
/** Submits the ISO value under this name (a hidden input), whatever the field shows. */
|
|
61
|
+
name?: string;
|
|
62
|
+
/** Lands on the text input, so a `<label htmlFor>` or `<Field>` names it. */
|
|
63
|
+
id?: string;
|
|
64
|
+
'aria-label'?: string;
|
|
65
|
+
'aria-labelledby'?: string;
|
|
66
|
+
'aria-describedby'?: string;
|
|
67
|
+
'aria-invalid'?: AriaAttributes['aria-invalid'];
|
|
68
|
+
'aria-required'?: AriaAttributes['aria-required'];
|
|
69
|
+
/** Where the calendar opens. Default `'bottom-start'`; it flips and shifts to stay on screen. */
|
|
70
|
+
placement?: Placement;
|
|
71
|
+
/** Told when typed text is refused, and `null` once the field is fine again. */
|
|
72
|
+
onInputProblem?: (problem: DateInputProblem | null) => void;
|
|
73
|
+
onOpenChange?: (open: boolean) => void;
|
|
74
|
+
/** Translatable words; English defaults in {@link DEFAULT_DATE_PICKER_LABELS}. */
|
|
75
|
+
labels?: Partial<DatePickerLabels>;
|
|
76
|
+
className?: string;
|
|
77
|
+
style?: CSSProperties;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A single date as a form field (HAR-1383): a text input that shows the formatted date and
|
|
81
|
+
* accepts a typed one, and a calendar button that opens one month in a popover. The value is an
|
|
82
|
+
* ISO `YYYY-MM-DD` string. Typed text is read on Enter and when focus leaves the field: ISO or
|
|
83
|
+
* the locale's own order (`3.4.2026` in German, `4/3/2026` in en-US); text that is not a date,
|
|
84
|
+
* or a date that cannot be chosen, marks the field invalid, says why, and leaves `value` alone.
|
|
85
|
+
* ArrowDown in the field opens the calendar; Escape closes it and focus returns to the field.
|
|
86
|
+
*/
|
|
87
|
+
declare const DatePicker: react.ForwardRefExoticComponent<DatePickerProps & react.RefAttributes<HTMLInputElement>>;
|
|
88
|
+
/** The words `DateTimePicker` adds to the date field's own ({@link DatePickerLabels}). */
|
|
89
|
+
interface DateTimePickerLabels extends DatePickerLabels {
|
|
90
|
+
/** Name of the date input when the picker has its own `aria-label`. */
|
|
91
|
+
date: string;
|
|
92
|
+
/** Name of the time input. */
|
|
93
|
+
time: string;
|
|
94
|
+
/** Name of the time input inside a labelled field; `{label}` is the field's label. */
|
|
95
|
+
timeOf: string;
|
|
96
|
+
/** Read after the time; `{zone}` is the zone shown beside it. */
|
|
97
|
+
timeZone: string;
|
|
98
|
+
}
|
|
99
|
+
declare const DEFAULT_DATE_TIME_PICKER_LABELS: DateTimePickerLabels;
|
|
100
|
+
interface DateTimePickerProps extends Omit<DatePickerProps, 'value' | 'onValueChange' | 'labels' | 'min' | 'max'> {
|
|
101
|
+
/** A wall-clock date and time, `YYYY-MM-DDTHH:mm` (what `datetime-local` holds), or `null`. */
|
|
102
|
+
value: string | null;
|
|
103
|
+
onValueChange: (value: string | null) => void;
|
|
104
|
+
/** First and last selectable day; a date-time is read by its date. */
|
|
105
|
+
min?: string;
|
|
106
|
+
max?: string;
|
|
107
|
+
/**
|
|
108
|
+
* The IANA zone the wall clock is in (`Europe/Vienna`). Its short name on the chosen day is
|
|
109
|
+
* shown beside the time. Without it the viewer's own zone is shown. The value itself carries
|
|
110
|
+
* no zone: turning it into an instant is the product's job.
|
|
111
|
+
*/
|
|
112
|
+
timeZone?: string;
|
|
113
|
+
/** Text shown instead of the zone's short name ("Vienna time", "UTC+2"); `null` shows none. */
|
|
114
|
+
timeZoneLabel?: string | null;
|
|
115
|
+
/** Minutes between time steps. Default 1. */
|
|
116
|
+
minuteStep?: number;
|
|
117
|
+
/** The time a date gets when it is chosen before any time. Default `'00:00'`. */
|
|
118
|
+
defaultTime?: string;
|
|
119
|
+
labels?: Partial<DateTimePickerLabels>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A date and a time as one field (HAR-1383): the {@link DatePicker} and a time input, with the
|
|
123
|
+
* time zone the pair is in shown beside them. The value is `YYYY-MM-DDTHH:mm`, the shape
|
|
124
|
+
* `datetime-local` uses, so it replaces that input without changing what a form submits.
|
|
125
|
+
* Clearing the date clears the value; a time alone is not a value.
|
|
126
|
+
*/
|
|
127
|
+
declare const DateTimePicker: react.ForwardRefExoticComponent<DateTimePickerProps & react.RefAttributes<HTMLInputElement>>;
|
|
128
|
+
|
|
129
|
+
export { DEFAULT_DATE_PICKER_LABELS, DEFAULT_DATE_TIME_PICKER_LABELS, type DateInputProblem, DatePicker, type DatePickerLabels, type DatePickerProps, DateTimePicker, type DateTimePickerLabels, type DateTimePickerProps };
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from "react";
|
|
4
|
+
import { isDateUnavailable } from "../calendar-model.js";
|
|
5
|
+
import { cx } from "../cx.js";
|
|
6
|
+
import { datePattern, formatDateKey, isValidDateKey, joinDateTime, parseDateInput, splitDateTime, timeZoneName } from "../date-field-model.js";
|
|
7
|
+
import { fillLabel } from "../fill-label.js";
|
|
8
|
+
import { CalendarIcon } from "../icons/components.js";
|
|
9
|
+
import { useUixLabels } from "../labels-context.js";
|
|
10
|
+
import { fullDateLabel, localDateKey, MonthCalendar, useFieldPopover } from "./DateFieldParts.js";
|
|
11
|
+
import { Popover } from "./Popover.js";
|
|
12
|
+
const DEFAULT_DATE_PICKER_LABELS = {
|
|
13
|
+
open: "Choose date",
|
|
14
|
+
openSelected: "Change date, {date}",
|
|
15
|
+
calendar: "Calendar",
|
|
16
|
+
previousMonth: "Show previous month",
|
|
17
|
+
nextMonth: "Show next month",
|
|
18
|
+
invalidDate: "Enter a date as {pattern}.",
|
|
19
|
+
notADate: "That is not a date.",
|
|
20
|
+
unavailable: "That date is not available."
|
|
21
|
+
};
|
|
22
|
+
const DatePicker = forwardRef(function DatePicker2({
|
|
23
|
+
value,
|
|
24
|
+
onValueChange,
|
|
25
|
+
min,
|
|
26
|
+
max,
|
|
27
|
+
isUnavailable,
|
|
28
|
+
formatDate,
|
|
29
|
+
parseDate,
|
|
30
|
+
locale,
|
|
31
|
+
weekStartsOn = 1,
|
|
32
|
+
today,
|
|
33
|
+
placeholder,
|
|
34
|
+
size = "md",
|
|
35
|
+
disabled,
|
|
36
|
+
readOnly,
|
|
37
|
+
required,
|
|
38
|
+
invalid,
|
|
39
|
+
name,
|
|
40
|
+
id: idProp,
|
|
41
|
+
"aria-label": ariaLabel,
|
|
42
|
+
"aria-labelledby": ariaLabelledBy,
|
|
43
|
+
"aria-describedby": ariaDescribedBy,
|
|
44
|
+
"aria-invalid": ariaInvalid,
|
|
45
|
+
"aria-required": ariaRequired,
|
|
46
|
+
placement = "bottom-start",
|
|
47
|
+
onInputProblem,
|
|
48
|
+
onOpenChange,
|
|
49
|
+
labels: labelOverrides,
|
|
50
|
+
className,
|
|
51
|
+
style
|
|
52
|
+
}, forwardedRef) {
|
|
53
|
+
const uid = useId();
|
|
54
|
+
const id = idProp ?? `${uid}-input`;
|
|
55
|
+
const popoverId = `${uid}-calendar`;
|
|
56
|
+
const errorId = `${uid}-error`;
|
|
57
|
+
const labels = { ...DEFAULT_DATE_PICKER_LABELS, ...useUixLabels().datePicker, ...labelOverrides };
|
|
58
|
+
const anchorRef = useRef(null);
|
|
59
|
+
const inputRef = useRef(null);
|
|
60
|
+
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
61
|
+
const format = (date) => formatDate?.(date) ?? formatDateKey(date, locale);
|
|
62
|
+
const [text, setText] = useState(() => value ? format(value) : "");
|
|
63
|
+
const [problem, setProblemState] = useState(null);
|
|
64
|
+
const reported = useRef(null);
|
|
65
|
+
const setProblem = (next) => {
|
|
66
|
+
if (next !== reported.current) {
|
|
67
|
+
reported.current = next;
|
|
68
|
+
onInputProblem?.(next);
|
|
69
|
+
}
|
|
70
|
+
setProblemState(next);
|
|
71
|
+
};
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
setText(value ? format(value) : "");
|
|
74
|
+
setProblem(null);
|
|
75
|
+
}, [value, locale]);
|
|
76
|
+
const { open, show, hide, triggerProps } = useFieldPopover(popoverId, () => inputRef.current?.focus({ preventScroll: true }), onOpenChange);
|
|
77
|
+
const locked = !!disabled || !!readOnly;
|
|
78
|
+
const commit = () => {
|
|
79
|
+
const typed = text.trim();
|
|
80
|
+
if (!typed) {
|
|
81
|
+
setProblem(null);
|
|
82
|
+
if (text) setText("");
|
|
83
|
+
if (value !== null) onValueChange(null);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (value && typed === format(value)) {
|
|
87
|
+
setProblem(null);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const parsed = parseDate ? parseDate(typed) : parseDateInput(typed, locale);
|
|
91
|
+
if (!parsed || !isValidDateKey(parsed)) {
|
|
92
|
+
setProblem("format");
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (isDateUnavailable(parsed, { min, max, isDisabled: isUnavailable })) {
|
|
96
|
+
setProblem("unavailable");
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
setProblem(null);
|
|
100
|
+
setText(format(parsed));
|
|
101
|
+
if (parsed !== value) onValueChange(parsed);
|
|
102
|
+
return true;
|
|
103
|
+
};
|
|
104
|
+
const onInputKeyDown = (event) => {
|
|
105
|
+
if (locked) return;
|
|
106
|
+
if (event.key === "ArrowDown") {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
show();
|
|
109
|
+
} else if (event.key === "Enter" && text.trim() !== (value ? format(value) : "")) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
commit();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const choose = (date) => {
|
|
115
|
+
setProblem(null);
|
|
116
|
+
setText(format(date));
|
|
117
|
+
if (date !== value) onValueChange(date);
|
|
118
|
+
hide(false);
|
|
119
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
120
|
+
};
|
|
121
|
+
const todayDate = today === void 0 ? localDateKey() : today;
|
|
122
|
+
const pattern = datePattern(locale);
|
|
123
|
+
const expected = placeholder ?? (parseDate ? null : pattern);
|
|
124
|
+
const describedBy = [ariaDescribedBy, problem ? errorId : void 0].filter(Boolean).join(" ") || void 0;
|
|
125
|
+
const isInvalid = !!invalid || !!problem || ariaInvalid === true || ariaInvalid === "true";
|
|
126
|
+
return /* @__PURE__ */ jsxs(
|
|
127
|
+
"div",
|
|
128
|
+
{
|
|
129
|
+
ref: anchorRef,
|
|
130
|
+
className: cx("uix-date-picker", size === "sm" && "uix-date-picker--sm", className),
|
|
131
|
+
style,
|
|
132
|
+
onKeyDown: (event) => {
|
|
133
|
+
if (event.key !== "Escape" || !open) return;
|
|
134
|
+
event.stopPropagation();
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
hide();
|
|
137
|
+
},
|
|
138
|
+
children: [
|
|
139
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-date-picker__control", children: [
|
|
140
|
+
/* @__PURE__ */ jsx(
|
|
141
|
+
"input",
|
|
142
|
+
{
|
|
143
|
+
ref: inputRef,
|
|
144
|
+
id,
|
|
145
|
+
type: "text",
|
|
146
|
+
autoComplete: "off",
|
|
147
|
+
spellCheck: false,
|
|
148
|
+
className: cx("uix-input", size === "sm" && "uix-input--sm", "uix-date-picker__input"),
|
|
149
|
+
value: text,
|
|
150
|
+
placeholder: placeholder ?? (formatDate ? void 0 : pattern),
|
|
151
|
+
disabled,
|
|
152
|
+
readOnly,
|
|
153
|
+
required,
|
|
154
|
+
"aria-label": ariaLabel,
|
|
155
|
+
"aria-labelledby": ariaLabelledBy,
|
|
156
|
+
"aria-describedby": describedBy,
|
|
157
|
+
"aria-invalid": isInvalid || void 0,
|
|
158
|
+
"aria-required": ariaRequired,
|
|
159
|
+
onChange: (event) => {
|
|
160
|
+
setText(event.target.value);
|
|
161
|
+
if (problem) setProblem(null);
|
|
162
|
+
},
|
|
163
|
+
onBlur: () => {
|
|
164
|
+
if (!locked) commit();
|
|
165
|
+
},
|
|
166
|
+
onKeyDown: onInputKeyDown
|
|
167
|
+
}
|
|
168
|
+
),
|
|
169
|
+
/* @__PURE__ */ jsx(
|
|
170
|
+
"button",
|
|
171
|
+
{
|
|
172
|
+
type: "button",
|
|
173
|
+
className: "uix-date-picker__toggle",
|
|
174
|
+
disabled: locked,
|
|
175
|
+
"aria-label": value && isValidDateKey(value) ? fillLabel(labels.openSelected, { date: fullDateLabel(value, locale) }) : labels.open,
|
|
176
|
+
...triggerProps,
|
|
177
|
+
children: /* @__PURE__ */ jsx(CalendarIcon, { size: "sm" })
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
] }),
|
|
181
|
+
name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: value ?? "", disabled }),
|
|
182
|
+
problem && /* @__PURE__ */ jsx("span", { id: errorId, className: "uix-field__error uix-date-picker__error", role: "alert", children: problem !== "format" ? labels.unavailable : expected ? fillLabel(labels.invalidDate, { pattern: expected }) : labels.notADate }),
|
|
183
|
+
/* @__PURE__ */ jsx(
|
|
184
|
+
Popover,
|
|
185
|
+
{
|
|
186
|
+
id: popoverId,
|
|
187
|
+
anchor: anchorRef,
|
|
188
|
+
placement,
|
|
189
|
+
className: "uix-date-picker__popover",
|
|
190
|
+
role: "dialog",
|
|
191
|
+
"aria-label": labels.calendar,
|
|
192
|
+
closeWhenAnchorHidden: true,
|
|
193
|
+
children: open && /* @__PURE__ */ jsx(
|
|
194
|
+
MonthCalendar,
|
|
195
|
+
{
|
|
196
|
+
value,
|
|
197
|
+
onSelect: choose,
|
|
198
|
+
initialDate: value && isValidDateKey(value) ? value : todayDate ?? localDateKey(),
|
|
199
|
+
min,
|
|
200
|
+
max,
|
|
201
|
+
isUnavailable,
|
|
202
|
+
locale,
|
|
203
|
+
weekStartsOn,
|
|
204
|
+
today: todayDate,
|
|
205
|
+
labels,
|
|
206
|
+
autoFocus: true
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
const DEFAULT_DATE_TIME_PICKER_LABELS = {
|
|
216
|
+
...DEFAULT_DATE_PICKER_LABELS,
|
|
217
|
+
date: "Date",
|
|
218
|
+
time: "Time",
|
|
219
|
+
timeOf: "{label}, time",
|
|
220
|
+
timeZone: "Time zone: {zone}"
|
|
221
|
+
};
|
|
222
|
+
const DateTimePicker = forwardRef(function DateTimePicker2({
|
|
223
|
+
value,
|
|
224
|
+
onValueChange,
|
|
225
|
+
timeZone,
|
|
226
|
+
timeZoneLabel,
|
|
227
|
+
minuteStep = 1,
|
|
228
|
+
defaultTime = "00:00",
|
|
229
|
+
min,
|
|
230
|
+
max,
|
|
231
|
+
name,
|
|
232
|
+
id: idProp,
|
|
233
|
+
"aria-label": ariaLabel,
|
|
234
|
+
"aria-labelledby": ariaLabelledBy,
|
|
235
|
+
"aria-describedby": ariaDescribedBy,
|
|
236
|
+
labels: labelOverrides,
|
|
237
|
+
locale,
|
|
238
|
+
size = "md",
|
|
239
|
+
disabled,
|
|
240
|
+
readOnly,
|
|
241
|
+
required,
|
|
242
|
+
className,
|
|
243
|
+
style,
|
|
244
|
+
...dateProps
|
|
245
|
+
}, forwardedRef) {
|
|
246
|
+
const uid = useId();
|
|
247
|
+
const id = idProp ?? `${uid}-date`;
|
|
248
|
+
const zoneId = `${uid}-zone`;
|
|
249
|
+
const labels = { ...DEFAULT_DATE_TIME_PICKER_LABELS, ...useUixLabels().dateTimePicker, ...labelOverrides };
|
|
250
|
+
const { date, time } = splitDateTime(value);
|
|
251
|
+
const dateRef = useRef(null);
|
|
252
|
+
useImperativeHandle(forwardedRef, () => dateRef.current);
|
|
253
|
+
const [pendingTime, setPendingTime] = useState("");
|
|
254
|
+
const shownTime = date ? time : pendingTime;
|
|
255
|
+
const [fieldLabel, setFieldLabel] = useState("");
|
|
256
|
+
const [viewerZone, setViewerZone] = useState(null);
|
|
257
|
+
const grouped = !!ariaLabel || !!ariaLabelledBy;
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (!grouped) setFieldLabel(dateRef.current?.labels?.[0]?.textContent?.trim() ?? "");
|
|
260
|
+
if (!timeZone) {
|
|
261
|
+
try {
|
|
262
|
+
setViewerZone(new Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}, [grouped, timeZone]);
|
|
267
|
+
const zone = timeZone ?? viewerZone;
|
|
268
|
+
const zoneText = timeZoneLabel === null ? null : timeZoneLabel ?? (zone ? timeZoneName(zone, date, locale) : null);
|
|
269
|
+
const timeName = grouped ? labels.time : fieldLabel ? fillLabel(labels.timeOf, { label: fieldLabel }) : labels.time;
|
|
270
|
+
const step = Math.max(1, Math.round(minuteStep)) * 60;
|
|
271
|
+
return /* @__PURE__ */ jsxs(
|
|
272
|
+
"div",
|
|
273
|
+
{
|
|
274
|
+
className: cx("uix-date-time-picker", size === "sm" && "uix-date-time-picker--sm", className),
|
|
275
|
+
style,
|
|
276
|
+
role: "group",
|
|
277
|
+
"aria-label": ariaLabel,
|
|
278
|
+
"aria-labelledby": ariaLabelledBy,
|
|
279
|
+
children: [
|
|
280
|
+
/* @__PURE__ */ jsx(
|
|
281
|
+
DatePicker,
|
|
282
|
+
{
|
|
283
|
+
...dateProps,
|
|
284
|
+
ref: dateRef,
|
|
285
|
+
id,
|
|
286
|
+
value: date,
|
|
287
|
+
locale,
|
|
288
|
+
size,
|
|
289
|
+
min: min?.slice(0, 10),
|
|
290
|
+
max: max?.slice(0, 10),
|
|
291
|
+
disabled,
|
|
292
|
+
readOnly,
|
|
293
|
+
required,
|
|
294
|
+
"aria-label": grouped ? labels.date : void 0,
|
|
295
|
+
"aria-describedby": ariaDescribedBy,
|
|
296
|
+
labels,
|
|
297
|
+
onValueChange: (next) => {
|
|
298
|
+
if (!next) {
|
|
299
|
+
setPendingTime("");
|
|
300
|
+
onValueChange(null);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
onValueChange(joinDateTime(next, shownTime, defaultTime));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
),
|
|
307
|
+
/* @__PURE__ */ jsx(
|
|
308
|
+
"input",
|
|
309
|
+
{
|
|
310
|
+
type: "time",
|
|
311
|
+
className: cx("uix-input", size === "sm" && "uix-input--sm", "uix-date-time-picker__time"),
|
|
312
|
+
value: shownTime,
|
|
313
|
+
step,
|
|
314
|
+
disabled,
|
|
315
|
+
readOnly,
|
|
316
|
+
required: required && !!date,
|
|
317
|
+
"aria-label": timeName,
|
|
318
|
+
"aria-describedby": zoneText ? zoneId : void 0,
|
|
319
|
+
onChange: (event) => {
|
|
320
|
+
const next = event.target.value;
|
|
321
|
+
if (date) onValueChange(joinDateTime(date, next, defaultTime));
|
|
322
|
+
else setPendingTime(next);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
),
|
|
326
|
+
zoneText && /* @__PURE__ */ jsxs("span", { className: "uix-date-time-picker__zone", children: [
|
|
327
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: zoneText }),
|
|
328
|
+
/* @__PURE__ */ jsx("span", { id: zoneId, className: "uix-visually-hidden", children: fillLabel(labels.timeZone, { zone: zoneText }) })
|
|
329
|
+
] }),
|
|
330
|
+
name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: value ?? "", disabled })
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
export {
|
|
336
|
+
DEFAULT_DATE_PICKER_LABELS,
|
|
337
|
+
DEFAULT_DATE_TIME_PICKER_LABELS,
|
|
338
|
+
DatePicker,
|
|
339
|
+
DateTimePicker
|
|
340
|
+
};
|