@windmorph/react 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
  2. package/dist/components/date-picker/date-picker-styles.d.ts +3 -3
  3. package/dist/components/dialog/dialog-styles.js +5 -5
  4. package/dist/components/dialog/dialog-styles.js.map +1 -1
  5. package/dist/components/menu/use-reveal-entering.d.ts +19 -0
  6. package/dist/components/navigation/NavItem.js +1 -1
  7. package/dist/components/navigation/NavItem.js.map +1 -1
  8. package/dist/components/navigation/NavigationRail.d.ts +21 -2
  9. package/dist/components/navigation/NavigationRail.js +18 -4
  10. package/dist/components/navigation/NavigationRail.js.map +1 -1
  11. package/dist/components/navigation/navigation-styles.d.ts +21 -0
  12. package/dist/components/navigation/navigation-styles.js +6 -1
  13. package/dist/components/navigation/navigation-styles.js.map +1 -1
  14. package/dist/components/select/Select.js +6 -1
  15. package/dist/components/select/Select.js.map +1 -1
  16. package/dist/components/select/search-list.js +1 -1
  17. package/dist/components/select/search-list.js.map +1 -1
  18. package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
  19. package/dist/components/text-field/TextField.js +9 -1
  20. package/dist/components/text-field/TextField.js.map +1 -1
  21. package/dist/components/time-picker/TimeField.d.ts +118 -0
  22. package/dist/components/time-picker/TimeField.js +183 -0
  23. package/dist/components/time-picker/TimeField.js.map +1 -0
  24. package/dist/components/time-picker/TimeFieldDialog.js +70 -0
  25. package/dist/components/time-picker/TimeFieldDialog.js.map +1 -0
  26. package/dist/components/time-picker/TimePicker.js +1 -4
  27. package/dist/components/time-picker/TimePicker.js.map +1 -1
  28. package/dist/components/time-picker/hour-cycle.js +10 -0
  29. package/dist/components/time-picker/hour-cycle.js.map +1 -0
  30. package/dist/components/time-picker/time-format.js +84 -0
  31. package/dist/components/time-picker/time-format.js.map +1 -0
  32. package/dist/composites/button-group/ButtonGroup.d.ts +7 -0
  33. package/dist/composites/button-group/ButtonGroup.js +3 -2
  34. package/dist/composites/button-group/ButtonGroup.js.map +1 -1
  35. package/dist/composites/button-group/button-group-styles.d.ts +15 -3
  36. package/dist/composites/button-group/button-group-styles.js +16 -1
  37. package/dist/composites/button-group/button-group-styles.js.map +1 -1
  38. package/dist/index.d.ts +4 -2
  39. package/dist/index.js +4 -2
  40. package/dist/primitives.d.ts +4 -1
  41. package/dist/primitives.js +4 -1
  42. package/dist/styles.compiled.css +1 -1
  43. package/dist/utils/closed-by.d.ts +5 -0
  44. package/dist/utils/placement.d.ts +11 -0
  45. package/package.json +2 -2
