@windmorph/react 0.1.2 → 0.2.1
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/date-picker/date-picker-styles.d.ts +9 -9
- package/dist/components/dialog/Dialog.d.ts +4 -2
- package/dist/components/dialog/Dialog.js +2 -2
- package/dist/components/dialog/Dialog.js.map +1 -1
- package/dist/components/dialog/dialog-styles.js +5 -5
- package/dist/components/dialog/dialog-styles.js.map +1 -1
- package/dist/components/icon-button/IconButton.d.ts +3 -0
- package/dist/components/icon-button/IconButton.js +3 -0
- package/dist/components/icon-button/IconButton.js.map +1 -1
- package/dist/components/menu/use-reveal-entering.d.ts +19 -0
- package/dist/components/navigation/NavItem.js +1 -1
- package/dist/components/navigation/NavItem.js.map +1 -1
- package/dist/components/navigation/NavigationRail.d.ts +21 -2
- package/dist/components/navigation/NavigationRail.js +18 -4
- package/dist/components/navigation/NavigationRail.js.map +1 -1
- package/dist/components/navigation/nav-item-styles.js +4 -4
- package/dist/components/navigation/nav-item-styles.js.map +1 -1
- package/dist/components/navigation/navigation-styles.d.ts +21 -0
- package/dist/components/navigation/navigation-styles.js +6 -1
- package/dist/components/navigation/navigation-styles.js.map +1 -1
- package/dist/components/select/Select.js +6 -1
- package/dist/components/select/Select.js.map +1 -1
- package/dist/components/select/search-list.js +1 -1
- package/dist/components/select/search-list.js.map +1 -1
- package/dist/components/sheet/SideSheet.d.ts +2 -0
- package/dist/components/sheet/SideSheet.js +1 -1
- package/dist/components/sheet/SideSheet.js.map +1 -1
- package/dist/components/text-field/TextField.js +9 -1
- package/dist/components/text-field/TextField.js.map +1 -1
- package/dist/components/time-picker/TimeField.d.ts +118 -0
- package/dist/components/time-picker/TimeField.js +183 -0
- package/dist/components/time-picker/TimeField.js.map +1 -0
- package/dist/components/time-picker/TimeFieldDialog.js +70 -0
- package/dist/components/time-picker/TimeFieldDialog.js.map +1 -0
- package/dist/components/time-picker/TimePicker.js +1 -4
- package/dist/components/time-picker/TimePicker.js.map +1 -1
- package/dist/components/time-picker/hour-cycle.js +10 -0
- package/dist/components/time-picker/hour-cycle.js.map +1 -0
- package/dist/components/time-picker/time-format.js +84 -0
- package/dist/components/time-picker/time-format.js.map +1 -0
- package/dist/composites/button-group/ButtonGroup.d.ts +7 -0
- package/dist/composites/button-group/ButtonGroup.js +3 -2
- package/dist/composites/button-group/ButtonGroup.js.map +1 -1
- package/dist/composites/button-group/button-group-styles.d.ts +15 -3
- package/dist/composites/button-group/button-group-styles.js +16 -1
- package/dist/composites/button-group/button-group-styles.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/primitives/ButtonBase.js +1 -0
- package/dist/primitives/ButtonBase.js.map +1 -1
- package/dist/primitives.d.ts +4 -1
- package/dist/primitives.js +4 -1
- package/dist/styles/utilities.css +8 -0
- package/dist/styles.compiled.css +1 -1
- package/dist/theme/ThemeProvider.js +17 -0
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/utils/closed-by.d.ts +5 -0
- package/dist/utils/placement.d.ts +11 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextField.js","names":[],"sources":["../../../src/components/text-field/TextField.tsx"],"sourcesContent":["'use client';\n\nimport {\n useId,\n useLayoutEffect,\n type AriaAttributes,\n type CSSProperties,\n type MouseEvent,\n type PointerEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n mergeProps,\n useFocusRing,\n useHover,\n useObjectRef,\n useTextField,\n type AriaTextFieldOptions,\n type AriaTextFieldProps,\n} from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport {\n CharacterCounter,\n ErrorText,\n FieldErrorIcon,\n FieldLabel,\n SupportingText,\n} from '../../primitives/Field';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { pickWidgetAria, splitRootProps, type WrapperRootDOMProps } from '../../utils/root-props';\nimport { textFieldStyles, type TextFieldVariant } from './text-field-styles';\n\nexport interface TextFieldClassNames {\n /** The root element, merged with `className`. */\n root?: string;\n /** The filled or outlined container around the label, input and icons. */\n container?: string;\n /** The floating label. */\n label?: string;\n /** The `<input>` or `<textarea>`. */\n input?: string;\n /** The leading icon's 48px box. */\n leadingIcon?: string;\n /** The trailing icon's 48px box. */\n trailingIcon?: string;\n /** The prefix text before the input. */\n prefix?: string;\n /** The suffix text after the input. */\n suffix?: string;\n /** The helper text below the field. */\n supportingText?: string;\n /** The error message below the field. */\n errorText?: string;\n /** The character counter below the field. */\n counter?: string;\n /** The built-in error icon. */\n errorIcon?: string;\n /** The outlined field's border (a `<fieldset>`, notched for the floated label). */\n outline?: string;\n /** The filled field's active indicator, the line along the container's bottom. */\n activeIndicator?: string;\n}\n\n/** Words the text field announces; override them for other languages. */\nexport interface TextFieldLabels {\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n /**\n * What screen readers hear for the character counter (M3 calls it the \"character count\").\n * @default (count, max) => `Character count: ${count} of ${max}`\n */\n characterCount: (count: number, max: number) => string;\n}\n\nconst DEFAULT_LABELS: TextFieldLabels = {\n error: 'Error',\n characterCount: (count, max) => `Character count: ${count} of ${max}`,\n};\n\ntype FieldState = 'disabled' | 'error-focus' | 'error-hover' | 'error' | 'focus' | 'hover' | 'rest';\n\n/** Props React Aria names with an `is` prefix; we use the HTML-style names instead. */\ntype RenamedAriaProps =\n | 'isDisabled'\n | 'isReadOnly'\n | 'isRequired'\n | 'isInvalid'\n | 'label'\n | 'description'\n | 'inputElementType'\n | 'value'\n | 'defaultValue'\n | 'onChange'\n // Left out: the deprecated `validationState` (`invalid` replaces it) and React Aria's\n // `excludeFromTabOrder` (`tabIndex={-1}` covers the rare case).\n | 'validationState'\n | 'excludeFromTabOrder';\n\ninterface TextFieldOwnProps\n extends\n Omit<AriaTextFieldProps<HTMLInputElement>, RenamedAriaProps>,\n Omit<WrapperRootDOMProps, keyof AriaTextFieldProps<HTMLInputElement>>,\n // The attributes that describe the input go to it, with React Aria's `aria-controls`,\n // `aria-haspopup`, `aria-autocomplete` and `aria-activedescendant`: a field that opens its\n // own suggestions can say so.\n Pick<\n AriaAttributes,\n | 'aria-expanded'\n | 'aria-required'\n | 'aria-invalid'\n | 'aria-readonly'\n | 'aria-multiline'\n | 'aria-placeholder'\n > {\n /** The `<input>`'s (or `<textarea>`'s) id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** The container style: a filled surface or an outline. @default \"filled\" */\n variant?: TextFieldVariant;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the container (56px at 0, 44px at -3).\n * Left out, the field takes the density of the nearest `ThemeProvider` or `ThemeScope`. M3\n * asks for density to be the reader's choice rather than a default, so prefer setting it\n * there, from a setting.\n * @default 0\n */\n density?: Density;\n /** The text, when controlled. */\n value?: string;\n /** The initial text, when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the new text on every change. */\n onChange?: (value: string) => void;\n /**\n * Helper text below the field. Replaced visually by the error message when invalid, and\n * shown in the error colour while the field is invalid.\n */\n supportingText?: ReactNode;\n /**\n * Shows the supporting text only while the field has focus (M3 allows it \"persistently\n * visible or visible only on focus\"). Its space is kept, and screen readers always get it.\n * @default false\n */\n supportingTextOnFocus?: boolean;\n /** Marks the field invalid (in addition to `validate` and native constraints). */\n invalid?: boolean;\n /** A value is required: adds the `*` to the label and native `required` validation. */\n required?: boolean;\n /** Disables the field: it can't be focused or edited. @default false */\n disabled?: boolean;\n /**\n * The value can be read and copied but not changed. M3 styles it as a regular field and\n * asks for it to be \"clearly labeled as read-only\", e.g. in the supporting text. The root\n * gets `data-readonly`.\n * @default false\n */\n readOnly?: boolean;\n /** Decorative icon at the start, in a 48px box. */\n leadingIcon?: ReactNode;\n /**\n * Icon or icon button at the end (e.g. clear, visibility toggle), in a 48px box. It takes\n * the place of the error icon.\n */\n trailingIcon?: ReactNode;\n /**\n * The icon shown at the end while the field is invalid and has no `trailingIcon` (M3\n * strongly recommends one). Defaults to Material Symbols `error`, named by `labels.error`;\n * `false` turns it off.\n */\n errorIcon?: ReactNode | false;\n /** Text before the input, e.g. \"$\". Shown once the label floats. */\n prefix?: ReactNode;\n /**\n * What screen readers hear for the prefix, e.g. \"US dollars\" for \"$\". The prefix is in the\n * input's description either way; without this, its visible text is read.\n */\n prefixLabel?: string;\n /** Text after the input, e.g. \"kg\". Shown once the label floats. */\n suffix?: ReactNode;\n /** What screen readers hear for the suffix, e.g. \"kilograms\" for \"kg\". */\n suffixLabel?: string;\n /**\n * Renders a `<textarea>`. By default it's an M3 text area: a fixed height of `rows` lines\n * that scrolls vertically, which M3 recommends over growing fields on the web.\n * @default false\n */\n multiline?: boolean;\n /**\n * With `multiline`: grow with the content from `rows` up to `maxRows` (M3's multi-line\n * field) instead of keeping a fixed height.\n * @default false\n */\n autoGrow?: boolean;\n /** Visible lines of a multiline field. @default 4, or 1 with autoGrow */\n rows?: number;\n /** With `autoGrow`: lines after which the field scrolls instead of growing. */\n maxRows?: number;\n /** Shows `count/maxLength` below the field. Defaults to `true` when `maxLength` is set. */\n counter?: boolean;\n /** Words the field announces, for other languages. */\n labels?: Partial<TextFieldLabels>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Classes for each part, by slot. */\n classNames?: TextFieldClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** The root element. Use `inputRef` for the input itself. */\n ref?: Ref<HTMLDivElement>;\n /** The `<input>` (or `<textarea>` when multiline), e.g. for form libraries. */\n inputRef?: Ref<HTMLInputElement | HTMLTextAreaElement>;\n}\n\n/** A visible label, or an accessible name for fields without one. */\ntype TextFieldLabel =\n | {\n /** The visible label, which floats above the input once it has focus or a value. */\n label: ReactNode;\n }\n | {\n label?: undefined;\n /** Accessible name when there's no visible label. */\n 'aria-label': string;\n }\n | {\n label?: undefined;\n /** The id of the element that names the field, when there's no visible label. */\n 'aria-labelledby': string;\n };\n\nexport type TextFieldProps = TextFieldOwnProps & TextFieldLabel;\n\nconst INTERACTIVE = 'input, textarea, button, a, [role=\"button\"]';\n\n/**\n * M3 text field: filled or outlined, with a floating label, leading / trailing icons,\n * prefix and suffix, supporting text, error message and icon, character counter, and a\n * multiline form (a fixed-height text area, or a growing field with `autoGrow`). Built on React Aria `useTextField`, so labelling,\n * descriptions, `validate` and native constraints work as in React Aria.\n *\n * `className` and `style` go on the root; `inputRef` reaches the input.\n *\n * @example\n * <TextField label=\"Email\" type=\"email\" required supportingText=\"We never share it.\" />\n * <TextField variant=\"outlined\" label=\"Price\" prefix=\"$\" inputMode=\"decimal\" />\n */\nexport function TextField(props: TextFieldProps) {\n const {\n variant = 'filled',\n density,\n label,\n value: valueProp,\n defaultValue,\n onChange,\n supportingText,\n supportingTextOnFocus = false,\n errorMessage,\n invalid,\n required,\n disabled = false,\n readOnly,\n leadingIcon,\n trailingIcon,\n errorIcon,\n prefix,\n prefixLabel,\n suffix,\n suffixLabel,\n multiline = false,\n autoGrow = false,\n rows = autoGrow ? 1 : 4,\n maxRows,\n counter,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n inputRef,\n ...otherProps\n } = props;\n\n // `data-*` attributes and the root's DOM attributes describe the component, so they go on the\n // root with `className`; everything else (the input's ARIA, attributes and events) goes to\n // the input via React Aria.\n const { root: rootData, rest: ariaProps } = splitRootProps(otherProps, { wrapper: true });\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const prefixId = useId();\n const suffixId = useId();\n const counterId = useId();\n\n const [value, setValue] = useControlledState(valueProp, defaultValue ?? '', onChange);\n const domInputRef = useObjectRef(inputRef as Ref<HTMLInputElement>);\n const {\n labelProps,\n inputProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useTextField(\n {\n ...ariaProps,\n label,\n description: supportingText,\n errorMessage,\n value,\n onChange: setValue,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n isReadOnly: readOnly,\n inputElementType: multiline ? 'textarea' : 'input',\n } as AriaTextFieldOptions<'input' | 'textarea'>,\n domInputRef,\n );\n const { focusProps, isFocused } = useFocusRing({ isTextInput: true });\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n const growing = multiline && autoGrow;\n // Grow a multiline field with its content, up to maxRows.\n useLayoutEffect(() => {\n const element = domInputRef.current as HTMLTextAreaElement | null;\n if (!growing || !element) return;\n element.style.height = 'auto';\n const lineHeight = Number.parseFloat(getComputedStyle(element).lineHeight) || 24;\n const max = maxRows ? lineHeight * maxRows : Number.POSITIVE_INFINITY;\n element.style.height = `${Math.min(element.scrollHeight, max)}px`;\n element.style.overflowY = element.scrollHeight > max ? 'auto' : 'hidden';\n }, [value, growing, maxRows, domInputRef]);\n\n const hasLabel = label !== undefined && label !== null;\n const floated = hasLabel && (isFocused || value.length > 0);\n const state: FieldState = disabled\n ? 'disabled'\n : isInvalid\n ? isFocused\n ? 'error-focus'\n : isHovered\n ? 'error-hover'\n : 'error'\n : isFocused\n ? 'focus'\n : isHovered\n ? 'hover'\n : 'rest';\n\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n const showError = isInvalid && Boolean(resolvedError);\n const maxLength = ariaProps.maxLength;\n const showCounter = maxLength !== undefined && counter !== false;\n const showErrorIcon = isInvalid && !trailingIcon && errorIcon !== false;\n\n const styles = textFieldStyles({\n variant,\n hasLabel,\n hasLeadingIcon: Boolean(leadingIcon),\n hasTrailingIcon: Boolean(trailingIcon) || showErrorIcon,\n multiline,\n fixedHeight: multiline && !autoGrow,\n supportingTextOnFocus,\n });\n // The affixes and the counter join React Aria's description (supporting text, then error).\n const describedBy =\n [\n prefix ? prefixId : null,\n suffix ? suffixId : null,\n renderedDescription(inputProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !showError && errorMessageProps.id,\n ]),\n showCounter ? counterId : null,\n ]\n .filter(Boolean)\n .join(' ') || undefined;\n\n // Pressing the container (not an icon button) focuses the input, like Compose.\n const focusInput = (event: PointerEvent<HTMLDivElement>) => {\n if ((event.target as Element).closest(INTERACTIVE) || disabled) return;\n event.preventDefault();\n // The field is where the press landed: the page never scrolls to it (2026-10-15).\n domInputRef.current?.focus({ preventScroll: true });\n };\n // Chrome's touch adjustment can send a tap's pointer events to the input while the mouse\n // events it emulates, and the click, land on the padding beside it, so the input never\n // takes focus (2026-10-17). A click on the field that leaves the input unfocused focuses it.\n // The mouse never needs it: its press on the padding already focused the input.\n const focusInputOnClick = (event: MouseEvent<HTMLDivElement>) => {\n const target = event.target as Element;\n if (\n (event.nativeEvent as globalThis.PointerEvent).pointerType === 'mouse' ||\n !event.currentTarget.contains(target) ||\n target.closest(INTERACTIVE) ||\n disabled ||\n document.activeElement === domInputRef.current\n ) {\n return;\n }\n domInputRef.current?.focus({ preventScroll: true });\n };\n\n // Part of the accessible name, as M3 asks (\"The accessibility label must include the\n // asterisk\"); the copy in the outline's notch is hidden with the fieldset.\n const requiredMark = required ? ' *' : null;\n const Input = multiline ? 'textarea' : 'input';\n\n return (\n <div\n {...rootData}\n ref={ref}\n style={style}\n className={styles.root({ class: cn(classNames?.root, className) })}\n data-tokens={mergeDataTokens('md-comp-text-field', rootData)}\n data-variant={variant}\n {...densityAttribute(density)}\n data-field-state={state}\n data-focused={isFocused || undefined}\n data-hovered={(isHovered && !disabled) || undefined}\n data-invalid={isInvalid || undefined}\n data-disabled={disabled || undefined}\n data-readonly={readOnly || undefined}\n data-floated={floated || undefined}\n >\n <div\n {...hoverProps}\n onPointerDown={focusInput}\n onClick={focusInputOnClick}\n data-slot=\"container\"\n className={styles.container({ class: classNames?.container })}\n >\n {variant === 'outlined' && (\n <fieldset\n aria-hidden=\"true\"\n data-slot=\"outline\"\n className={styles.outline({ class: classNames?.outline })}\n >\n {hasLabel && (\n <legend className={styles.notch()}>\n <span className={styles.notchClip()}>\n <span className={styles.notchText()}>\n {label}\n {requiredMark}\n </span>\n </span>\n </legend>\n )}\n </fieldset>\n )}\n {leadingIcon && (\n <span\n aria-hidden=\"true\"\n data-slot=\"leading-icon\"\n className={styles.icon({ class: cn(styles.leadingIcon(), classNames?.leadingIcon) })}\n >\n {leadingIcon}\n </span>\n )}\n <div className={styles.field()}>\n {prefix && (\n <span\n id={prefixId}\n className={styles.affix({ class: cn(styles.prefix(), classNames?.prefix) })}\n >\n <Affix text={prefix} label={prefixLabel} />\n </span>\n )}\n <Input\n {...(mergeProps(inputProps, pickWidgetAria(ariaProps), focusProps) as object)}\n aria-describedby={describedBy}\n ref={domInputRef as Ref<never>}\n data-slot=\"input\"\n rows={multiline ? rows : undefined}\n className={styles.input({ class: classNames?.input })}\n />\n {suffix && (\n <span\n id={suffixId}\n className={styles.affix({ class: cn(styles.suffix(), classNames?.suffix) })}\n >\n <Affix text={suffix} label={suffixLabel} />\n </span>\n )}\n </div>\n {trailingIcon ? (\n <span\n data-slot=\"trailing-icon\"\n className={styles.icon({\n class: cn(styles.trailingIcon(), classNames?.trailingIcon),\n })}\n >\n {trailingIcon}\n </span>\n ) : showErrorIcon ? (\n <span className={styles.icon({ class: styles.trailingIcon() })}>\n <FieldErrorIcon\n label={labels.error}\n icon={errorIcon}\n className={styles.errorIcon({ class: classNames?.errorIcon })}\n />\n </span>\n ) : null}\n {hasLabel && (\n <FieldLabel\n {...labelProps}\n data-slot=\"label\"\n className={styles.label({ class: classNames?.label })}\n >\n {label}\n {requiredMark}\n </FieldLabel>\n )}\n <span\n aria-hidden=\"true\"\n data-slot=\"active-indicator\"\n className={styles.indicator({ class: classNames?.activeIndicator })}\n />\n </div>\n {(supportingText || showError || showCounter) && (\n <div className={styles.supporting()}>\n {supportingText && (\n <SupportingText\n {...descriptionProps}\n data-slot=\"supporting-text\"\n className={cn(\n styles.supportingText({\n class: cn(styles.supportingTextColor(), classNames?.supportingText),\n }),\n showError && 'sr-only',\n )}\n >\n {supportingText}\n </SupportingText>\n )}\n {showError && (\n <ErrorText\n {...errorMessageProps}\n className={styles.supportingText({\n supportingTextOnFocus: false,\n class: classNames?.errorText,\n })}\n >\n {resolvedError}\n </ErrorText>\n )}\n {showCounter && (\n <CharacterCounter\n id={counterId}\n count={value.length}\n max={maxLength}\n label={labels.characterCount(value.length, maxLength)}\n className={styles.counter({ class: classNames?.counter })}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** An affix's visible text, read as its `label` when there is one. */\nfunction Affix({ text, label }: { text: ReactNode; label: string | undefined }) {\n if (label === undefined) return text;\n return (\n <>\n <span aria-hidden=\"true\">{text}</span>\n <span className=\"sr-only\">{label}</span>\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TextField.js","names":[],"sources":["../../../src/components/text-field/TextField.tsx"],"sourcesContent":["'use client';\n\nimport {\n useId,\n useLayoutEffect,\n type AriaAttributes,\n type CSSProperties,\n type MouseEvent,\n type PointerEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n mergeProps,\n useFocusRing,\n useHover,\n useObjectRef,\n useTextField,\n type AriaTextFieldOptions,\n type AriaTextFieldProps,\n} from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport {\n CharacterCounter,\n ErrorText,\n FieldErrorIcon,\n FieldLabel,\n SupportingText,\n} from '../../primitives/Field';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { pickWidgetAria, splitRootProps, type WrapperRootDOMProps } from '../../utils/root-props';\nimport { textFieldStyles, type TextFieldVariant } from './text-field-styles';\n\nexport interface TextFieldClassNames {\n /** The root element, merged with `className`. */\n root?: string;\n /** The filled or outlined container around the label, input and icons. */\n container?: string;\n /** The floating label. */\n label?: string;\n /** The `<input>` or `<textarea>`. */\n input?: string;\n /** The leading icon's 48px box. */\n leadingIcon?: string;\n /** The trailing icon's 48px box. */\n trailingIcon?: string;\n /** The prefix text before the input. */\n prefix?: string;\n /** The suffix text after the input. */\n suffix?: string;\n /** The helper text below the field. */\n supportingText?: string;\n /** The error message below the field. */\n errorText?: string;\n /** The character counter below the field. */\n counter?: string;\n /** The built-in error icon. */\n errorIcon?: string;\n /** The outlined field's border (a `<fieldset>`, notched for the floated label). */\n outline?: string;\n /** The filled field's active indicator, the line along the container's bottom. */\n activeIndicator?: string;\n}\n\n/** Words the text field announces; override them for other languages. */\nexport interface TextFieldLabels {\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n /**\n * What screen readers hear for the character counter (M3 calls it the \"character count\").\n * @default (count, max) => `Character count: ${count} of ${max}`\n */\n characterCount: (count: number, max: number) => string;\n}\n\nconst DEFAULT_LABELS: TextFieldLabels = {\n error: 'Error',\n characterCount: (count, max) => `Character count: ${count} of ${max}`,\n};\n\n/** Input types the browser draws its own format in when empty, which a resting label would cover. */\nconst FORMAT_SHOWING_TYPES = new Set(['date', 'time', 'datetime-local', 'month', 'week']);\n\ntype FieldState = 'disabled' | 'error-focus' | 'error-hover' | 'error' | 'focus' | 'hover' | 'rest';\n\n/** Props React Aria names with an `is` prefix; we use the HTML-style names instead. */\ntype RenamedAriaProps =\n | 'isDisabled'\n | 'isReadOnly'\n | 'isRequired'\n | 'isInvalid'\n | 'label'\n | 'description'\n | 'inputElementType'\n | 'value'\n | 'defaultValue'\n | 'onChange'\n // Left out: the deprecated `validationState` (`invalid` replaces it) and React Aria's\n // `excludeFromTabOrder` (`tabIndex={-1}` covers the rare case).\n | 'validationState'\n | 'excludeFromTabOrder';\n\ninterface TextFieldOwnProps\n extends\n Omit<AriaTextFieldProps<HTMLInputElement>, RenamedAriaProps>,\n Omit<WrapperRootDOMProps, keyof AriaTextFieldProps<HTMLInputElement>>,\n // The attributes that describe the input go to it, with React Aria's `aria-controls`,\n // `aria-haspopup`, `aria-autocomplete` and `aria-activedescendant`: a field that opens its\n // own suggestions can say so.\n Pick<\n AriaAttributes,\n | 'aria-expanded'\n | 'aria-required'\n | 'aria-invalid'\n | 'aria-readonly'\n | 'aria-multiline'\n | 'aria-placeholder'\n > {\n /** The `<input>`'s (or `<textarea>`'s) id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** The container style: a filled surface or an outline. @default \"filled\" */\n variant?: TextFieldVariant;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the container (56px at 0, 44px at -3).\n * Left out, the field takes the density of the nearest `ThemeProvider` or `ThemeScope`. M3\n * asks for density to be the reader's choice rather than a default, so prefer setting it\n * there, from a setting.\n * @default 0\n */\n density?: Density;\n /** The text, when controlled. */\n value?: string;\n /** The initial text, when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the new text on every change. */\n onChange?: (value: string) => void;\n /**\n * Helper text below the field. Replaced visually by the error message when invalid, and\n * shown in the error colour while the field is invalid.\n */\n supportingText?: ReactNode;\n /**\n * Shows the supporting text only while the field has focus (M3 allows it \"persistently\n * visible or visible only on focus\"). Its space is kept, and screen readers always get it.\n * @default false\n */\n supportingTextOnFocus?: boolean;\n /** Marks the field invalid (in addition to `validate` and native constraints). */\n invalid?: boolean;\n /** A value is required: adds the `*` to the label and native `required` validation. */\n required?: boolean;\n /** Disables the field: it can't be focused or edited. @default false */\n disabled?: boolean;\n /**\n * The value can be read and copied but not changed. M3 styles it as a regular field and\n * asks for it to be \"clearly labeled as read-only\", e.g. in the supporting text. The root\n * gets `data-readonly`.\n * @default false\n */\n readOnly?: boolean;\n /** Decorative icon at the start, in a 48px box. */\n leadingIcon?: ReactNode;\n /**\n * Icon or icon button at the end (e.g. clear, visibility toggle), in a 48px box. It takes\n * the place of the error icon.\n */\n trailingIcon?: ReactNode;\n /**\n * The icon shown at the end while the field is invalid and has no `trailingIcon` (M3\n * strongly recommends one). Defaults to Material Symbols `error`, named by `labels.error`;\n * `false` turns it off.\n */\n errorIcon?: ReactNode | false;\n /** Text before the input, e.g. \"$\". Shown once the label floats. */\n prefix?: ReactNode;\n /**\n * What screen readers hear for the prefix, e.g. \"US dollars\" for \"$\". The prefix is in the\n * input's description either way; without this, its visible text is read.\n */\n prefixLabel?: string;\n /** Text after the input, e.g. \"kg\". Shown once the label floats. */\n suffix?: ReactNode;\n /** What screen readers hear for the suffix, e.g. \"kilograms\" for \"kg\". */\n suffixLabel?: string;\n /**\n * Renders a `<textarea>`. By default it's an M3 text area: a fixed height of `rows` lines\n * that scrolls vertically, which M3 recommends over growing fields on the web.\n * @default false\n */\n multiline?: boolean;\n /**\n * With `multiline`: grow with the content from `rows` up to `maxRows` (M3's multi-line\n * field) instead of keeping a fixed height.\n * @default false\n */\n autoGrow?: boolean;\n /** Visible lines of a multiline field. @default 4, or 1 with autoGrow */\n rows?: number;\n /** With `autoGrow`: lines after which the field scrolls instead of growing. */\n maxRows?: number;\n /** Shows `count/maxLength` below the field. Defaults to `true` when `maxLength` is set. */\n counter?: boolean;\n /** Words the field announces, for other languages. */\n labels?: Partial<TextFieldLabels>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Classes for each part, by slot. */\n classNames?: TextFieldClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** The root element. Use `inputRef` for the input itself. */\n ref?: Ref<HTMLDivElement>;\n /** The `<input>` (or `<textarea>` when multiline), e.g. for form libraries. */\n inputRef?: Ref<HTMLInputElement | HTMLTextAreaElement>;\n}\n\n/** A visible label, or an accessible name for fields without one. */\ntype TextFieldLabel =\n | {\n /** The visible label, which floats above the input once it has focus or a value. */\n label: ReactNode;\n }\n | {\n label?: undefined;\n /** Accessible name when there's no visible label. */\n 'aria-label': string;\n }\n | {\n label?: undefined;\n /** The id of the element that names the field, when there's no visible label. */\n 'aria-labelledby': string;\n };\n\nexport type TextFieldProps = TextFieldOwnProps & TextFieldLabel;\n\nconst INTERACTIVE = 'input, textarea, button, a, [role=\"button\"]';\n\n/**\n * M3 text field: filled or outlined, with a floating label, leading / trailing icons,\n * prefix and suffix, supporting text, error message and icon, character counter, and a\n * multiline form (a fixed-height text area, or a growing field with `autoGrow`). Built on React Aria `useTextField`, so labelling,\n * descriptions, `validate` and native constraints work as in React Aria.\n *\n * `className` and `style` go on the root; `inputRef` reaches the input.\n *\n * @example\n * <TextField label=\"Email\" type=\"email\" required supportingText=\"We never share it.\" />\n * <TextField variant=\"outlined\" label=\"Price\" prefix=\"$\" inputMode=\"decimal\" />\n */\nexport function TextField(props: TextFieldProps) {\n const {\n variant = 'filled',\n density,\n label,\n value: valueProp,\n defaultValue,\n onChange,\n supportingText,\n supportingTextOnFocus = false,\n errorMessage,\n invalid,\n required,\n disabled = false,\n readOnly,\n leadingIcon,\n trailingIcon,\n errorIcon,\n prefix,\n prefixLabel,\n suffix,\n suffixLabel,\n multiline = false,\n autoGrow = false,\n rows = autoGrow ? 1 : 4,\n maxRows,\n counter,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n inputRef,\n ...otherProps\n } = props;\n\n // `data-*` attributes and the root's DOM attributes describe the component, so they go on the\n // root with `className`; everything else (the input's ARIA, attributes and events) goes to\n // the input via React Aria.\n const { root: rootData, rest: ariaProps } = splitRootProps(otherProps, { wrapper: true });\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const prefixId = useId();\n const suffixId = useId();\n const counterId = useId();\n\n const [value, setValue] = useControlledState(valueProp, defaultValue ?? '', onChange);\n const domInputRef = useObjectRef(inputRef as Ref<HTMLInputElement>);\n const {\n labelProps,\n inputProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useTextField(\n {\n ...ariaProps,\n label,\n description: supportingText,\n errorMessage,\n value,\n onChange: setValue,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n isReadOnly: readOnly,\n inputElementType: multiline ? 'textarea' : 'input',\n } as AriaTextFieldOptions<'input' | 'textarea'>,\n domInputRef,\n );\n const { focusProps, isFocused } = useFocusRing({ isTextInput: true });\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n const growing = multiline && autoGrow;\n // Grow a multiline field with its content, up to maxRows.\n useLayoutEffect(() => {\n const element = domInputRef.current as HTMLTextAreaElement | null;\n if (!growing || !element) return;\n element.style.height = 'auto';\n const lineHeight = Number.parseFloat(getComputedStyle(element).lineHeight) || 24;\n const max = maxRows ? lineHeight * maxRows : Number.POSITIVE_INFINITY;\n element.style.height = `${Math.min(element.scrollHeight, max)}px`;\n element.style.overflowY = element.scrollHeight > max ? 'auto' : 'hidden';\n }, [value, growing, maxRows, domInputRef]);\n\n const hasLabel = label !== undefined && label !== null;\n const floated =\n hasLabel && (isFocused || value.length > 0 || FORMAT_SHOWING_TYPES.has(ariaProps.type ?? ''));\n const state: FieldState = disabled\n ? 'disabled'\n : isInvalid\n ? isFocused\n ? 'error-focus'\n : isHovered\n ? 'error-hover'\n : 'error'\n : isFocused\n ? 'focus'\n : isHovered\n ? 'hover'\n : 'rest';\n\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n const showError = isInvalid && Boolean(resolvedError);\n const maxLength = ariaProps.maxLength;\n const showCounter = maxLength !== undefined && counter !== false;\n const showErrorIcon = isInvalid && !trailingIcon && errorIcon !== false;\n\n const styles = textFieldStyles({\n variant,\n hasLabel,\n hasLeadingIcon: Boolean(leadingIcon),\n hasTrailingIcon: Boolean(trailingIcon) || showErrorIcon,\n multiline,\n fixedHeight: multiline && !autoGrow,\n supportingTextOnFocus,\n });\n // The affixes and the counter join React Aria's description (supporting text, then error).\n const describedBy =\n [\n prefix ? prefixId : null,\n suffix ? suffixId : null,\n renderedDescription(inputProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !showError && errorMessageProps.id,\n ]),\n showCounter ? counterId : null,\n ]\n .filter(Boolean)\n .join(' ') || undefined;\n\n // Pressing the container (not an icon button) focuses the input, like Compose.\n const focusInput = (event: PointerEvent<HTMLDivElement>) => {\n if ((event.target as Element).closest(INTERACTIVE) || disabled) return;\n event.preventDefault();\n // The field is where the press landed: the page never scrolls to it (2026-10-15).\n domInputRef.current?.focus({ preventScroll: true });\n };\n // Chrome's touch adjustment can send a tap's pointer events to the input while the mouse\n // events it emulates, and the click, land on the padding beside it, so the input never\n // takes focus (2026-10-17). A click on the field that leaves the input unfocused focuses it.\n // The mouse never needs it: its press on the padding already focused the input.\n const focusInputOnClick = (event: MouseEvent<HTMLDivElement>) => {\n const target = event.target as Element;\n if (\n (event.nativeEvent as globalThis.PointerEvent).pointerType === 'mouse' ||\n !event.currentTarget.contains(target) ||\n target.closest(INTERACTIVE) ||\n disabled ||\n document.activeElement === domInputRef.current\n ) {\n return;\n }\n domInputRef.current?.focus({ preventScroll: true });\n };\n\n // Part of the accessible name, as M3 asks (\"The accessibility label must include the\n // asterisk\"); the copy in the outline's notch is hidden with the fieldset.\n const requiredMark = required ? ' *' : null;\n const Input = multiline ? 'textarea' : 'input';\n\n return (\n <div\n {...rootData}\n ref={ref}\n style={style}\n className={styles.root({ class: cn(classNames?.root, className) })}\n data-tokens={mergeDataTokens('md-comp-text-field', rootData)}\n data-variant={variant}\n {...densityAttribute(density)}\n data-field-state={state}\n data-focused={isFocused || undefined}\n data-hovered={(isHovered && !disabled) || undefined}\n data-invalid={isInvalid || undefined}\n data-disabled={disabled || undefined}\n data-readonly={readOnly || undefined}\n data-floated={floated || undefined}\n >\n <div\n {...hoverProps}\n onPointerDown={focusInput}\n onClick={focusInputOnClick}\n data-slot=\"container\"\n className={styles.container({ class: classNames?.container })}\n >\n {variant === 'outlined' && (\n <fieldset\n aria-hidden=\"true\"\n data-slot=\"outline\"\n className={styles.outline({ class: classNames?.outline })}\n >\n {hasLabel && (\n <legend className={styles.notch()}>\n <span className={styles.notchClip()}>\n <span className={styles.notchText()}>\n {label}\n {requiredMark}\n </span>\n </span>\n </legend>\n )}\n </fieldset>\n )}\n {leadingIcon && (\n <span\n aria-hidden=\"true\"\n data-slot=\"leading-icon\"\n className={styles.icon({ class: cn(styles.leadingIcon(), classNames?.leadingIcon) })}\n >\n {leadingIcon}\n </span>\n )}\n <div className={styles.field()}>\n {prefix && (\n <span\n id={prefixId}\n className={styles.affix({ class: cn(styles.prefix(), classNames?.prefix) })}\n >\n <Affix text={prefix} label={prefixLabel} />\n </span>\n )}\n <Input\n {...(mergeProps(inputProps, pickWidgetAria(ariaProps), focusProps) as object)}\n aria-describedby={describedBy}\n ref={domInputRef as Ref<never>}\n data-slot=\"input\"\n rows={multiline ? rows : undefined}\n className={styles.input({ class: classNames?.input })}\n />\n {suffix && (\n <span\n id={suffixId}\n className={styles.affix({ class: cn(styles.suffix(), classNames?.suffix) })}\n >\n <Affix text={suffix} label={suffixLabel} />\n </span>\n )}\n </div>\n {trailingIcon ? (\n <span\n data-slot=\"trailing-icon\"\n className={styles.icon({\n class: cn(styles.trailingIcon(), classNames?.trailingIcon),\n })}\n >\n {trailingIcon}\n </span>\n ) : showErrorIcon ? (\n <span className={styles.icon({ class: styles.trailingIcon() })}>\n <FieldErrorIcon\n label={labels.error}\n icon={errorIcon}\n className={styles.errorIcon({ class: classNames?.errorIcon })}\n />\n </span>\n ) : null}\n {hasLabel && (\n <FieldLabel\n {...labelProps}\n data-slot=\"label\"\n className={styles.label({ class: classNames?.label })}\n >\n {label}\n {requiredMark}\n </FieldLabel>\n )}\n <span\n aria-hidden=\"true\"\n data-slot=\"active-indicator\"\n className={styles.indicator({ class: classNames?.activeIndicator })}\n />\n </div>\n {(supportingText || showError || showCounter) && (\n <div className={styles.supporting()}>\n {supportingText && (\n <SupportingText\n {...descriptionProps}\n data-slot=\"supporting-text\"\n className={cn(\n styles.supportingText({\n class: cn(styles.supportingTextColor(), classNames?.supportingText),\n }),\n showError && 'sr-only',\n )}\n >\n {supportingText}\n </SupportingText>\n )}\n {showError && (\n <ErrorText\n {...errorMessageProps}\n className={styles.supportingText({\n supportingTextOnFocus: false,\n class: classNames?.errorText,\n })}\n >\n {resolvedError}\n </ErrorText>\n )}\n {showCounter && (\n <CharacterCounter\n id={counterId}\n count={value.length}\n max={maxLength}\n label={labels.characterCount(value.length, maxLength)}\n className={styles.counter({ class: classNames?.counter })}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** An affix's visible text, read as its `label` when there is one. */\nfunction Affix({ text, label }: { text: ReactNode; label: string | undefined }) {\n if (label === undefined) return text;\n return (\n <>\n <span aria-hidden=\"true\">{text}</span>\n <span className=\"sr-only\">{label}</span>\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -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
|