@windmorph/react 0.2.2 → 0.2.4
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/dist/components/autocomplete/Autocomplete.d.ts +4 -1
- package/dist/components/autocomplete/Autocomplete.js +3 -5
- package/dist/components/autocomplete/Autocomplete.js.map +1 -1
- package/dist/components/badge/Badge.d.ts +5 -2
- package/dist/components/badge/Badge.js +7 -3
- package/dist/components/badge/Badge.js.map +1 -1
- package/dist/components/button/Button.d.ts +4 -2
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/button/ButtonContext.d.ts +1 -1
- package/dist/components/button/button-pending.js +2 -4
- package/dist/components/button/button-pending.js.map +1 -1
- package/dist/{composites → components}/button-group/ButtonGroup.d.ts +4 -4
- package/dist/{composites → components}/button-group/ButtonGroup.js +9 -7
- package/dist/components/button-group/ButtonGroup.js.map +1 -0
- package/dist/{composites → components}/button-group/button-group-styles.d.ts +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js.map +1 -1
- package/dist/{composites → components}/button-group/lazy-overflow-menu.js +2 -2
- package/dist/components/button-group/lazy-overflow-menu.js.map +1 -0
- package/dist/{composites → components}/button-group/overflow-menu-list.js +2 -2
- package/dist/components/button-group/overflow-menu-list.js.map +1 -0
- package/dist/{composites → components}/button-group/use-overflow.js +1 -1
- package/dist/{composites → components}/button-group/use-overflow.js.map +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js.map +1 -1
- package/dist/components/carousel/Carousel.d.ts +4 -1
- package/dist/components/carousel/Carousel.js +2 -4
- package/dist/components/carousel/Carousel.js.map +1 -1
- package/dist/components/checkbox/checkbox-styles.d.ts +3 -3
- package/dist/components/chip/Chip.d.ts +8 -2
- package/dist/components/chip/Chip.js +3 -8
- package/dist/components/chip/Chip.js.map +1 -1
- package/dist/components/chip/chip-tokens.d.ts +4 -4
- package/dist/components/date-picker/DateField.d.ts +4 -1
- package/dist/components/date-picker/DateField.js +3 -1
- package/dist/components/date-picker/DateField.js.map +1 -1
- package/dist/components/date-picker/DatePicker.d.ts +4 -1
- package/dist/components/date-picker/DatePicker.js +5 -2
- package/dist/components/date-picker/DatePicker.js.map +1 -1
- package/dist/components/date-picker/PickerDialog.d.ts +4 -1
- package/dist/components/date-picker/PickerDialog.js +3 -1
- package/dist/components/date-picker/PickerDialog.js.map +1 -1
- package/dist/components/date-picker/date-picker-styles.d.ts +12 -12
- package/dist/components/date-picker/date-picker-tokens.d.ts +1 -1
- package/dist/components/dialog/Dialog.d.ts +5 -2
- package/dist/components/dialog/Dialog.js +4 -2
- package/dist/components/dialog/Dialog.js.map +1 -1
- package/dist/components/dialog/dialog-tokens.d.ts +1 -1
- package/dist/components/fab-menu/FabMenu.js +2 -2
- package/dist/components/fab-menu/FabMenu.js.map +1 -1
- package/dist/components/icon-button/IconButton.d.ts +4 -2
- package/dist/components/icon-button/IconButton.js.map +1 -1
- package/dist/components/labels/LabelsProvider.d.ts +35 -0
- package/dist/components/labels/LabelsProvider.js +47 -0
- package/dist/components/labels/LabelsProvider.js.map +1 -0
- package/dist/components/labels/use-labels.d.ts +16 -0
- package/dist/components/labels/use-labels.js +42 -0
- package/dist/components/labels/use-labels.js.map +1 -0
- package/dist/components/menu/Menu.d.ts +17 -1
- package/dist/components/menu/Menu.js +21 -11
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/menu/menu-styles.js +12 -12
- package/dist/components/menu/menu-styles.js.map +1 -1
- package/dist/components/menu/menu-tokens.d.ts +1 -1
- package/dist/components/menu/menu-tokens.js.map +1 -1
- package/dist/components/navigation/NavItem.d.ts +1 -1
- package/dist/components/navigation/NavItem.js +9 -6
- package/dist/components/navigation/NavItem.js.map +1 -1
- package/dist/components/navigation/navigation-styles.js +1 -1
- package/dist/components/navigation/navigation-styles.js.map +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.d.ts +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.js +2 -4
- package/dist/components/pull-to-refresh/PullToRefresh.js.map +1 -1
- package/dist/components/search/SearchField.d.ts +4 -1
- package/dist/components/search/SearchField.js +2 -4
- package/dist/components/search/SearchField.js.map +1 -1
- package/dist/components/search/search-styles.js +2 -2
- package/dist/components/search/search-styles.js.map +1 -1
- package/dist/components/select/Select.d.ts +4 -1
- package/dist/components/select/Select.js +2 -4
- package/dist/components/select/Select.js.map +1 -1
- package/dist/components/sheet/BottomSheet.d.ts +4 -1
- package/dist/components/sheet/BottomSheet.js +2 -4
- package/dist/components/sheet/BottomSheet.js.map +1 -1
- package/dist/components/sheet/SideSheet.d.ts +4 -1
- package/dist/components/sheet/SideSheet.js +2 -4
- package/dist/components/sheet/SideSheet.js.map +1 -1
- package/dist/components/slider/Slider.d.ts +4 -1
- package/dist/components/slider/Slider.js +7 -5
- package/dist/components/slider/Slider.js.map +1 -1
- package/dist/components/snackbar/Snackbar.d.ts +8 -2
- package/dist/components/snackbar/Snackbar.js +2 -4
- package/dist/components/snackbar/Snackbar.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
- package/dist/components/split-button/SplitButton.d.ts +4 -1
- package/dist/components/split-button/SplitButton.js +4 -2
- package/dist/components/split-button/SplitButton.js.map +1 -1
- package/dist/components/switch/switch-styles.d.ts +3 -3
- package/dist/components/text-field/TextField.d.ts +4 -1
- package/dist/components/text-field/TextField.js +2 -4
- package/dist/components/text-field/TextField.js.map +1 -1
- package/dist/components/text-field/text-field-styles.js +2 -2
- package/dist/components/text-field/text-field-styles.js.map +1 -1
- package/dist/components/time-picker/TimeField.d.ts +24 -2
- package/dist/components/time-picker/TimeField.js +40 -12
- package/dist/components/time-picker/TimeField.js.map +1 -1
- package/dist/components/time-picker/TimeFieldList.js +95 -0
- package/dist/components/time-picker/TimeFieldList.js.map +1 -0
- package/dist/components/time-picker/TimePicker.d.ts +9 -5
- package/dist/components/time-picker/TimePicker.js +5 -2
- package/dist/components/time-picker/TimePicker.js.map +1 -1
- package/dist/components/time-picker/time-list.js +33 -0
- package/dist/components/time-picker/time-list.js.map +1 -0
- package/dist/components/tooltip/Tooltip.d.ts +1 -1
- package/dist/components/tooltip/tooltip-tokens.d.ts +1 -1
- package/dist/index.d.ts +40 -38
- package/dist/index.js +4 -3
- package/dist/primitives/use-m3-interaction.js +10 -6
- package/dist/primitives/use-m3-interaction.js.map +1 -1
- package/dist/primitives.d.ts +7 -4
- package/dist/primitives.js +4 -1
- package/dist/styles/utilities.css +22 -15
- package/dist/styles.compiled.css +1 -1
- package/dist/utils/use-scroll-keys.d.ts +29 -0
- package/dist/utils/use-scroll-keys.js +66 -0
- package/dist/utils/use-scroll-keys.js.map +1 -0
- package/dist/utils/use-scroll-restoration.d.ts +43 -0
- package/dist/utils/use-scroll-restoration.js +173 -0
- package/dist/utils/use-scroll-restoration.js.map +1 -0
- package/dist/windmorph-labels.d.ts +101 -0
- package/package.json +1 -1
- package/dist/composites/button-group/ButtonGroup.js.map +0 -1
- package/dist/composites/button-group/lazy-overflow-menu.js.map +0 -1
- package/dist/composites/button-group/overflow-menu-list.js.map +0 -1
|
@@ -29,6 +29,10 @@ export interface TimeFieldClassNames extends Omit<TextFieldClassNames, 'leadingI
|
|
|
29
29
|
root?: string;
|
|
30
30
|
/** The clock icon button. */
|
|
31
31
|
button?: string;
|
|
32
|
+
/** The docked list of times: its surface. */
|
|
33
|
+
list?: string;
|
|
34
|
+
/** Each time of the docked list. */
|
|
35
|
+
option?: string;
|
|
32
36
|
/** The picker dialog's surface. */
|
|
33
37
|
picker?: string;
|
|
34
38
|
}
|
|
@@ -49,6 +53,20 @@ export interface TimeFieldProps extends WrapperRootDOMProps {
|
|
|
49
53
|
hourCycle?: 12 | 24;
|
|
50
54
|
/** The field's style. @default "outlined" */
|
|
51
55
|
variant?: 'outlined' | 'filled';
|
|
56
|
+
/**
|
|
57
|
+
* Where the picker opens. `dialog` is the M3 time picker in a dialog. `docked` is a list of
|
|
58
|
+
* times under the field (every `step` minutes, within `min` and `max`) on windows 600px wide
|
|
59
|
+
* or more, for forms with many times; narrower windows still get the dialog. Not in the M3
|
|
60
|
+
* spec, which has no docked time picker.
|
|
61
|
+
* @default "dialog"
|
|
62
|
+
*/
|
|
63
|
+
presentation?: 'dialog' | 'docked';
|
|
64
|
+
/**
|
|
65
|
+
* Minutes between the times a docked list offers, counted from midnight; 5 at least. It
|
|
66
|
+
* only shapes the list: any time can still be typed.
|
|
67
|
+
* @default 30
|
|
68
|
+
*/
|
|
69
|
+
step?: number;
|
|
52
70
|
/** Whether the picker is open (controlled). */
|
|
53
71
|
open?: boolean;
|
|
54
72
|
/** Whether the picker starts open when uncontrolled. @default false */
|
|
@@ -89,7 +107,10 @@ export interface TimeFieldProps extends WrapperRootDOMProps {
|
|
|
89
107
|
* @default 0
|
|
90
108
|
*/
|
|
91
109
|
density?: Density;
|
|
92
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Words the field and its picker show and announce, with English defaults.
|
|
112
|
+
* A {@link LabelsProvider} sets them for the whole app.
|
|
113
|
+
*/
|
|
93
114
|
labels?: Partial<TimeFieldLabels>;
|
|
94
115
|
/** The text field's root. */
|
|
95
116
|
ref?: Ref<HTMLDivElement>;
|
|
@@ -108,7 +129,8 @@ export interface TimeFieldProps extends WrapperRootDOMProps {
|
|
|
108
129
|
* A time in a form, for pages that hold many of them, such as a weekly schedule. One compact
|
|
109
130
|
* field with no input mask that reads what people type or paste (`9`, `930`, `9:30`, `9h30`,
|
|
110
131
|
* `21:30`, `9pm`) and shows it in the active format once they leave it; a clock button opens
|
|
111
|
-
* the {@link TimePicker} in a dialog, typed first
|
|
132
|
+
* the {@link TimePicker} in a dialog, typed first, or with `presentation="docked"` a list of
|
|
133
|
+
* times under the field. The time sibling of `DateField`.
|
|
112
134
|
*
|
|
113
135
|
* @example
|
|
114
136
|
* <TimeField label="Opens at" value={time} onChange={setTime} />
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
3
4
|
import { onDemand, usePreloadOnIntent } from "../../utils/load-on-demand.js";
|
|
4
5
|
import { IconButton } from "../icon-button/IconButton.js";
|
|
5
6
|
import { DomDirectionLocale } from "../../primitives/DomDirectionLocale.js";
|
|
6
7
|
import { ariaFlag, pickDescriptionAria, splitRootProps } from "../../utils/root-props.js";
|
|
8
|
+
import { COMPACT_WINDOW, useMediaQuery } from "../date-picker/use-media-query.js";
|
|
7
9
|
import { TextField } from "../text-field/TextField.js";
|
|
8
10
|
import { ScheduleIcon } from "../date-picker/picker-icons.js";
|
|
9
11
|
import { localeHourCycle } from "./hour-cycle.js";
|
|
10
12
|
import { formatTime, parseTimeText } from "./time-format.js";
|
|
11
|
-
import { useRef, useState } from "react";
|
|
13
|
+
import { useId, useRef, useState } from "react";
|
|
12
14
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
13
15
|
import { useLocale, useObjectRef } from "react-aria";
|
|
14
16
|
import { useOverlayTriggerState } from "react-stately";
|
|
15
17
|
import { useControlledState } from "react-stately/useControlledState";
|
|
16
18
|
//#region src/components/time-picker/TimeField.tsx
|
|
17
19
|
const pickerDialog = onDemand(() => import("./TimeFieldDialog.js").then((module) => module.TimeFieldDialog));
|
|
20
|
+
const dockedList = onDemand(() => import("./TimeFieldList.js").then((module) => module.TimeFieldList));
|
|
18
21
|
const DEFAULT_LABELS = {
|
|
19
22
|
timeOutOfRange: "Time out of range",
|
|
20
23
|
openPicker: "Open time picker",
|
|
@@ -37,7 +40,8 @@ const DEFAULT_LABELS = {
|
|
|
37
40
|
* A time in a form, for pages that hold many of them, such as a weekly schedule. One compact
|
|
38
41
|
* field with no input mask that reads what people type or paste (`9`, `930`, `9:30`, `9h30`,
|
|
39
42
|
* `21:30`, `9pm`) and shows it in the active format once they leave it; a clock button opens
|
|
40
|
-
* the {@link TimePicker} in a dialog, typed first
|
|
43
|
+
* the {@link TimePicker} in a dialog, typed first, or with `presentation="docked"` a list of
|
|
44
|
+
* times under the field. The time sibling of `DateField`.
|
|
41
45
|
*
|
|
42
46
|
* @example
|
|
43
47
|
* <TimeField label="Opens at" value={time} onChange={setTime} />
|
|
@@ -48,19 +52,23 @@ function TimeField(props) {
|
|
|
48
52
|
directionRef
|
|
49
53
|
}) });
|
|
50
54
|
}
|
|
51
|
-
function TimeFieldInner({ label, value: valueProp, defaultValue, onChange, min, max, hourCycle: hourCycleProp, variant = "outlined", open, defaultOpen, onOpenChange, supportingText, invalid: invalidProp, errorMessage, required: requiredProp, disabled, readOnly: readOnlyProp, "aria-describedby": describedBy, "aria-details": details, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, "aria-readonly": ariaReadonly, name, id, clockIcon, density, labels: labelsProp, ref, inputRef, className, style, classNames, directionRef, ...rest }) {
|
|
55
|
+
function TimeFieldInner({ label, value: valueProp, defaultValue, onChange, min, max, hourCycle: hourCycleProp, variant = "outlined", presentation = "dialog", step = 30, open, defaultOpen, onOpenChange, supportingText, invalid: invalidProp, errorMessage, required: requiredProp, disabled, readOnly: readOnlyProp, "aria-describedby": describedBy, "aria-details": details, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, "aria-readonly": ariaReadonly, name, id, clockIcon, density, labels: labelsProp, ref, inputRef, className, style, classNames, directionRef, ...rest }) {
|
|
52
56
|
const { root, rest: aria } = splitRootProps(rest, { wrapper: true });
|
|
53
57
|
const invalid = invalidProp ?? ariaFlag(ariaInvalid);
|
|
54
58
|
const required = requiredProp ?? ariaFlag(ariaRequired);
|
|
55
59
|
const readOnly = readOnlyProp ?? ariaFlag(ariaReadonly);
|
|
56
60
|
const { locale } = useLocale();
|
|
57
61
|
const hourCycle = hourCycleProp ?? localeHourCycle(locale);
|
|
58
|
-
const
|
|
62
|
+
const words = useLabels([
|
|
63
|
+
"TimePicker",
|
|
64
|
+
"PickerDialog",
|
|
65
|
+
"TimeField"
|
|
66
|
+
], DEFAULT_LABELS, labelsProp);
|
|
67
|
+
const hint = words.timeFormatHint ?? (hourCycle === 24 ? "HH:MM" : "H:MM AM");
|
|
59
68
|
const labels = {
|
|
60
|
-
...
|
|
69
|
+
...words,
|
|
61
70
|
timeFormatHint: hint,
|
|
62
|
-
invalidTime: `Enter a time such as ${hint}
|
|
63
|
-
...labelsProp
|
|
71
|
+
invalidTime: words.invalidTime ?? `Enter a time such as ${hint}`
|
|
64
72
|
};
|
|
65
73
|
const [value, setValue] = useControlledState(valueProp, defaultValue ?? null, onChange);
|
|
66
74
|
const show = (time) => time ? formatTime(time, locale, hourCycle) : "";
|
|
@@ -80,7 +88,10 @@ function TimeFieldInner({ label, value: valueProp, defaultValue, onChange, min,
|
|
|
80
88
|
});
|
|
81
89
|
const fieldRef = useRef(null);
|
|
82
90
|
const domInputRef = useObjectRef(inputRef);
|
|
83
|
-
|
|
91
|
+
const compact = useMediaQuery(COMPACT_WINDOW);
|
|
92
|
+
const docked = presentation === "docked" && !compact;
|
|
93
|
+
const listId = useId();
|
|
94
|
+
usePreloadOnIntent(fieldRef, docked ? dockedList.preload : pickerDialog.preload, true);
|
|
84
95
|
const [opened, setOpened] = useState(overlay.isOpen);
|
|
85
96
|
if (overlay.isOpen && !opened) setOpened(true);
|
|
86
97
|
const outOfRange = (time) => min !== void 0 && time.compare(min) < 0 || max !== void 0 && time.compare(max) > 0;
|
|
@@ -111,7 +122,7 @@ function TimeFieldInner({ label, value: valueProp, defaultValue, onChange, min,
|
|
|
111
122
|
}
|
|
112
123
|
accept(time);
|
|
113
124
|
};
|
|
114
|
-
const { root: rootClassName, button, picker, ...fieldClassNames } = classNames ?? {};
|
|
125
|
+
const { root: rootClassName, button, picker, list, option, ...fieldClassNames } = classNames ?? {};
|
|
115
126
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(TextField, {
|
|
116
127
|
...root,
|
|
117
128
|
...pickDescriptionAria(aria),
|
|
@@ -158,8 +169,9 @@ function TimeFieldInner({ label, value: valueProp, defaultValue, onChange, min,
|
|
|
158
169
|
variant: "standard",
|
|
159
170
|
icon: clockIcon ?? /* @__PURE__ */ jsx(ScheduleIcon, {}),
|
|
160
171
|
"aria-label": labels.openPicker,
|
|
161
|
-
"aria-haspopup": "dialog",
|
|
172
|
+
"aria-haspopup": docked ? "listbox" : "dialog",
|
|
162
173
|
"aria-expanded": overlay.isOpen,
|
|
174
|
+
...docked && overlay.isOpen && { "aria-controls": listId },
|
|
163
175
|
disabled: disabled || readOnly,
|
|
164
176
|
className: button,
|
|
165
177
|
onPress: (event) => {
|
|
@@ -167,7 +179,23 @@ function TimeFieldInner({ label, value: valueProp, defaultValue, onChange, min,
|
|
|
167
179
|
overlay.open();
|
|
168
180
|
}
|
|
169
181
|
})
|
|
170
|
-
}), opened ? /* @__PURE__ */ jsx(
|
|
182
|
+
}), !opened ? null : docked ? /* @__PURE__ */ jsx(dockedList.Component, {
|
|
183
|
+
state: overlay,
|
|
184
|
+
triggerRef: fieldRef,
|
|
185
|
+
id: listId,
|
|
186
|
+
value,
|
|
187
|
+
onCommit: accept,
|
|
188
|
+
min,
|
|
189
|
+
max,
|
|
190
|
+
step,
|
|
191
|
+
hourCycle,
|
|
192
|
+
label: labels.title,
|
|
193
|
+
density,
|
|
194
|
+
classNames: {
|
|
195
|
+
list,
|
|
196
|
+
option
|
|
197
|
+
}
|
|
198
|
+
}) : /* @__PURE__ */ jsx(pickerDialog.Component, {
|
|
171
199
|
open: overlay.isOpen,
|
|
172
200
|
onOpenChange: overlay.setOpen,
|
|
173
201
|
value,
|
|
@@ -175,7 +203,7 @@ function TimeFieldInner({ label, value: valueProp, defaultValue, onChange, min,
|
|
|
175
203
|
hourCycle,
|
|
176
204
|
labels,
|
|
177
205
|
className: picker
|
|
178
|
-
})
|
|
206
|
+
})] });
|
|
179
207
|
}
|
|
180
208
|
//#endregion
|
|
181
209
|
export { TimeField };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeField.js","names":[],"sources":["../../../src/components/time-picker/TimeField.tsx"],"sourcesContent":["'use client';\n\nimport type { Time } from '@internationalized/date';\nimport {\n useRef,\n useState,\n type AriaAttributes,\n type CSSProperties,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { useLocale, useObjectRef } from 'react-aria';\nimport { useOverlayTriggerState } from 'react-stately';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { DomDirectionLocale } from '../../primitives/DomDirectionLocale';\nimport type { Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { onDemand, usePreloadOnIntent } from '../../utils/load-on-demand';\nimport {\n ariaFlag,\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { ScheduleIcon } from '../date-picker/picker-icons';\nimport type { PickerDialogLabels } from '../date-picker/PickerDialog';\nimport { IconButton } from '../icon-button/IconButton';\nimport { TextField, type TextFieldClassNames } from '../text-field/TextField';\nimport { localeHourCycle } from './hour-cycle';\nimport { formatTime, parseTimeText } from './time-format';\nimport type { TimeFieldDialogProps } from './TimeFieldDialog';\nimport type { TimePickerLabels } from './TimePicker';\n\n/** Words of a {@link TimeField} and its picker, for localisation. */\nexport interface TimeFieldLabels extends TimePickerLabels, PickerDialogLabels {\n /** The field's placeholder. @default the clock's format, \"HH:MM\" or \"H:MM AM\" */\n timeFormatHint: string;\n /** Error when the text isn't a time. @default \"Enter a time such as {hint}\" */\n invalidTime: string;\n /** Error for a time before `min` or after `max`. @default \"Time out of range\" */\n timeOutOfRange: string;\n /** The clock icon button. @default \"Open time picker\" */\n openPicker: string;\n /** The picker's title and name. @default \"Select time\" */\n title: string;\n /** The picker's dismissing action. @default \"Cancel\" */\n cancel: string;\n /** The picker's confirming action. @default \"OK\" */\n ok: string;\n}\n\n/** Classes for each part of a {@link TimeField}. */\nexport interface TimeFieldClassNames extends Omit<\n TextFieldClassNames,\n 'leadingIcon' | 'prefix' | 'suffix' | 'counter'\n> {\n /** The text field's root; `className` goes here too. */\n root?: string;\n /** The clock icon button. */\n button?: string;\n /** The picker dialog's surface. */\n picker?: string;\n}\n\nexport interface TimeFieldProps extends WrapperRootDOMProps {\n /** The field's label, e.g. \"Opens at\". */\n label: ReactNode;\n /** The time (controlled); `null` for none. */\n value?: Time | null;\n /** Initial time when uncontrolled. @default null */\n defaultValue?: Time | null;\n /** A time was typed and committed, or picked and confirmed; `null` when cleared. */\n onChange?: (value: Time | null) => void;\n /** Earliest time allowed. */\n min?: Time;\n /** Latest time allowed. */\n max?: Time;\n /** 12- or 24-hour clock, for the field and the picker. Left out, the locale's. @default the locale's */\n hourCycle?: 12 | 24;\n /** The field's style. @default \"outlined\" */\n variant?: 'outlined' | 'filled';\n /** Whether the picker is open (controlled). */\n open?: boolean;\n /** Whether the picker starts open when uncontrolled. @default false */\n defaultOpen?: boolean;\n /** Called when the picker opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** Helper text below the field. */\n supportingText?: ReactNode;\n /** Marks the field invalid, with `errorMessage` below it. */\n invalid?: boolean;\n /** Error text shown below the field while it's invalid, in place of `supportingText`. */\n errorMessage?: ReactNode;\n /** The field must have a time. @default false */\n required?: boolean;\n /** The field and its clock button can't be used. @default false */\n disabled?: boolean;\n /** The time can be read and focused but not changed. @default false */\n readOnly?: boolean;\n /** More description for the `<input>`, after the supporting or error text. */\n 'aria-describedby'?: string;\n /** The id of an element with more details about the `<input>`. */\n 'aria-details'?: string;\n /** Read as `invalid` when that's left out, for a form library that sets the attribute. */\n 'aria-invalid'?: AriaAttributes['aria-invalid'];\n /** Read as `required` when that's left out, for a form library that sets the attribute. */\n 'aria-required'?: AriaAttributes['aria-required'];\n /** Read as `readOnly` when that's left out, for a form library that sets the attribute. */\n 'aria-readonly'?: AriaAttributes['aria-readonly'];\n /** Form field name; the input submits the time as shown. */\n name?: string;\n /** The `<input>`'s id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** Icon of the button that opens the picker. */\n clockIcon?: ReactElement;\n /**\n * Web density of the field, `0` to `-3`: each step takes 4px off it (56px at 0, 48px at -2).\n * The picker keeps its size. Left out, the field takes the nearest theme's.\n * @default 0\n */\n density?: Density;\n /** Words the field and its picker show and announce, with English defaults. */\n labels?: Partial<TimeFieldLabels>;\n /** The text field's root. */\n ref?: Ref<HTMLDivElement>;\n /** The `<input>`. */\n inputRef?: Ref<HTMLInputElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. */\n classNames?: TimeFieldClassNames;\n /** `data-*` attributes go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nconst pickerDialog = onDemand<TimeFieldDialogProps>(() =>\n import('./TimeFieldDialog').then((module) => module.TimeFieldDialog),\n);\n\nconst DEFAULT_LABELS: Omit<TimeFieldLabels, 'timeFormatHint' | 'invalidTime'> = {\n timeOutOfRange: 'Time out of range',\n openPicker: 'Open time picker',\n title: 'Select time',\n cancel: 'Cancel',\n ok: 'OK',\n close: 'Close',\n save: 'Save',\n hour: 'Hour',\n minute: 'Minute',\n am: 'AM',\n pm: 'PM',\n period: 'AM or PM',\n dial: 'Clock',\n inputMode: 'Toggle input picker',\n dialMode: 'Toggle dial picker',\n dialValue: (value, total, unit) => `${value} ${unit} of ${total}`,\n};\n\n/**\n * A time in a form, for pages that hold many of them, such as a weekly schedule. One compact\n * field with no input mask that reads what people type or paste (`9`, `930`, `9:30`, `9h30`,\n * `21:30`, `9pm`) and shows it in the active format once they leave it; a clock button opens\n * the {@link TimePicker} in a dialog, typed first. The time sibling of `DateField`.\n *\n * @example\n * <TimeField label=\"Opens at\" value={time} onChange={setTime} />\n */\nexport function TimeField(props: TimeFieldProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <TimeFieldInner {...props} directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\nfunction TimeFieldInner({\n label,\n value: valueProp,\n defaultValue,\n onChange,\n min,\n max,\n hourCycle: hourCycleProp,\n variant = 'outlined',\n open,\n defaultOpen,\n onOpenChange,\n supportingText,\n invalid: invalidProp,\n errorMessage,\n required: requiredProp,\n disabled,\n readOnly: readOnlyProp,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n 'aria-invalid': ariaInvalid,\n 'aria-required': ariaRequired,\n 'aria-readonly': ariaReadonly,\n name,\n id,\n clockIcon,\n density,\n labels: labelsProp,\n ref,\n inputRef,\n className,\n style,\n classNames,\n directionRef,\n ...rest\n}: TimeFieldProps & { directionRef: (element: HTMLElement | null) => void }) {\n const { root, rest: aria } = splitRootProps(rest, { wrapper: true });\n const invalid = invalidProp ?? ariaFlag(ariaInvalid);\n const required = requiredProp ?? ariaFlag(ariaRequired);\n const readOnly = readOnlyProp ?? ariaFlag(ariaReadonly);\n const { locale } = useLocale();\n const hourCycle = hourCycleProp ?? localeHourCycle(locale);\n const hint = labelsProp?.timeFormatHint ?? (hourCycle === 24 ? 'HH:MM' : 'H:MM AM');\n const labels: TimeFieldLabels = {\n ...DEFAULT_LABELS,\n timeFormatHint: hint,\n invalidTime: `Enter a time such as ${hint}`,\n ...labelsProp,\n };\n\n const [value, setValue] = useControlledState(valueProp, defaultValue ?? null, onChange);\n const show = (time: Time | null) => (time ? formatTime(time, locale, hourCycle) : '');\n const [text, setText] = useState(() => show(value));\n const [error, setError] = useState<string | null>(null);\n // A new value or clock from outside replaces what is typed.\n const key = `${value?.toString() ?? ''}|${hourCycle}|${locale}`;\n const [shown, setShown] = useState(key);\n if (key !== shown) {\n setShown(key);\n setText(show(value));\n setError(null);\n }\n\n const overlay = useOverlayTriggerState({\n ...(open !== undefined && { isOpen: open }),\n ...(defaultOpen !== undefined && { defaultOpen }),\n ...(onOpenChange && { onOpenChange }),\n });\n const fieldRef = useRef<HTMLDivElement | null>(null);\n const domInputRef = useObjectRef(inputRef);\n usePreloadOnIntent(fieldRef, pickerDialog.preload, true);\n const [opened, setOpened] = useState(overlay.isOpen);\n if (overlay.isOpen && !opened) setOpened(true);\n\n const outOfRange = (time: Time) =>\n (min !== undefined && time.compare(min) < 0) || (max !== undefined && time.compare(max) > 0);\n\n const accept = (time: Time) => {\n if (outOfRange(time)) {\n setText(show(time));\n setError(labels.timeOutOfRange);\n return;\n }\n setError(null);\n setText(show(time));\n if (time.toString() !== value?.toString()) setValue(time);\n };\n\n const commit = () => {\n if (text.trim() === '') {\n setError(null);\n if (value) setValue(null);\n return;\n }\n // Unchanged text needs no reading: it is already this value's format.\n if (value && text === show(value)) {\n setError(null);\n return;\n }\n const time = parseTimeText(text, locale);\n if (!time) {\n setError(labels.invalidTime);\n return;\n }\n accept(time);\n };\n\n const { root: rootClassName, button, picker, ...fieldClassNames } = classNames ?? {};\n return (\n <>\n <TextField\n {...root}\n {...pickDescriptionAria(aria)}\n {...(describedBy !== undefined && { 'aria-describedby': describedBy })}\n {...(details !== undefined && { 'aria-details': details })}\n {...(id !== undefined && { id })}\n ref={(element: HTMLDivElement | null) => {\n fieldRef.current = element;\n directionRef(element);\n if (typeof ref === 'function') ref(element);\n else if (ref) ref.current = element;\n }}\n style={style}\n inputRef={domInputRef}\n variant={variant}\n {...(density !== undefined && { density })}\n label={label}\n value={text}\n onChange={(next: string) => {\n setText(next);\n if (error) setError(null);\n }}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key !== 'Enter') {\n event.continuePropagation();\n return;\n }\n commit();\n }}\n placeholder={hint}\n supportingText={supportingText}\n invalid={invalid || error !== null}\n errorMessage={error ?? errorMessage}\n // A 24-hour time needs only digits and a separator; \"pm\" needs letters.\n inputMode={hourCycle === 24 ? 'decimal' : 'text'}\n autoComplete=\"off\"\n required={required}\n disabled={disabled}\n readOnly={readOnly}\n name={name}\n className={cn('w-[9.25rem] max-w-full', rootClassName, className)}\n classNames={fieldClassNames}\n trailingIcon={\n <IconButton\n variant=\"standard\"\n icon={clockIcon ?? <ScheduleIcon />}\n aria-label={labels.openPicker}\n aria-haspopup=\"dialog\"\n aria-expanded={overlay.isOpen}\n disabled={disabled || readOnly}\n className={button}\n onPress={(event) => {\n // Focus returns to where it was when the dialog opened: the text box, except\n // after a touch, where focusing it would raise the on-screen keyboard.\n if (event.pointerType !== 'touch') {\n domInputRef.current?.focus({ preventScroll: true });\n }\n overlay.open();\n }}\n />\n }\n />\n {opened ? (\n <pickerDialog.Component\n open={overlay.isOpen}\n onOpenChange={overlay.setOpen}\n value={value}\n onCommit={accept}\n hourCycle={hourCycle}\n labels={labels}\n className={picker}\n />\n ) : null}\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TimeField.js","names":[],"sources":["../../../src/components/time-picker/TimeField.tsx"],"sourcesContent":["'use client';\n\nimport type { Time } from '@internationalized/date';\nimport {\n useId,\n useRef,\n useState,\n type AriaAttributes,\n type CSSProperties,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { useLocale, useObjectRef } from 'react-aria';\nimport { useOverlayTriggerState } from 'react-stately';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { useLabels } from '../labels/use-labels';\nimport { DomDirectionLocale } from '../../primitives/DomDirectionLocale';\nimport type { Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { onDemand, usePreloadOnIntent } from '../../utils/load-on-demand';\nimport {\n ariaFlag,\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { ScheduleIcon } from '../date-picker/picker-icons';\nimport { COMPACT_WINDOW, useMediaQuery } from '../date-picker/use-media-query';\nimport type { PickerDialogLabels } from '../date-picker/PickerDialog';\nimport { IconButton } from '../icon-button/IconButton';\nimport { TextField, type TextFieldClassNames } from '../text-field/TextField';\nimport { localeHourCycle } from './hour-cycle';\nimport { formatTime, parseTimeText } from './time-format';\nimport type { TimeFieldDialogProps } from './TimeFieldDialog';\nimport type { TimeFieldListProps } from './TimeFieldList';\nimport type { TimePickerLabels } from './TimePicker';\n\n/** Words of a {@link TimeField} and its picker, for localisation. */\nexport interface TimeFieldLabels extends TimePickerLabels, PickerDialogLabels {\n /** The field's placeholder. @default the clock's format, \"HH:MM\" or \"H:MM AM\" */\n timeFormatHint: string;\n /** Error when the text isn't a time. @default \"Enter a time such as {hint}\" */\n invalidTime: string;\n /** Error for a time before `min` or after `max`. @default \"Time out of range\" */\n timeOutOfRange: string;\n /** The clock icon button. @default \"Open time picker\" */\n openPicker: string;\n /** The picker's title and name. @default \"Select time\" */\n title: string;\n /** The picker's dismissing action. @default \"Cancel\" */\n cancel: string;\n /** The picker's confirming action. @default \"OK\" */\n ok: string;\n}\n\n/** Classes for each part of a {@link TimeField}. */\nexport interface TimeFieldClassNames extends Omit<\n TextFieldClassNames,\n 'leadingIcon' | 'prefix' | 'suffix' | 'counter'\n> {\n /** The text field's root; `className` goes here too. */\n root?: string;\n /** The clock icon button. */\n button?: string;\n /** The docked list of times: its surface. */\n list?: string;\n /** Each time of the docked list. */\n option?: string;\n /** The picker dialog's surface. */\n picker?: string;\n}\n\nexport interface TimeFieldProps extends WrapperRootDOMProps {\n /** The field's label, e.g. \"Opens at\". */\n label: ReactNode;\n /** The time (controlled); `null` for none. */\n value?: Time | null;\n /** Initial time when uncontrolled. @default null */\n defaultValue?: Time | null;\n /** A time was typed and committed, or picked and confirmed; `null` when cleared. */\n onChange?: (value: Time | null) => void;\n /** Earliest time allowed. */\n min?: Time;\n /** Latest time allowed. */\n max?: Time;\n /** 12- or 24-hour clock, for the field and the picker. Left out, the locale's. @default the locale's */\n hourCycle?: 12 | 24;\n /** The field's style. @default \"outlined\" */\n variant?: 'outlined' | 'filled';\n /**\n * Where the picker opens. `dialog` is the M3 time picker in a dialog. `docked` is a list of\n * times under the field (every `step` minutes, within `min` and `max`) on windows 600px wide\n * or more, for forms with many times; narrower windows still get the dialog. Not in the M3\n * spec, which has no docked time picker.\n * @default \"dialog\"\n */\n presentation?: 'dialog' | 'docked';\n /**\n * Minutes between the times a docked list offers, counted from midnight; 5 at least. It\n * only shapes the list: any time can still be typed.\n * @default 30\n */\n step?: number;\n /** Whether the picker is open (controlled). */\n open?: boolean;\n /** Whether the picker starts open when uncontrolled. @default false */\n defaultOpen?: boolean;\n /** Called when the picker opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** Helper text below the field. */\n supportingText?: ReactNode;\n /** Marks the field invalid, with `errorMessage` below it. */\n invalid?: boolean;\n /** Error text shown below the field while it's invalid, in place of `supportingText`. */\n errorMessage?: ReactNode;\n /** The field must have a time. @default false */\n required?: boolean;\n /** The field and its clock button can't be used. @default false */\n disabled?: boolean;\n /** The time can be read and focused but not changed. @default false */\n readOnly?: boolean;\n /** More description for the `<input>`, after the supporting or error text. */\n 'aria-describedby'?: string;\n /** The id of an element with more details about the `<input>`. */\n 'aria-details'?: string;\n /** Read as `invalid` when that's left out, for a form library that sets the attribute. */\n 'aria-invalid'?: AriaAttributes['aria-invalid'];\n /** Read as `required` when that's left out, for a form library that sets the attribute. */\n 'aria-required'?: AriaAttributes['aria-required'];\n /** Read as `readOnly` when that's left out, for a form library that sets the attribute. */\n 'aria-readonly'?: AriaAttributes['aria-readonly'];\n /** Form field name; the input submits the time as shown. */\n name?: string;\n /** The `<input>`'s id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** Icon of the button that opens the picker. */\n clockIcon?: ReactElement;\n /**\n * Web density of the field, `0` to `-3`: each step takes 4px off it (56px at 0, 48px at -2).\n * The picker keeps its size. Left out, the field takes the nearest theme's.\n * @default 0\n */\n density?: Density;\n /**\n * Words the field and its picker show and announce, with English defaults.\n * A {@link LabelsProvider} sets them for the whole app.\n */\n labels?: Partial<TimeFieldLabels>;\n /** The text field's root. */\n ref?: Ref<HTMLDivElement>;\n /** The `<input>`. */\n inputRef?: Ref<HTMLInputElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. */\n classNames?: TimeFieldClassNames;\n /** `data-*` attributes go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nconst pickerDialog = onDemand<TimeFieldDialogProps>(() =>\n import('./TimeFieldDialog').then((module) => module.TimeFieldDialog),\n);\n\nconst dockedList = onDemand<TimeFieldListProps>(() =>\n import('./TimeFieldList').then((module) => module.TimeFieldList),\n);\n\nconst DEFAULT_LABELS: Omit<TimeFieldLabels, 'timeFormatHint' | 'invalidTime'> = {\n timeOutOfRange: 'Time out of range',\n openPicker: 'Open time picker',\n title: 'Select time',\n cancel: 'Cancel',\n ok: 'OK',\n close: 'Close',\n save: 'Save',\n hour: 'Hour',\n minute: 'Minute',\n am: 'AM',\n pm: 'PM',\n period: 'AM or PM',\n dial: 'Clock',\n inputMode: 'Toggle input picker',\n dialMode: 'Toggle dial picker',\n dialValue: (value, total, unit) => `${value} ${unit} of ${total}`,\n};\n\n/**\n * A time in a form, for pages that hold many of them, such as a weekly schedule. One compact\n * field with no input mask that reads what people type or paste (`9`, `930`, `9:30`, `9h30`,\n * `21:30`, `9pm`) and shows it in the active format once they leave it; a clock button opens\n * the {@link TimePicker} in a dialog, typed first, or with `presentation=\"docked\"` a list of\n * times under the field. The time sibling of `DateField`.\n *\n * @example\n * <TimeField label=\"Opens at\" value={time} onChange={setTime} />\n */\nexport function TimeField(props: TimeFieldProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <TimeFieldInner {...props} directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\nfunction TimeFieldInner({\n label,\n value: valueProp,\n defaultValue,\n onChange,\n min,\n max,\n hourCycle: hourCycleProp,\n variant = 'outlined',\n presentation = 'dialog',\n step = 30,\n open,\n defaultOpen,\n onOpenChange,\n supportingText,\n invalid: invalidProp,\n errorMessage,\n required: requiredProp,\n disabled,\n readOnly: readOnlyProp,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n 'aria-invalid': ariaInvalid,\n 'aria-required': ariaRequired,\n 'aria-readonly': ariaReadonly,\n name,\n id,\n clockIcon,\n density,\n labels: labelsProp,\n ref,\n inputRef,\n className,\n style,\n classNames,\n directionRef,\n ...rest\n}: TimeFieldProps & { directionRef: (element: HTMLElement | null) => void }) {\n const { root, rest: aria } = splitRootProps(rest, { wrapper: true });\n const invalid = invalidProp ?? ariaFlag(ariaInvalid);\n const required = requiredProp ?? ariaFlag(ariaRequired);\n const readOnly = readOnlyProp ?? ariaFlag(ariaReadonly);\n const { locale } = useLocale();\n const hourCycle = hourCycleProp ?? localeHourCycle(locale);\n const words = useLabels(['TimePicker', 'PickerDialog', 'TimeField'], DEFAULT_LABELS, labelsProp);\n const hint = words.timeFormatHint ?? (hourCycle === 24 ? 'HH:MM' : 'H:MM AM');\n const labels: TimeFieldLabels = {\n ...words,\n timeFormatHint: hint,\n invalidTime: words.invalidTime ?? `Enter a time such as ${hint}`,\n };\n\n const [value, setValue] = useControlledState(valueProp, defaultValue ?? null, onChange);\n const show = (time: Time | null) => (time ? formatTime(time, locale, hourCycle) : '');\n const [text, setText] = useState(() => show(value));\n const [error, setError] = useState<string | null>(null);\n // A new value or clock from outside replaces what is typed.\n const key = `${value?.toString() ?? ''}|${hourCycle}|${locale}`;\n const [shown, setShown] = useState(key);\n if (key !== shown) {\n setShown(key);\n setText(show(value));\n setError(null);\n }\n\n const overlay = useOverlayTriggerState({\n ...(open !== undefined && { isOpen: open }),\n ...(defaultOpen !== undefined && { defaultOpen }),\n ...(onOpenChange && { onOpenChange }),\n });\n const fieldRef = useRef<HTMLDivElement | null>(null);\n const domInputRef = useObjectRef(inputRef);\n // A docked list needs room under the field; a compact window gets the dialog, full screen.\n const compact = useMediaQuery(COMPACT_WINDOW);\n const docked = presentation === 'docked' && !compact;\n const listId = useId();\n usePreloadOnIntent(fieldRef, docked ? dockedList.preload : pickerDialog.preload, true);\n const [opened, setOpened] = useState(overlay.isOpen);\n if (overlay.isOpen && !opened) setOpened(true);\n\n const outOfRange = (time: Time) =>\n (min !== undefined && time.compare(min) < 0) || (max !== undefined && time.compare(max) > 0);\n\n const accept = (time: Time) => {\n if (outOfRange(time)) {\n setText(show(time));\n setError(labels.timeOutOfRange);\n return;\n }\n setError(null);\n setText(show(time));\n if (time.toString() !== value?.toString()) setValue(time);\n };\n\n const commit = () => {\n if (text.trim() === '') {\n setError(null);\n if (value) setValue(null);\n return;\n }\n // Unchanged text needs no reading: it is already this value's format.\n if (value && text === show(value)) {\n setError(null);\n return;\n }\n const time = parseTimeText(text, locale);\n if (!time) {\n setError(labels.invalidTime);\n return;\n }\n accept(time);\n };\n\n const {\n root: rootClassName,\n button,\n picker,\n list,\n option,\n ...fieldClassNames\n } = classNames ?? {};\n return (\n <>\n <TextField\n {...root}\n {...pickDescriptionAria(aria)}\n {...(describedBy !== undefined && { 'aria-describedby': describedBy })}\n {...(details !== undefined && { 'aria-details': details })}\n {...(id !== undefined && { id })}\n ref={(element: HTMLDivElement | null) => {\n fieldRef.current = element;\n directionRef(element);\n if (typeof ref === 'function') ref(element);\n else if (ref) ref.current = element;\n }}\n style={style}\n inputRef={domInputRef}\n variant={variant}\n {...(density !== undefined && { density })}\n label={label}\n value={text}\n onChange={(next: string) => {\n setText(next);\n if (error) setError(null);\n }}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key !== 'Enter') {\n event.continuePropagation();\n return;\n }\n commit();\n }}\n placeholder={hint}\n supportingText={supportingText}\n invalid={invalid || error !== null}\n errorMessage={error ?? errorMessage}\n // A 24-hour time needs only digits and a separator; \"pm\" needs letters.\n inputMode={hourCycle === 24 ? 'decimal' : 'text'}\n autoComplete=\"off\"\n required={required}\n disabled={disabled}\n readOnly={readOnly}\n name={name}\n className={cn('w-[9.25rem] max-w-full', rootClassName, className)}\n classNames={fieldClassNames}\n trailingIcon={\n <IconButton\n variant=\"standard\"\n icon={clockIcon ?? <ScheduleIcon />}\n aria-label={labels.openPicker}\n aria-haspopup={docked ? 'listbox' : 'dialog'}\n aria-expanded={overlay.isOpen}\n {...(docked && overlay.isOpen && { 'aria-controls': listId })}\n disabled={disabled || readOnly}\n className={button}\n onPress={(event) => {\n // Focus returns to where it was when the picker opened: the text box, except\n // after a touch, where focusing it would raise the on-screen keyboard.\n if (event.pointerType !== 'touch') {\n domInputRef.current?.focus({ preventScroll: true });\n }\n overlay.open();\n }}\n />\n }\n />\n {!opened ? null : docked ? (\n <dockedList.Component\n state={overlay}\n triggerRef={fieldRef}\n id={listId}\n value={value}\n onCommit={accept}\n min={min}\n max={max}\n step={step}\n hourCycle={hourCycle}\n label={labels.title}\n density={density}\n classNames={{ list, option }}\n />\n ) : (\n <pickerDialog.Component\n open={overlay.isOpen}\n onOpenChange={overlay.setOpen}\n value={value}\n onCommit={accept}\n hourCycle={hourCycle}\n labels={labels}\n className={picker}\n />\n )}\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { OptionList, OptionPopover } from "../select/option-list.js";
|
|
3
|
+
import { formatTime } from "./time-format.js";
|
|
4
|
+
import { listTimes, minutesOf, nearestTime } from "./time-list.js";
|
|
5
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
import { useLocale } from "react-aria";
|
|
8
|
+
import { Item, useListState } from "react-stately";
|
|
9
|
+
//#region src/components/time-picker/TimeFieldList.tsx
|
|
10
|
+
/**
|
|
11
|
+
* The docked picker of a {@link TimeField}: a list of times under the field, drawn as the
|
|
12
|
+
* library's menu (the popover and options of `Select`). It opens at the field's time, or with
|
|
13
|
+
* none at the time nearest now; a press or Enter picks a time and closes it. Loaded on
|
|
14
|
+
* demand, so a form of many fields carries none of it.
|
|
15
|
+
*/
|
|
16
|
+
function TimeFieldList({ state, triggerRef, id, value, onCommit, min, max, step, hourCycle, label, density, classNames }) {
|
|
17
|
+
const { locale } = useLocale();
|
|
18
|
+
const popoverRef = useRef(null);
|
|
19
|
+
const listRef = useRef(null);
|
|
20
|
+
const options = useMemo(() => listTimes(step, min, max, value).map((time) => ({
|
|
21
|
+
key: String(minutesOf(time)),
|
|
22
|
+
time,
|
|
23
|
+
text: formatTime(time, locale, hourCycle)
|
|
24
|
+
})), [
|
|
25
|
+
step,
|
|
26
|
+
min,
|
|
27
|
+
max,
|
|
28
|
+
value,
|
|
29
|
+
locale,
|
|
30
|
+
hourCycle
|
|
31
|
+
]);
|
|
32
|
+
const chosen = value ? String(minutesOf(value)) : null;
|
|
33
|
+
const selected = chosen !== null && options.some((option) => option.key === chosen);
|
|
34
|
+
const list = useListState({
|
|
35
|
+
items: options,
|
|
36
|
+
children: (option) => /* @__PURE__ */ jsx(Item, {
|
|
37
|
+
textValue: option.text,
|
|
38
|
+
children: option.text
|
|
39
|
+
}, option.key),
|
|
40
|
+
selectionMode: "single",
|
|
41
|
+
disallowEmptySelection: true,
|
|
42
|
+
allowDuplicateSelectionEvents: true,
|
|
43
|
+
selectedKeys: selected ? [chosen] : [],
|
|
44
|
+
onSelectionChange: (keys) => {
|
|
45
|
+
const [key] = keys === "all" ? [] : [...keys];
|
|
46
|
+
const picked = options.find((option) => option.key === key);
|
|
47
|
+
if (picked) onCommit(picked.time);
|
|
48
|
+
state.close();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const { isOpen } = state;
|
|
52
|
+
const { selectionManager } = list;
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!isOpen) return;
|
|
55
|
+
let start = chosen;
|
|
56
|
+
if (!selected) {
|
|
57
|
+
const now = /* @__PURE__ */ new Date();
|
|
58
|
+
const nearest = nearestTime(options.map((option) => option.time), now.getHours() * 60 + now.getMinutes());
|
|
59
|
+
start = String(minutesOf(nearest ?? options[0].time));
|
|
60
|
+
selectionManager.setFocused(true);
|
|
61
|
+
selectionManager.setFocusedKey(start);
|
|
62
|
+
}
|
|
63
|
+
const scroller = listRef.current;
|
|
64
|
+
const option = scroller?.querySelector(`[data-key="${start}"]`);
|
|
65
|
+
if (!scroller || !option) return;
|
|
66
|
+
const offset = option.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
|
|
67
|
+
scroller.scrollTop += offset - (scroller.clientHeight - option.offsetHeight) / 2;
|
|
68
|
+
}, [isOpen]);
|
|
69
|
+
return /* @__PURE__ */ jsx(OptionPopover, {
|
|
70
|
+
state,
|
|
71
|
+
triggerRef,
|
|
72
|
+
popoverRef,
|
|
73
|
+
onPressOutside: state.close,
|
|
74
|
+
density,
|
|
75
|
+
children: /* @__PURE__ */ jsx(OptionList, {
|
|
76
|
+
listBoxProps: {
|
|
77
|
+
id,
|
|
78
|
+
"aria-label": label,
|
|
79
|
+
autoFocus: true,
|
|
80
|
+
disallowEmptySelection: true
|
|
81
|
+
},
|
|
82
|
+
state: list,
|
|
83
|
+
listBoxRef: listRef,
|
|
84
|
+
emptyLabel: null,
|
|
85
|
+
classNames: {
|
|
86
|
+
list: classNames?.list,
|
|
87
|
+
item: classNames?.option
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { TimeFieldList };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=TimeFieldList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeFieldList.js","names":[],"sources":["../../../src/components/time-picker/TimeFieldList.tsx"],"sourcesContent":["'use client';\n\nimport type { Time } from '@internationalized/date';\nimport { useEffect, useMemo, useRef, type RefObject } from 'react';\nimport { useLocale } from 'react-aria';\nimport { Item, useListState, type OverlayTriggerState } from 'react-stately';\nimport type { Density } from '../../tokens/density';\nimport { OptionList, OptionPopover } from '../select/option-list';\nimport { formatTime } from './time-format';\nimport { listTimes, minutesOf, nearestTime } from './time-list';\n\nexport interface TimeFieldListProps {\n /** The field's open state, which the list follows. */\n state: OverlayTriggerState;\n /** The field: the list opens under it, at least as wide. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The list's id, which the clock button controls. */\n id: string;\n value: Time | null;\n /** A time was picked. */\n onCommit: (time: Time) => void;\n min?: Time | undefined;\n max?: Time | undefined;\n /** Minutes between the listed times. */\n step: number;\n hourCycle: 12 | 24;\n /** The list's name. */\n label: string;\n density?: Density | undefined;\n classNames?: { list?: string | undefined; option?: string | undefined } | undefined;\n}\n\ninterface TimeOption {\n key: string;\n time: Time;\n text: string;\n}\n\n/**\n * The docked picker of a {@link TimeField}: a list of times under the field, drawn as the\n * library's menu (the popover and options of `Select`). It opens at the field's time, or with\n * none at the time nearest now; a press or Enter picks a time and closes it. Loaded on\n * demand, so a form of many fields carries none of it.\n */\nexport function TimeFieldList({\n state,\n triggerRef,\n id,\n value,\n onCommit,\n min,\n max,\n step,\n hourCycle,\n label,\n density,\n classNames,\n}: TimeFieldListProps) {\n const { locale } = useLocale();\n const popoverRef = useRef<HTMLDivElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const options = useMemo<TimeOption[]>(\n () =>\n listTimes(step, min, max, value).map((time) => ({\n key: String(minutesOf(time)),\n time,\n text: formatTime(time, locale, hourCycle),\n })),\n [step, min, max, value, locale, hourCycle],\n );\n const chosen = value ? String(minutesOf(value)) : null;\n const selected = chosen !== null && options.some((option) => option.key === chosen);\n const list = useListState<TimeOption>({\n items: options,\n children: (option) => (\n <Item key={option.key} textValue={option.text}>\n {option.text}\n </Item>\n ),\n selectionMode: 'single',\n disallowEmptySelection: true,\n // Picking the field's own time again still closes the list.\n allowDuplicateSelectionEvents: true,\n selectedKeys: selected ? [chosen] : [],\n onSelectionChange: (keys) => {\n const [key] = keys === 'all' ? [] : [...keys];\n const picked = options.find((option) => option.key === key);\n if (picked) onCommit(picked.time);\n state.close();\n },\n });\n\n // As it opens, the list shows where the field is: its time in the middle, or with none the\n // time nearest now, which takes focus. Only the list scrolls, never the page.\n const { isOpen } = state;\n const { selectionManager } = list;\n useEffect(() => {\n if (!isOpen) return;\n let start = chosen;\n if (!selected) {\n const now = new Date();\n const nearest = nearestTime(\n options.map((option) => option.time),\n now.getHours() * 60 + now.getMinutes(),\n );\n start = String(minutesOf(nearest ?? options[0]!.time));\n selectionManager.setFocused(true);\n selectionManager.setFocusedKey(start);\n }\n const scroller = listRef.current;\n const option = scroller?.querySelector<HTMLElement>(`[data-key=\"${start}\"]`);\n if (!scroller || !option) return;\n const offset = option.getBoundingClientRect().top - scroller.getBoundingClientRect().top;\n scroller.scrollTop += offset - (scroller.clientHeight - option.offsetHeight) / 2;\n // Once per opening: the person scrolls from there.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen]);\n\n return (\n <OptionPopover\n state={state}\n triggerRef={triggerRef}\n popoverRef={popoverRef}\n onPressOutside={state.close}\n density={density}\n >\n <OptionList\n // A time is always chosen once one is, so Escape closes the list instead of clearing it.\n listBoxProps={{\n id,\n 'aria-label': label,\n autoFocus: true,\n disallowEmptySelection: true,\n }}\n state={list}\n listBoxRef={listRef}\n emptyLabel={null}\n classNames={{ list: classNames?.list, item: classNames?.option }}\n />\n </OptionPopover>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AA4CA,SAAgB,cAAc,EAC5B,OACA,YACA,IACA,OACA,UACA,KACA,KACA,MACA,WACA,OACA,SACA,cACqB;CACrB,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,aAAa,OAAuB,IAAI;CAC9C,MAAM,UAAU,OAAyB,IAAI;CAC7C,MAAM,UAAU,cAEZ,UAAU,MAAM,KAAK,KAAK,KAAK,CAAC,CAAC,KAAK,UAAU;EAC9C,KAAK,OAAO,UAAU,IAAI,CAAC;EAC3B;EACA,MAAM,WAAW,MAAM,QAAQ,SAAS;CAC1C,EAAE,GACJ;EAAC;EAAM;EAAK;EAAK;EAAO;EAAQ;CAAS,CAC3C;CACA,MAAM,SAAS,QAAQ,OAAO,UAAU,KAAK,CAAC,IAAI;CAClD,MAAM,WAAW,WAAW,QAAQ,QAAQ,MAAM,WAAW,OAAO,QAAQ,MAAM;CAClF,MAAM,OAAO,aAAyB;EACpC,OAAO;EACP,WAAW,WACT,oBAAC,MAAD;GAAuB,WAAW,OAAO;GACtC,UAAA,OAAO;EACJ,GAFK,OAAO,GAEZ;EAER,eAAe;EACf,wBAAwB;EAExB,+BAA+B;EAC/B,cAAc,WAAW,CAAC,MAAM,IAAI,CAAC;EACrC,oBAAoB,SAAS;GAC3B,MAAM,CAAC,OAAO,SAAS,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI;GAC5C,MAAM,SAAS,QAAQ,MAAM,WAAW,OAAO,QAAQ,GAAG;GAC1D,IAAI,QAAQ,SAAS,OAAO,IAAI;GAChC,MAAM,MAAM;EACd;CACF,CAAC;CAID,MAAM,EAAE,WAAW;CACnB,MAAM,EAAE,qBAAqB;CAC7B,gBAAgB;EACd,IAAI,CAAC,QAAQ;EACb,IAAI,QAAQ;EACZ,IAAI,CAAC,UAAU;GACb,MAAM,sBAAM,IAAI,KAAK;GACrB,MAAM,UAAU,YACd,QAAQ,KAAK,WAAW,OAAO,IAAI,GACnC,IAAI,SAAS,IAAI,KAAK,IAAI,WAAW,CACvC;GACA,QAAQ,OAAO,UAAU,WAAW,QAAQ,EAAE,CAAE,IAAI,CAAC;GACrD,iBAAiB,WAAW,IAAI;GAChC,iBAAiB,cAAc,KAAK;EACtC;EACA,MAAM,WAAW,QAAQ;EACzB,MAAM,SAAS,UAAU,cAA2B,cAAc,MAAM,GAAG;EAC3E,IAAI,CAAC,YAAY,CAAC,QAAQ;EAC1B,MAAM,SAAS,OAAO,sBAAsB,CAAC,CAAC,MAAM,SAAS,sBAAsB,CAAC,CAAC;EACrF,SAAS,aAAa,UAAU,SAAS,eAAe,OAAO,gBAAgB;CAGjF,GAAG,CAAC,MAAM,CAAC;CAEX,OACE,oBAAC,eAAD;EACS;EACK;EACA;EACZ,gBAAgB,MAAM;EACb;EAET,UAAA,oBAAC,YAAD;GAEE,cAAc;IACZ;IACA,cAAc;IACd,WAAW;IACX,wBAAwB;GAC1B;GACA,OAAO;GACP,YAAY;GACZ,YAAY;GACZ,YAAY;IAAE,MAAM,YAAY;IAAM,MAAM,YAAY;GAAO;EAChE,CAAA;CACY,CAAA;AAEnB"}
|
|
@@ -66,7 +66,10 @@ export interface TimePickerProps extends Omit<WrapperRootDOMProps, DescriptionAr
|
|
|
66
66
|
* time picker placement and adaptive design). @default "auto"
|
|
67
67
|
*/
|
|
68
68
|
layout?: 'vertical' | 'horizontal' | 'auto';
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Words the picker shows and announces, with English defaults. A {@link LabelsProvider} sets them
|
|
71
|
+
* for the whole app.
|
|
72
|
+
*/
|
|
70
73
|
labels?: Partial<TimePickerLabels>;
|
|
71
74
|
/**
|
|
72
75
|
* Web density of the input mode, `0` to `-3`: each step takes 4px off the typed hour and
|
|
@@ -102,15 +105,16 @@ export declare function useTimePickerRoom(): 'vertical' | 'horizontal' | 'none';
|
|
|
102
105
|
* @example
|
|
103
106
|
* <TimePicker value={time} onChange={setTime} />
|
|
104
107
|
*/
|
|
105
|
-
export declare function TimePicker({ value, defaultValue, onChange, hourCycle: hourCycleProp, mode: modeProp, defaultMode, onModeChange, layout, labels, density, id, ref, className, style, classNames, ...rest }: TimePickerProps): import("react").JSX.Element;
|
|
108
|
+
export declare function TimePicker({ value, defaultValue, onChange, hourCycle: hourCycleProp, mode: modeProp, defaultMode, onModeChange, layout, labels: labelsProp, density, id, ref, className, style, classNames, ...rest }: TimePickerProps): import("react").JSX.Element;
|
|
106
109
|
export interface TimePickerModeToggleProps extends RootDOMProps<HTMLButtonElement> {
|
|
107
110
|
/** The time picker's mode; the toggle switches to the other one. */
|
|
108
111
|
mode: TimePickerMode;
|
|
109
112
|
/** Called with the other mode when the toggle is pressed. */
|
|
110
113
|
onModeChange: (mode: TimePickerMode) => void;
|
|
111
114
|
/**
|
|
112
|
-
* Words the toggle announces (`inputMode`, `dialMode`), with English defaults. Takes the
|
|
113
|
-
*
|
|
115
|
+
* Words the toggle announces (`inputMode`, `dialMode`), with English defaults. Takes the same
|
|
116
|
+
* object as the time picker's `labels`, so one set of words covers both. A {@link LabelsProvider}
|
|
117
|
+
* sets them for the whole app.
|
|
114
118
|
*/
|
|
115
119
|
labels?: Partial<TimePickerLabels>;
|
|
116
120
|
/** Shown in dial mode. Defaults to Material Symbols' keyboard. */
|
|
@@ -137,6 +141,6 @@ export interface TimePickerModeToggleProps extends RootDOMProps<HTMLButtonElemen
|
|
|
137
141
|
* <TimePicker mode={mode} onModeChange={setMode} />
|
|
138
142
|
* </PickerDialog>
|
|
139
143
|
*/
|
|
140
|
-
export declare function TimePickerModeToggle({ mode, onModeChange, labels, keyboardIcon, clockIcon, className, style, id, ref, ...dom }: TimePickerModeToggleProps): import("react").JSX.Element | null;
|
|
144
|
+
export declare function TimePickerModeToggle({ mode, onModeChange, labels: labelsProp, keyboardIcon, clockIcon, className, style, id, ref, ...dom }: TimePickerModeToggleProps): import("react").JSX.Element | null;
|
|
141
145
|
//#endregion
|
|
142
146
|
//# sourceMappingURL=TimePicker.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { densityAttribute } from "../../tokens/density.js";
|
|
3
3
|
import { cn } from "../../utils/cn.js";
|
|
4
|
+
import { NO_DEFAULT_LABELS, useLabels } from "../labels/use-labels.js";
|
|
4
5
|
import { useM3Spring } from "../../motion/use-m3-spring.js";
|
|
5
6
|
import { useM3Interaction } from "../../primitives/use-m3-interaction.js";
|
|
6
7
|
import { mergeDataTokens } from "../../utils/merge-data-tokens.js";
|
|
@@ -46,7 +47,8 @@ const defaultDialValue = (value, total, unit) => `${value} ${unit} of ${total}`;
|
|
|
46
47
|
* @example
|
|
47
48
|
* <TimePicker value={time} onChange={setTime} />
|
|
48
49
|
*/
|
|
49
|
-
function TimePicker({ value, defaultValue, onChange, hourCycle: hourCycleProp, mode: modeProp, defaultMode = "dial", onModeChange, layout = "auto", labels
|
|
50
|
+
function TimePicker({ value, defaultValue, onChange, hourCycle: hourCycleProp, mode: modeProp, defaultMode = "dial", onModeChange, layout = "auto", labels: labelsProp, density, id, ref, className, style, classNames, ...rest }) {
|
|
51
|
+
const labels = useLabels("TimePicker", NO_DEFAULT_LABELS, labelsProp);
|
|
50
52
|
const { root: data } = splitRootProps(rest, { wrapper: true });
|
|
51
53
|
const { locale } = useLocale();
|
|
52
54
|
const hourCycle = hourCycleProp ?? localeHourCycle(locale);
|
|
@@ -168,7 +170,8 @@ function TimePicker({ value, defaultValue, onChange, hourCycle: hourCycleProp, m
|
|
|
168
170
|
* <TimePicker mode={mode} onModeChange={setMode} />
|
|
169
171
|
* </PickerDialog>
|
|
170
172
|
*/
|
|
171
|
-
function TimePickerModeToggle({ mode, onModeChange, labels
|
|
173
|
+
function TimePickerModeToggle({ mode, onModeChange, labels: labelsProp, keyboardIcon, clockIcon, className, style, id, ref, ...dom }) {
|
|
174
|
+
const labels = useLabels("TimePicker", NO_DEFAULT_LABELS, labelsProp);
|
|
172
175
|
if (useTimePickerRoom() === "none") return null;
|
|
173
176
|
const own = {
|
|
174
177
|
...dom,
|