@@ -0,0 +1,118 @@
1
+ import { Density } from "../../tokens/density.js";
2
+ import { WrapperRootDOMProps } from "../../utils/root-props.js";
3
+ import { TextFieldClassNames } from "../text-field/TextField.js";
4
+ import { PickerDialogLabels } from "../date-picker/PickerDialog.js";
5
+ import { TimePickerLabels } from "./TimePicker.js";
6
+ import { AriaAttributes, CSSProperties, ReactElement, ReactNode, Ref } from "react";
7
+ import { Time } from "@internationalized/date";
8
+ //#region src/components/time-picker/TimeField.d.ts
9
+ /** Words of a {@link TimeField} and its picker, for localisation. */
10
+ export interface TimeFieldLabels extends TimePickerLabels, PickerDialogLabels {
11
+ /** The field's placeholder. @default the clock's format, "HH:MM" or "H:MM AM" */
12
+ timeFormatHint: string;
13
+ /** Error when the text isn't a time. @default "Enter a time such as {hint}" */
14
+ invalidTime: string;
15
+ /** Error for a time before `min` or after `max`. @default "Time out of range" */
16
+ timeOutOfRange: string;
17
+ /** The clock icon button. @default "Open time picker" */
18
+ openPicker: string;
19
+ /** The picker's title and name. @default "Select time" */
20
+ title: string;
21
+ /** The picker's dismissing action. @default "Cancel" */
22
+ cancel: string;
23
+ /** The picker's confirming action. @default "OK" */
24
+ ok: string;
25
+ }
26
+ /** Classes for each part of a {@link TimeField}. */
27
+ export interface TimeFieldClassNames extends Omit<TextFieldClassNames, 'leadingIcon' | 'prefix' | 'suffix' | 'counter'> {
28
+ /** The text field's root; `className` goes here too. */
29
+ root?: string;
30
+ /** The clock icon button. */
31
+ button?: string;
32
+ /** The picker dialog's surface. */
33
+ picker?: string;
34
+ }
35
+ export interface TimeFieldProps extends WrapperRootDOMProps {
36
+ /** The field's label, e.g. "Opens at". */
37
+ label: ReactNode;
38
+ /** The time (controlled); `null` for none. */
39
+ value?: Time | null;
40
+ /** Initial time when uncontrolled. @default null */
41
+ defaultValue?: Time | null;
42
+ /** A time was typed and committed, or picked and confirmed; `null` when cleared. */
43
+ onChange?: (value: Time | null) => void;
44
+ /** Earliest time allowed. */
45
+ min?: Time;
46
+ /** Latest time allowed. */
47
+ max?: Time;
48
+ /** 12- or 24-hour clock, for the field and the picker. Left out, the locale's. @default the locale's */
49
+ hourCycle?: 12 | 24;
50
+ /** The field's style. @default "outlined" */
51
+ variant?: 'outlined' | 'filled';
52
+ /** Whether the picker is open (controlled). */
53
+ open?: boolean;
54
+ /** Whether the picker starts open when uncontrolled. @default false */
55
+ defaultOpen?: boolean;
56
+ /** Called when the picker opens or closes. */
57
+ onOpenChange?: (open: boolean) => void;
58
+ /** Helper text below the field. */
59
+ supportingText?: ReactNode;
60
+ /** Marks the field invalid, with `errorMessage` below it. */
61
+ invalid?: boolean;
62
+ /** Error text shown below the field while it's invalid, in place of `supportingText`. */
63
+ errorMessage?: ReactNode;
64
+ /** The field must have a time. @default false */
65
+ required?: boolean;
66
+ /** The field and its clock button can't be used. @default false */
67
+ disabled?: boolean;
68
+ /** The time can be read and focused but not changed. @default false */
69
+ readOnly?: boolean;
70
+ /** More description for the `<input>`, after the supporting or error text. */
71
+ 'aria-describedby'?: string;
72
+ /** The id of an element with more details about the `<input>`. */
73
+ 'aria-details'?: string;
74
+ /** Read as `invalid` when that's left out, for a form library that sets the attribute. */
75
+ 'aria-invalid'?: AriaAttributes['aria-invalid'];
76
+ /** Read as `required` when that's left out, for a form library that sets the attribute. */
77
+ 'aria-required'?: AriaAttributes['aria-required'];
78
+ /** Read as `readOnly` when that's left out, for a form library that sets the attribute. */
79
+ 'aria-readonly'?: AriaAttributes['aria-readonly'];
80
+ /** Form field name; the input submits the time as shown. */
81
+ name?: string;
82
+ /** The `<input>`'s id, for a `<label for>` or an error summary's link. */
83
+ id?: string;
84
+ /** Icon of the button that opens the picker. */
85
+ clockIcon?: ReactElement;
86
+ /**
87
+ * Web density of the field, `0` to `-3`: each step takes 4px off it (56px at 0, 48px at -2).
88
+ * The picker keeps its size. Left out, the field takes the nearest theme's.
89
+ * @default 0
90
+ */
91
+ density?: Density;
92
+ /** Words the field and its picker show and announce, with English defaults. */
93
+ labels?: Partial<TimeFieldLabels>;
94
+ /** The text field's root. */
95
+ ref?: Ref<HTMLDivElement>;
96
+ /** The `<input>`. */
97
+ inputRef?: Ref<HTMLInputElement>;
98
+ /** Classes for the root element, merged with `classNames.root`; consumer classes win. */
99
+ className?: string;
100
+ /** Inline styles for the root element. */
101
+ style?: CSSProperties;
102
+ /** Classes for each part, by slot. */
103
+ classNames?: TimeFieldClassNames;
104
+ /** `data-*` attributes go to the root. */
105
+ [data: `data-${string}`]: string | number | boolean | undefined;
106
+ }
107
+ /**
108
+ * A time in a form, for pages that hold many of them, such as a weekly schedule. One compact
109
+ * field with no input mask that reads what people type or paste (`9`, `930`, `9:30`, `9h30`,
110
+ * `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. The time sibling of `DateField`.
112
+ *
113
+ * @example
114
+ * <TimeField label="Opens at" value={time} onChange={setTime} />
115
+ */
116
+ export declare function TimeField(props: TimeFieldProps): import("react").JSX.Element;
117
+ //#endregion
118
+ //# sourceMappingURL=TimeField.d.ts.map
@@ -0,0 +1,183 @@
1
+ "use client";
2
+ import { cn } from "../../utils/cn.js";
3
+ import { onDemand, usePreloadOnIntent } from "../../utils/load-on-demand.js";
4
+ import { IconButton } from "../icon-button/IconButton.js";
5
+ import { DomDirectionLocale } from "../../primitives/DomDirectionLocale.js";
6
+ import { ariaFlag, pickDescriptionAria, splitRootProps } from "../../utils/root-props.js";
7
+ import { TextField } from "../text-field/TextField.js";
8
+ import { ScheduleIcon } from "../date-picker/picker-icons.js";
9
+ import { localeHourCycle } from "./hour-cycle.js";
10
+ import { formatTime, parseTimeText } from "./time-format.js";
11
+ import { useRef, useState } from "react";
12
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
13
+ import { useLocale, useObjectRef } from "react-aria";
14
+ import { useOverlayTriggerState } from "react-stately";
15
+ import { useControlledState } from "react-stately/useControlledState";
16
+ //#region src/components/time-picker/TimeField.tsx
17
+ const pickerDialog = onDemand(() => import("./TimeFieldDialog.js").then((module) => module.TimeFieldDialog));
18
+ const DEFAULT_LABELS = {
19
+ timeOutOfRange: "Time out of range",
20
+ openPicker: "Open time picker",
21
+ title: "Select time",
22
+ cancel: "Cancel",
23
+ ok: "OK",
24
+ close: "Close",
25
+ save: "Save",
26
+ hour: "Hour",
27
+ minute: "Minute",
28
+ am: "AM",
29
+ pm: "PM",
30
+ period: "AM or PM",
31
+ dial: "Clock",
32
+ inputMode: "Toggle input picker",
33
+ dialMode: "Toggle dial picker",
34
+ dialValue: (value, total, unit) => `${value} ${unit} of ${total}`
35
+ };
36
+ /**
37
+ * A time in a form, for pages that hold many of them, such as a weekly schedule. One compact
38
+ * field with no input mask that reads what people type or paste (`9`, `930`, `9:30`, `9h30`,
39
+ * `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. The time sibling of `DateField`.
41
+ *
42
+ * @example
43
+ * <TimeField label="Opens at" value={time} onChange={setTime} />
44
+ */
45
+ function TimeField(props) {
46
+ return /* @__PURE__ */ jsx(DomDirectionLocale, { children: (directionRef) => /* @__PURE__ */ jsx(TimeFieldInner, {
47
+ ...props,
48
+ directionRef
49
+ }) });
50
+ }
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 }) {
52
+ const { root, rest: aria } = splitRootProps(rest, { wrapper: true });
53
+ const invalid = invalidProp ?? ariaFlag(ariaInvalid);
54
+ const required = requiredProp ?? ariaFlag(ariaRequired);
55
+ const readOnly = readOnlyProp ?? ariaFlag(ariaReadonly);
56
+ const { locale } = useLocale();
57
+ const hourCycle = hourCycleProp ?? localeHourCycle(locale);
58
+ const hint = labelsProp?.timeFormatHint ?? (hourCycle === 24 ? "HH:MM" : "H:MM AM");
59
+ const labels = {
60
+ ...DEFAULT_LABELS,
61
+ timeFormatHint: hint,
62
+ invalidTime: `Enter a time such as ${hint}`,
63
+ ...labelsProp
64
+ };
65
+ const [value, setValue] = useControlledState(valueProp, defaultValue ?? null, onChange);
66
+ const show = (time) => time ? formatTime(time, locale, hourCycle) : "";
67
+ const [text, setText] = useState(() => show(value));
68
+ const [error, setError] = useState(null);
69
+ const key = `${value?.toString() ?? ""}|${hourCycle}|${locale}`;
70
+ const [shown, setShown] = useState(key);
71
+ if (key !== shown) {
72
+ setShown(key);
73
+ setText(show(value));
74
+ setError(null);
75
+ }
76
+ const overlay = useOverlayTriggerState({
77
+ ...open !== void 0 && { isOpen: open },
78
+ ...defaultOpen !== void 0 && { defaultOpen },
79
+ ...onOpenChange && { onOpenChange }
80
+ });
81
+ const fieldRef = useRef(null);
82
+ const domInputRef = useObjectRef(inputRef);
83
+ usePreloadOnIntent(fieldRef, pickerDialog.preload, true);
84
+ const [opened, setOpened] = useState(overlay.isOpen);
85
+ if (overlay.isOpen && !opened) setOpened(true);
86
+ const outOfRange = (time) => min !== void 0 && time.compare(min) < 0 || max !== void 0 && time.compare(max) > 0;
87
+ const accept = (time) => {
88
+ if (outOfRange(time)) {
89
+ setText(show(time));
90
+ setError(labels.timeOutOfRange);
91
+ return;
92
+ }
93
+ setError(null);
94
+ setText(show(time));
95
+ if (time.toString() !== value?.toString()) setValue(time);
96
+ };
97
+ const commit = () => {
98
+ if (text.trim() === "") {
99
+ setError(null);
100
+ if (value) setValue(null);
101
+ return;
102
+ }
103
+ if (value && text === show(value)) {
104
+ setError(null);
105
+ return;
106
+ }
107
+ const time = parseTimeText(text, locale);
108
+ if (!time) {
109
+ setError(labels.invalidTime);
110
+ return;
111
+ }
112
+ accept(time);
113
+ };
114
+ const { root: rootClassName, button, picker, ...fieldClassNames } = classNames ?? {};
115
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(TextField, {
116
+ ...root,
117
+ ...pickDescriptionAria(aria),
118
+ ...describedBy !== void 0 && { "aria-describedby": describedBy },
119
+ ...details !== void 0 && { "aria-details": details },
120
+ ...id !== void 0 && { id },
121
+ ref: (element) => {
122
+ fieldRef.current = element;
123
+ directionRef(element);
124
+ if (typeof ref === "function") ref(element);
125
+ else if (ref) ref.current = element;
126
+ },
127
+ style,
128
+ inputRef: domInputRef,
129
+ variant,
130
+ ...density !== void 0 && { density },
131
+ label,
132
+ value: text,
133
+ onChange: (next) => {
134
+ setText(next);
135
+ if (error) setError(null);
136
+ },
137
+ onBlur: commit,
138
+ onKeyDown: (event) => {
139
+ if (event.key !== "Enter") {
140
+ event.continuePropagation();
141
+ return;
142
+ }
143
+ commit();
144
+ },
145
+ placeholder: hint,
146
+ supportingText,
147
+ invalid: invalid || error !== null,
148
+ errorMessage: error ?? errorMessage,
149
+ inputMode: hourCycle === 24 ? "decimal" : "text",
150
+ autoComplete: "off",
151
+ required,
152
+ disabled,
153
+ readOnly,
154
+ name,
155
+ className: cn("w-[9.25rem] max-w-full", rootClassName, className),
156
+ classNames: fieldClassNames,
157
+ trailingIcon: /* @__PURE__ */ jsx(IconButton, {
158
+ variant: "standard",
159
+ icon: clockIcon ?? /* @__PURE__ */ jsx(ScheduleIcon, {}),
160
+ "aria-label": labels.openPicker,
161
+ "aria-haspopup": "dialog",
162
+ "aria-expanded": overlay.isOpen,
163
+ disabled: disabled || readOnly,
164
+ className: button,
165
+ onPress: (event) => {
166
+ if (event.pointerType !== "touch") domInputRef.current?.focus({ preventScroll: true });
167
+ overlay.open();
168
+ }
169
+ })
170
+ }), opened ? /* @__PURE__ */ jsx(pickerDialog.Component, {
171
+ open: overlay.isOpen,
172
+ onOpenChange: overlay.setOpen,
173
+ value,
174
+ onCommit: accept,
175
+ hourCycle,
176
+ labels,
177
+ className: picker
178
+ }) : null] });
179
+ }
180
+ //#endregion
181
+ export { TimeField };
182
+
183
+ //# sourceMappingURL=TimeField.js.map
@@ -0,0 +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":";;;;;;;;;;;;;;;;AA0IA,MAAM,eAAe,eACnB,OAAO,uBAAoB,CAAC,MAAM,WAAW,OAAO,eAAe,CACrE;AAEA,MAAM,iBAA0E;CAC9E,gBAAgB;CAChB,YAAY;CACZ,OAAO;CACP,QAAQ;CACR,IAAI;CACJ,OAAO;CACP,MAAM;CACN,MAAM;CACN,QAAQ;CACR,IAAI;CACJ,IAAI;CACJ,QAAQ;CACR,MAAM;CACN,WAAW;CACX,UAAU;CACV,YAAY,OAAO,OAAO,SAAS,GAAG,MAAM,GAAG,KAAK,MAAM;AAC5D;;;;;;;;;;AAWA,SAAgB,UAAU,OAAuB;CAC/C,OACE,oBAAC,oBAAD,EAAA,WACI,iBAAiB,oBAAC,gBAAD;EAAgB,GAAI;EAAqB;CAAe,CAAA,EACzD,CAAA;AAExB;AAEA,SAAS,eAAe,EACtB,OACA,OAAO,WACP,cACA,UACA,KACA,KACA,WAAW,eACX,UAAU,YACV,MACA,aACA,cACA,gBACA,SAAS,aACT,cACA,UAAU,cACV,UACA,UAAU,cACV,oBAAoB,aACpB,gBAAgB,SAChB,gBAAgB,aAChB,iBAAiB,cACjB,iBAAiB,cACjB,MACA,IACA,WACA,SACA,QAAQ,YACR,KACA,UACA,WACA,OACA,YACA,cACA,GAAG,QACwE;CAC3E,MAAM,EAAE,MAAM,MAAM,SAAS,eAAe,MAAM,EAAE,SAAS,KAAK,CAAC;CACnE,MAAM,UAAU,eAAe,SAAS,WAAW;CACnD,MAAM,WAAW,gBAAgB,SAAS,YAAY;CACtD,MAAM,WAAW,gBAAgB,SAAS,YAAY;CACtD,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,YAAY,iBAAiB,gBAAgB,MAAM;CACzD,MAAM,OAAO,YAAY,mBAAmB,cAAc,KAAK,UAAU;CACzE,MAAM,SAA0B;EAC9B,GAAG;EACH,gBAAgB;EAChB,aAAa,wBAAwB;EACrC,GAAG;CACL;CAEA,MAAM,CAAC,OAAO,YAAY,mBAAmB,WAAW,gBAAgB,MAAM,QAAQ;CACtF,MAAM,QAAQ,SAAuB,OAAO,WAAW,MAAM,QAAQ,SAAS,IAAI;CAClF,MAAM,CAAC,MAAM,WAAW,eAAe,KAAK,KAAK,CAAC;CAClD,MAAM,CAAC,OAAO,YAAY,SAAwB,IAAI;CAEtD,MAAM,MAAM,GAAG,OAAO,SAAS,KAAK,GAAG,GAAG,UAAU,GAAG;CACvD,MAAM,CAAC,OAAO,YAAY,SAAS,GAAG;CACtC,IAAI,QAAQ,OAAO;EACjB,SAAS,GAAG;EACZ,QAAQ,KAAK,KAAK,CAAC;EACnB,SAAS,IAAI;CACf;CAEA,MAAM,UAAU,uBAAuB;EACrC,GAAI,SAAS,KAAA,KAAa,EAAE,QAAQ,KAAK;EACzC,GAAI,gBAAgB,KAAA,KAAa,EAAE,YAAY;EAC/C,GAAI,gBAAgB,EAAE,aAAa;CACrC,CAAC;CACD,MAAM,WAAW,OAA8B,IAAI;CACnD,MAAM,cAAc,aAAa,QAAQ;CACzC,mBAAmB,UAAU,aAAa,SAAS,IAAI;CACvD,MAAM,CAAC,QAAQ,aAAa,SAAS,QAAQ,MAAM;CACnD,IAAI,QAAQ,UAAU,CAAC,QAAQ,UAAU,IAAI;CAE7C,MAAM,cAAc,SACjB,QAAQ,KAAA,KAAa,KAAK,QAAQ,GAAG,IAAI,KAAO,QAAQ,KAAA,KAAa,KAAK,QAAQ,GAAG,IAAI;CAE5F,MAAM,UAAU,SAAe;EAC7B,IAAI,WAAW,IAAI,GAAG;GACpB,QAAQ,KAAK,IAAI,CAAC;GAClB,SAAS,OAAO,cAAc;GAC9B;EACF;EACA,SAAS,IAAI;EACb,QAAQ,KAAK,IAAI,CAAC;EAClB,IAAI,KAAK,SAAS,MAAM,OAAO,SAAS,GAAG,SAAS,IAAI;CAC1D;CAEA,MAAM,eAAe;EACnB,IAAI,KAAK,KAAK,MAAM,IAAI;GACtB,SAAS,IAAI;GACb,IAAI,OAAO,SAAS,IAAI;GACxB;EACF;EAEA,IAAI,SAAS,SAAS,KAAK,KAAK,GAAG;GACjC,SAAS,IAAI;GACb;EACF;EACA,MAAM,OAAO,cAAc,MAAM,MAAM;EACvC,IAAI,CAAC,MAAM;GACT,SAAS,OAAO,WAAW;GAC3B;EACF;EACA,OAAO,IAAI;CACb;CAEA,MAAM,EAAE,MAAM,eAAe,QAAQ,QAAQ,GAAG,oBAAoB,cAAc,CAAC;CACnF,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,WAAD;EACE,GAAI;EACJ,GAAI,oBAAoB,IAAI;EAC5B,GAAK,gBAAgB,KAAA,KAAa,EAAE,oBAAoB,YAAY;EACpE,GAAK,YAAY,KAAA,KAAa,EAAE,gBAAgB,QAAQ;EACxD,GAAK,OAAO,KAAA,KAAa,EAAE,GAAG;EAC9B,MAAM,YAAmC;GACvC,SAAS,UAAU;GACnB,aAAa,OAAO;GACpB,IAAI,OAAO,QAAQ,YAAY,IAAI,OAAO;QACrC,IAAI,KAAK,IAAI,UAAU;EAC9B;EACO;EACP,UAAU;EACD;EACT,GAAK,YAAY,KAAA,KAAa,EAAE,QAAQ;EACjC;EACP,OAAO;EACP,WAAW,SAAiB;GAC1B,QAAQ,IAAI;GACZ,IAAI,OAAO,SAAS,IAAI;EAC1B;EACA,QAAQ;EACR,YAAY,UAAU;GACpB,IAAI,MAAM,QAAQ,SAAS;IACzB,MAAM,oBAAoB;IAC1B;GACF;GACA,OAAO;EACT;EACA,aAAa;EACG;EAChB,SAAS,WAAW,UAAU;EAC9B,cAAc,SAAS;EAEvB,WAAW,cAAc,KAAK,YAAY;EAC1C,cAAa;EACH;EACA;EACA;EACJ;EACN,WAAW,GAAG,0BAA0B,eAAe,SAAS;EAChE,YAAY;EACZ,cACE,oBAAC,YAAD;GACE,SAAQ;GACR,MAAM,aAAa,oBAAC,cAAD,CAAe,CAAA;GAClC,cAAY,OAAO;GACnB,iBAAc;GACd,iBAAe,QAAQ;GACvB,UAAU,YAAY;GACtB,WAAW;GACX,UAAU,UAAU;IAGlB,IAAI,MAAM,gBAAgB,SACxB,YAAY,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;IAEpD,QAAQ,KAAK;GACf;EACD,CAAA;CAEJ,CAAA,GACA,SACC,oBAAC,aAAa,WAAd;EACE,MAAM,QAAQ;EACd,cAAc,QAAQ;EACf;EACP,UAAU;EACC;EACH;EACR,WAAW;CACZ,CAAA,IACC,IACJ,EAAA,CAAA;AAEN"}
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import { Button } from "../button/Button.js";
3
+ import { PickerDialog } from "../date-picker/PickerDialog.js";
4
+ import { TimePicker, TimePickerModeToggle } from "./TimePicker.js";
5
+ import { useState } from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+ import { Time } from "@internationalized/date";
8
+ //#region src/components/time-picker/TimeFieldDialog.tsx
9
+ /**
10
+ * A {@link TimeField}'s picker: the time picker in its typed form first, in a dialog, editing a
11
+ * draft that OK keeps and Cancel drops. `TimeField` loads this module with `import()` as it is
12
+ * first needed, so a form of many fields carries none of it.
13
+ */
14
+ function TimeFieldDialog({ open, onOpenChange, value, onCommit, hourCycle, labels, className }) {
15
+ const [draft, setDraft] = useState(value ?? new Time(12));
16
+ const [mode, setMode] = useState("input");
17
+ const [wasOpen, setWasOpen] = useState(open);
18
+ if (open !== wasOpen) {
19
+ setWasOpen(open);
20
+ if (open) {
21
+ setDraft(value ?? new Time(12));
22
+ setMode("input");
23
+ }
24
+ }
25
+ const confirm = () => {
26
+ onCommit(draft);
27
+ onOpenChange(false);
28
+ };
29
+ return /* @__PURE__ */ jsx(PickerDialog, {
30
+ "aria-label": labels.title,
31
+ variant: "time",
32
+ title: labels.title,
33
+ open,
34
+ onOpenChange,
35
+ onConfirm: confirm,
36
+ fullScreen: "compact",
37
+ labels: {
38
+ close: labels.close,
39
+ save: labels.save
40
+ },
41
+ classNames: className === void 0 ? {} : { panel: className },
42
+ modeToggleButton: /* @__PURE__ */ jsx(TimePickerModeToggle, {
43
+ mode,
44
+ onModeChange: setMode,
45
+ labels
46
+ }),
47
+ dismissButton: /* @__PURE__ */ jsx(Button, {
48
+ variant: "text",
49
+ onPress: () => onOpenChange(false),
50
+ children: labels.cancel
51
+ }),
52
+ confirmButton: /* @__PURE__ */ jsx(Button, {
53
+ variant: "text",
54
+ onPress: confirm,
55
+ children: labels.ok
56
+ }),
57
+ children: /* @__PURE__ */ jsx(TimePicker, {
58
+ value: draft,
59
+ onChange: setDraft,
60
+ mode,
61
+ onModeChange: setMode,
62
+ hourCycle,
63
+ labels
64
+ })
65
+ });
66
+ }
67
+ //#endregion
68
+ export { TimeFieldDialog };
69
+
70
+ //# sourceMappingURL=TimeFieldDialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimeFieldDialog.js","names":[],"sources":["../../../src/components/time-picker/TimeFieldDialog.tsx"],"sourcesContent":["'use client';\n\nimport { Time } from '@internationalized/date';\nimport { useState } from 'react';\nimport { Button } from '../button/Button';\nimport { PickerDialog } from '../date-picker/PickerDialog';\nimport type { TimeFieldLabels } from './TimeField';\nimport { TimePicker, TimePickerModeToggle, type TimePickerMode } from './TimePicker';\n\n/** Props of a {@link TimeFieldDialog}. */\nexport interface TimeFieldDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n value: Time | null;\n onCommit: (time: Time) => void;\n hourCycle: 12 | 24;\n labels: TimeFieldLabels;\n className: string | undefined;\n}\n\n/**\n * A {@link TimeField}'s picker: the time picker in its typed form first, in a dialog, editing a\n * draft that OK keeps and Cancel drops. `TimeField` loads this module with `import()` as it is\n * first needed, so a form of many fields carries none of it.\n */\nexport function TimeFieldDialog({\n open,\n onOpenChange,\n value,\n onCommit,\n hourCycle,\n labels,\n className,\n}: TimeFieldDialogProps) {\n const [draft, setDraft] = useState(value ?? new Time(12));\n const [mode, setMode] = useState<TimePickerMode>('input');\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setDraft(value ?? new Time(12));\n setMode('input');\n }\n }\n const confirm = () => {\n onCommit(draft);\n onOpenChange(false);\n };\n return (\n <PickerDialog\n aria-label={labels.title}\n variant=\"time\"\n title={labels.title}\n open={open}\n onOpenChange={onOpenChange}\n onConfirm={confirm}\n fullScreen=\"compact\"\n labels={{ close: labels.close, save: labels.save }}\n classNames={className === undefined ? {} : { panel: className }}\n modeToggleButton={<TimePickerModeToggle mode={mode} onModeChange={setMode} labels={labels} />}\n dismissButton={\n <Button variant=\"text\" onPress={() => onOpenChange(false)}>\n {labels.cancel}\n </Button>\n }\n confirmButton={\n <Button variant=\"text\" onPress={confirm}>\n {labels.ok}\n </Button>\n }\n >\n <TimePicker\n value={draft}\n onChange={setDraft}\n mode={mode}\n onModeChange={setMode}\n hourCycle={hourCycle}\n labels={labels}\n />\n </PickerDialog>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAyBA,SAAgB,gBAAgB,EAC9B,MACA,cACA,OACA,UACA,WACA,QACA,aACuB;CACvB,MAAM,CAAC,OAAO,YAAY,SAAS,SAAS,IAAI,KAAK,EAAE,CAAC;CACxD,MAAM,CAAC,MAAM,WAAW,SAAyB,OAAO;CACxD,MAAM,CAAC,SAAS,cAAc,SAAS,IAAI;CAC3C,IAAI,SAAS,SAAS;EACpB,WAAW,IAAI;EACf,IAAI,MAAM;GACR,SAAS,SAAS,IAAI,KAAK,EAAE,CAAC;GAC9B,QAAQ,OAAO;EACjB;CACF;CACA,MAAM,gBAAgB;EACpB,SAAS,KAAK;EACd,aAAa,KAAK;CACpB;CACA,OACE,oBAAC,cAAD;EACE,cAAY,OAAO;EACnB,SAAQ;EACR,OAAO,OAAO;EACR;EACQ;EACd,WAAW;EACX,YAAW;EACX,QAAQ;GAAE,OAAO,OAAO;GAAO,MAAM,OAAO;EAAK;EACjD,YAAY,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,UAAU;EAC9D,kBAAkB,oBAAC,sBAAD;GAA4B;GAAM,cAAc;GAAiB;EAAS,CAAA;EAC5F,eACE,oBAAC,QAAD;GAAQ,SAAQ;GAAO,eAAe,aAAa,KAAK;GACrD,UAAA,OAAO;EACF,CAAA;EAEV,eACE,oBAAC,QAAD;GAAQ,SAAQ;GAAO,SAAS;GAC7B,UAAA,OAAO;EACF,CAAA;EAGV,UAAA,oBAAC,YAAD;GACE,OAAO;GACP,UAAU;GACJ;GACN,cAAc;GACH;GACH;EACT,CAAA;CACW,CAAA;AAElB"}
@@ -8,6 +8,7 @@ import { IconButton } from "../icon-button/IconButton.js";
8
8
  import { splitRootProps } from "../../utils/root-props.js";
9
9
  import { useMediaQuery } from "../date-picker/use-media-query.js";
10
10
  import { KeyboardIcon, ScheduleIcon } from "../date-picker/picker-icons.js";
11
+ import { localeHourCycle } from "./hour-cycle.js";
11
12
  import { timePickerStyles } from "./time-picker-styles.js";
12
13
  import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
13
14
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -33,10 +34,6 @@ function useTimePickerRoom() {
33
34
  const short = useMediaQuery(SHORT_WINDOW);
34
35
  return useMediaQuery(NO_ROOM_FOR_DIAL) ? "none" : short ? "horizontal" : "vertical";
35
36
  }
36
- function localeHourCycle(locale) {
37
- const cycle = new Intl.DateTimeFormat(locale, { hour: "numeric" }).resolvedOptions().hourCycle;
38
- return cycle === "h11" || cycle === "h12" ? 12 : 24;
39
- }
40
37
  const defaultDialValue = (value, total, unit) => `${value} ${unit} of ${total}`;
41
38
  /**
42
39
  * M3 time picker (Compose's `TimePicker` and `TimeInput`): hour and minute selectors you