@windmorph/react 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/autocomplete/Autocomplete.d.ts +4 -1
- package/dist/components/autocomplete/Autocomplete.js +3 -5
- package/dist/components/autocomplete/Autocomplete.js.map +1 -1
- package/dist/components/badge/Badge.d.ts +5 -2
- package/dist/components/badge/Badge.js +7 -3
- package/dist/components/badge/Badge.js.map +1 -1
- package/dist/components/button/Button.d.ts +4 -2
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/button/ButtonContext.d.ts +1 -1
- package/dist/components/button/button-pending.js +2 -4
- package/dist/components/button/button-pending.js.map +1 -1
- package/dist/{composites → components}/button-group/ButtonGroup.d.ts +4 -4
- package/dist/{composites → components}/button-group/ButtonGroup.js +9 -7
- package/dist/components/button-group/ButtonGroup.js.map +1 -0
- package/dist/{composites → components}/button-group/button-group-styles.d.ts +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js.map +1 -1
- package/dist/{composites → components}/button-group/lazy-overflow-menu.js +2 -2
- package/dist/components/button-group/lazy-overflow-menu.js.map +1 -0
- package/dist/{composites → components}/button-group/overflow-menu-list.js +2 -2
- package/dist/components/button-group/overflow-menu-list.js.map +1 -0
- package/dist/{composites → components}/button-group/use-overflow.js +1 -1
- package/dist/{composites → components}/button-group/use-overflow.js.map +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js.map +1 -1
- package/dist/components/carousel/Carousel.d.ts +4 -1
- package/dist/components/carousel/Carousel.js +2 -4
- package/dist/components/carousel/Carousel.js.map +1 -1
- package/dist/components/checkbox/checkbox-styles.d.ts +3 -3
- package/dist/components/chip/Chip.d.ts +8 -2
- package/dist/components/chip/Chip.js +3 -8
- package/dist/components/chip/Chip.js.map +1 -1
- package/dist/components/chip/chip-tokens.d.ts +4 -4
- package/dist/components/date-picker/DateField.d.ts +4 -1
- package/dist/components/date-picker/DateField.js +3 -1
- package/dist/components/date-picker/DateField.js.map +1 -1
- package/dist/components/date-picker/DatePicker.d.ts +4 -1
- package/dist/components/date-picker/DatePicker.js +5 -2
- package/dist/components/date-picker/DatePicker.js.map +1 -1
- package/dist/components/date-picker/PickerDialog.d.ts +4 -1
- package/dist/components/date-picker/PickerDialog.js +3 -1
- package/dist/components/date-picker/PickerDialog.js.map +1 -1
- package/dist/components/date-picker/date-picker-styles.d.ts +12 -12
- package/dist/components/date-picker/date-picker-tokens.d.ts +1 -1
- package/dist/components/dialog/Dialog.d.ts +5 -2
- package/dist/components/dialog/Dialog.js +4 -2
- package/dist/components/dialog/Dialog.js.map +1 -1
- package/dist/components/dialog/dialog-tokens.d.ts +1 -1
- package/dist/components/fab-menu/FabMenu.js +2 -2
- package/dist/components/fab-menu/FabMenu.js.map +1 -1
- package/dist/components/icon-button/IconButton.d.ts +4 -2
- package/dist/components/icon-button/IconButton.js.map +1 -1
- package/dist/components/labels/LabelsProvider.d.ts +35 -0
- package/dist/components/labels/LabelsProvider.js +47 -0
- package/dist/components/labels/LabelsProvider.js.map +1 -0
- package/dist/components/labels/use-labels.d.ts +16 -0
- package/dist/components/labels/use-labels.js +42 -0
- package/dist/components/labels/use-labels.js.map +1 -0
- package/dist/components/menu/Menu.d.ts +17 -1
- package/dist/components/menu/Menu.js +21 -11
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/menu/menu-styles.js +12 -12
- package/dist/components/menu/menu-styles.js.map +1 -1
- package/dist/components/menu/menu-tokens.d.ts +1 -1
- package/dist/components/menu/menu-tokens.js.map +1 -1
- package/dist/components/navigation/NavItem.d.ts +1 -1
- package/dist/components/navigation/NavItem.js +9 -6
- package/dist/components/navigation/NavItem.js.map +1 -1
- package/dist/components/navigation/navigation-styles.js +1 -1
- package/dist/components/navigation/navigation-styles.js.map +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.d.ts +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.js +2 -4
- package/dist/components/pull-to-refresh/PullToRefresh.js.map +1 -1
- package/dist/components/search/SearchField.d.ts +4 -1
- package/dist/components/search/SearchField.js +2 -4
- package/dist/components/search/SearchField.js.map +1 -1
- package/dist/components/search/search-styles.js +2 -2
- package/dist/components/search/search-styles.js.map +1 -1
- package/dist/components/select/Select.d.ts +4 -1
- package/dist/components/select/Select.js +2 -4
- package/dist/components/select/Select.js.map +1 -1
- package/dist/components/sheet/BottomSheet.d.ts +4 -1
- package/dist/components/sheet/BottomSheet.js +2 -4
- package/dist/components/sheet/BottomSheet.js.map +1 -1
- package/dist/components/sheet/SideSheet.d.ts +4 -1
- package/dist/components/sheet/SideSheet.js +2 -4
- package/dist/components/sheet/SideSheet.js.map +1 -1
- package/dist/components/slider/Slider.d.ts +4 -1
- package/dist/components/slider/Slider.js +7 -5
- package/dist/components/slider/Slider.js.map +1 -1
- package/dist/components/snackbar/Snackbar.d.ts +8 -2
- package/dist/components/snackbar/Snackbar.js +2 -4
- package/dist/components/snackbar/Snackbar.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
- package/dist/components/split-button/SplitButton.d.ts +4 -1
- package/dist/components/split-button/SplitButton.js +4 -2
- package/dist/components/split-button/SplitButton.js.map +1 -1
- package/dist/components/switch/switch-styles.d.ts +3 -3
- package/dist/components/text-field/TextField.d.ts +4 -1
- package/dist/components/text-field/TextField.js +2 -4
- package/dist/components/text-field/TextField.js.map +1 -1
- package/dist/components/text-field/text-field-styles.js +2 -2
- package/dist/components/text-field/text-field-styles.js.map +1 -1
- package/dist/components/time-picker/TimeField.d.ts +24 -2
- package/dist/components/time-picker/TimeField.js +40 -12
- package/dist/components/time-picker/TimeField.js.map +1 -1
- package/dist/components/time-picker/TimeFieldList.js +95 -0
- package/dist/components/time-picker/TimeFieldList.js.map +1 -0
- package/dist/components/time-picker/TimePicker.d.ts +9 -5
- package/dist/components/time-picker/TimePicker.js +5 -2
- package/dist/components/time-picker/TimePicker.js.map +1 -1
- package/dist/components/time-picker/time-list.js +33 -0
- package/dist/components/time-picker/time-list.js.map +1 -0
- package/dist/components/tooltip/Tooltip.d.ts +1 -1
- package/dist/components/tooltip/tooltip-tokens.d.ts +1 -1
- package/dist/index.d.ts +40 -38
- package/dist/index.js +4 -3
- package/dist/primitives/use-m3-interaction.js +10 -6
- package/dist/primitives/use-m3-interaction.js.map +1 -1
- package/dist/primitives.d.ts +7 -4
- package/dist/primitives.js +4 -1
- package/dist/styles/utilities.css +22 -15
- package/dist/styles.compiled.css +1 -1
- package/dist/utils/use-scroll-keys.d.ts +29 -0
- package/dist/utils/use-scroll-keys.js +66 -0
- package/dist/utils/use-scroll-keys.js.map +1 -0
- package/dist/utils/use-scroll-restoration.d.ts +43 -0
- package/dist/utils/use-scroll-restoration.js +173 -0
- package/dist/utils/use-scroll-restoration.js.map +1 -0
- package/dist/windmorph-labels.d.ts +101 -0
- package/package.json +1 -1
- package/dist/composites/button-group/ButtonGroup.js.map +0 -1
- package/dist/composites/button-group/lazy-overflow-menu.js.map +0 -1
- package/dist/composites/button-group/overflow-menu-list.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Snackbar.js","names":[],"sources":["../../../src/components/snackbar/Snackbar.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n useSyncExternalStore,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { usePresence } from '../../primitives/use-presence';\nimport { announcePolite, LIVE_REGION_DELAY } from '../../utils/announce';\nimport { cn } from '../../utils/cn';\nimport { firstTabbable, tabbables } from '../../utils/focus';\nimport { matchesShortcut } from '../../utils/shortcut';\nimport { Button } from '../button/Button';\nimport { IconButton } from '../icon-button/IconButton';\nimport { durationMillis, SnackbarHostState, type SnackbarData } from './snackbar-state';\nimport { snackbarHostStyles, snackbarStyles } from './snackbar-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Whether a snackbar is inside a {@link SnackbarHost}, whose live region announces it. */\nconst InSnackbarHost = createContext(false);\n\n/** Material Symbols \"close\" (Apache-2.0). */\nfunction CloseIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z\" />\n </svg>\n );\n}\n\nexport interface SnackbarClassNames {\n /** The snackbar's container. */\n root?: string;\n /** The message text. */\n message?: string;\n /** The row holding the action and dismiss buttons. */\n actions?: string;\n /** The action button. */\n action?: string;\n /** The dismiss (×) button. */\n dismiss?: string;\n}\n\nexport interface SnackbarLabels {\n /** Name of the dismiss (×) button. @default \"Dismiss\" */\n dismiss: string;\n}\n\nconst DEFAULT_LABELS: SnackbarLabels = { dismiss: 'Dismiss' };\n\ninterface SnackbarOwnProps {\n /** The message. */\n children: React.ReactNode;\n /** Label of the action button. */\n actionLabel?: string;\n /** Called when the action button is pressed. */\n onAction?: () => void;\n /** Shows a dismiss (×) button that calls this. Escape calls it too. */\n onDismiss?: () => void;\n /**\n * Called on Escape while focus is inside, which dismisses a snackbar with or without the ×\n * button. @default onDismiss\n */\n onEscape?: () => void;\n /** Words the snackbar announces, with English defaults. */\n labels?: Partial<SnackbarLabels>;\n /** Put the actions under the message, for long action labels. @default false */\n actionOnNewLine?: boolean;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SnackbarClassNames;\n}\n\nexport type SnackbarProps = SnackbarOwnProps &\n Omit<ComponentPropsWithRef<'div'>, keyof SnackbarOwnProps>;\n\n/**\n * M3 snackbar: a brief message about an app process, with an optional action and dismiss\n * button. Usually shown through a {@link SnackbarHost}; render it directly for custom\n * placement. Escape dismisses it while focus is inside (`onEscape`, or `onDismiss`).\n *\n * Outside a host it announces its message politely itself, when it appears and when the\n * message changes, without moving focus (the host's live region does this inside one).\n *\n * @example\n * <Snackbar actionLabel=\"Undo\" onAction={undo} onDismiss={close}>Message archived</Snackbar>\n */\nexport function Snackbar({\n children,\n actionLabel,\n onAction,\n onDismiss,\n onEscape = onDismiss,\n labels: labelsProp,\n actionOnNewLine = false,\n classNames,\n className,\n onKeyDown,\n ...rest\n}: SnackbarProps) {\n const styles = snackbarStyles({\n newLine: actionOnNewLine && actionLabel !== undefined,\n dismissible: onDismiss !== undefined,\n });\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const hasActions = actionLabel !== undefined || onDismiss !== undefined;\n const inHost = useContext(InSnackbarHost);\n const messageRef = useRef<HTMLDivElement>(null);\n const announced = useRef('');\n // Runs after every render, but announces only a new message.\n useEffect(() => {\n if (inHost) return;\n const text = messageRef.current?.textContent?.trim() ?? '';\n if (!text || text === announced.current) return;\n announced.current = text;\n announcePolite(text);\n });\n return (\n <div\n {...rest}\n onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key === 'Escape' && onEscape && !event.defaultPrevented) {\n event.stopPropagation();\n onEscape();\n }\n }}\n // The component tokens' hook (snackbar-tokens.ts).\n data-tokens={mergeDataTokens('md-comp-snackbar', rest)}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div ref={messageRef} className={styles.message({ class: classNames?.message })}>\n {children}\n </div>\n {hasActions && (\n <div className={styles.actions({ class: classNames?.actions })}>\n {actionLabel !== undefined && (\n <Button\n variant=\"text\"\n onPress={onAction}\n data-slot=\"action\"\n className={styles.action({ class: classNames?.action })}\n >\n {actionLabel}\n </Button>\n )}\n {onDismiss && (\n <IconButton\n variant=\"standard\"\n // The snackbar keeps its 48px height at every density, as a dialog header does.\n density={0}\n icon={<CloseIcon />}\n aria-label={labels.dismiss}\n onPress={onDismiss}\n data-slot=\"dismiss\"\n className={styles.dismiss({ class: classNames?.dismiss })}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** A {@link SnackbarHostState} that lives as long as the component. */\nexport function useSnackbarHostState(): SnackbarHostState {\n const [state] = useState(() => new SnackbarHostState());\n return state;\n}\n\nexport interface SnackbarHostProps extends Omit<ComponentPropsWithRef<'div'>, 'children'> {\n /** The queue of snackbars to show, from `useSnackbarHostState()`. */\n state: SnackbarHostState;\n /** Renders each snackbar; defaults to {@link Snackbar}. */\n snackbar?: (data: SnackbarData) => React.ReactNode;\n /** Words each default snackbar announces, with English defaults. */\n labels?: Partial<SnackbarLabels>;\n /**\n * The key combination that moves focus to a snackbar with actions, as the spec asks for on\n * the web. F6 by default, the key React Aria's toasts take (browsers use it to move between\n * the window's panes): while a snackbar with a control shows, F6 goes to it from anywhere on\n * the page, and pressed again there it stays. Document it where your app lists its\n * shortcuts. `false` turns it off.\n * @default \"F6\"\n */\n focusShortcut?: string | false;\n}\n\n/**\n * Shows the snackbars of a {@link SnackbarHostState} one at a time, in a polite live\n * region. Position it with `className` (e.g. `fixed inset-x-0 bottom-0`); a snackbar\n * expands upward from the bottom edge as it enters.\n *\n * - Snackbars without an action leave after 4s (`short`) or 10s (`long`); the timer pauses\n * while the pointer or focus is on the snackbar. On the web, use an auto-dismissing\n * snackbar only alongside inline feedback for the same event: it may be gone before\n * someone reads it. Snackbars with an action stay until acted on or dismissed.\n * - The live region is rendered empty first: a snackbar shown before the host mounts (one\n * at launch) appears once the region has been registered, so it's announced.\n * - `focusShortcut` (F6) moves focus to a snackbar with actions; Escape dismisses it.\n * When it leaves, focus returns to where it was before it entered the snackbar; if that\n * element is gone (an undone row was removed, say), to the control that followed it in\n * the Tab order, or else the one before it.\n *\n * @example\n * const snackbar = useSnackbarHostState();\n * <SnackbarHost state={snackbar} className=\"fixed inset-x-0 bottom-0\" />\n * snackbar.showSnackbar({ message: 'Saved' });\n */\nexport function SnackbarHost({\n state,\n snackbar,\n labels,\n focusShortcut = 'F6',\n className,\n ref,\n ...rest\n}: SnackbarHostProps) {\n const current = useSyncExternalStore(state.subscribe, state.getSnapshot, () => null);\n // Keep a leaving snackbar rendered while the next one fades in (Compose cross-fades).\n const [items, setItems] = useState<SnackbarData[]>([]);\n const [shown, setShown] = useState<SnackbarData | null>(null);\n if (current !== shown) {\n setShown(current);\n if (current && !items.includes(current)) setItems([...items, current]);\n }\n const rootRef = useObjectRef(ref);\n // Screen readers often skip content that arrives with its live region, so snackbars go in\n // only once the empty region has been on the page for a moment.\n const [regionReady, setRegionReady] = useState(false);\n useEffect(() => {\n const timer = setTimeout(() => setRegionReady(true), LIVE_REGION_DELAY);\n return () => clearTimeout(timer);\n }, []);\n\n // The shortcut focuses the shown snackbar's first control, if it has one.\n useEffect(() => {\n if (!focusShortcut || !current) return;\n const onKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.defaultPrevented || !matchesShortcut(event, focusShortcut)) return;\n const item = rootRef.current?.querySelector(`[data-snackbar-id=\"${current.id}\"]`);\n const target = item ? firstTabbable(item) : null;\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [focusShortcut, current, rootRef]);\n\n const styles = snackbarHostStyles();\n return (\n <div\n {...rest}\n ref={rootRef}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n className={styles.root({ class: className })}\n >\n {(regionReady ? items : []).map((data) => (\n <HostItem\n key={data.id}\n data={data}\n visible={data === current}\n shortcut={\n focusShortcut &&\n (data.visuals.actionLabel !== undefined || data.visuals.withDismissAction)\n ? focusShortcut\n : undefined\n }\n onExited={() => setItems((all) => all.filter((item) => item !== data))}\n className={styles.item()}\n >\n {snackbar ? (\n snackbar(data)\n ) : (\n <Snackbar\n actionLabel={data.visuals.actionLabel}\n onAction={data.performAction}\n onDismiss={data.visuals.withDismissAction ? data.dismiss : undefined}\n onEscape={data.dismiss}\n labels={labels}\n actionOnNewLine={data.visuals.actionOnNewLine}\n >\n {data.visuals.message}\n </Snackbar>\n )}\n </HostItem>\n ))}\n </div>\n );\n}\n\nfunction HostItem({\n data,\n visible,\n shortcut,\n onExited,\n className,\n children,\n}: {\n data: SnackbarData;\n visible: boolean;\n shortcut: string | undefined;\n onExited: () => void;\n className: string;\n children: React.ReactNode;\n}) {\n const { isPresent, isExiting, exitProps } = usePresence(visible);\n const [paused, setPaused] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n // Where focus was before it entered, and whether it's still inside: when the snackbar\n // leaves with focus in it, focus goes back there rather than falling to the page.\n const returnTo = useRef<FocusReturn | null>(null);\n const focusInside = useRef(false);\n\n useLayoutEffect(() => {\n if (visible || !focusInside.current) return;\n focusInside.current = false;\n const record = returnTo.current;\n returnTo.current = null;\n if (record) focusFirstAvailable(record);\n }, [visible]);\n\n useEffect(() => {\n if (!isPresent) onExited();\n }, [isPresent, onExited]);\n\n // Auto-dismiss after the duration, paused while hovered or focused within.\n useEffect(() => {\n const ms = durationMillis(data.visuals);\n if (!visible || paused || !Number.isFinite(ms)) return;\n const timer = setTimeout(data.dismiss, ms);\n return () => clearTimeout(timer);\n }, [data, visible, paused]);\n\n return (\n <div\n {...exitProps}\n ref={ref}\n data-exiting={isExiting || undefined}\n data-snackbar-id={data.id}\n aria-keyshortcuts={visible ? shortcut : undefined}\n inert={!visible}\n onPointerEnter={() => setPaused(true)}\n onPointerLeave={() => setPaused(ref.current?.contains(document.activeElement) ?? false)}\n onFocus={(event) => {\n const from = event.relatedTarget;\n if (!focusInside.current && !event.currentTarget.contains(from as Node | null)) {\n returnTo.current =\n from instanceof HTMLElement ? focusReturn(from, event.currentTarget) : null;\n }\n focusInside.current = true;\n setPaused(true);\n }}\n onBlur={(event) => {\n if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n // A snackbar that is leaving keeps the record, to hand focus back.\n if (visible) focusInside.current = false;\n setPaused(false);\n }}\n className={className}\n >\n <InSnackbarHost value>{children}</InSnackbarHost>\n </div>\n );\n}\n\n/** Where focus goes back to when a snackbar leaves, with fallbacks in Tab order. */\ninterface FocusReturn {\n target: HTMLElement;\n next: HTMLElement | null;\n previous: HTMLElement | null;\n}\n\n/**\n * Records `target` with its neighbours in the page's Tab order (leaving out the snackbar),\n * taken as focus enters the snackbar, so focus has somewhere sensible to go if `target` is\n * removed while the snackbar is open (APG: when the element that had focus is gone, move\n * focus to a logical place rather than letting it fall to the page).\n */\nfunction focusReturn(target: HTMLElement, snackbar: Element): FocusReturn {\n const order = tabbables(target.ownerDocument.body, (el) => snackbar.contains(el));\n const isOutside = (el: HTMLElement) => !el.contains(target) && !target.contains(el);\n const next =\n order.find(\n (el) =>\n isOutside(el) && target.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING,\n ) ?? null;\n const previous =\n order.findLast(\n (el) =>\n isOutside(el) && target.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING,\n ) ?? null;\n return { target, next, previous };\n}\n\nfunction focusFirstAvailable({ target, next, previous }: FocusReturn) {\n const available = [target, next, previous].find(\n (el) => el?.isConnected && !el.closest('[inert]') && !el.matches(':disabled'),\n );\n // The element may have scrolled out of view while the snackbar had focus; returning\n // focus never scrolls the page back to it (2026-10-15).\n available?.focus({ preventScroll: true });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Snackbar.js","names":[],"sources":["../../../src/components/snackbar/Snackbar.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n useSyncExternalStore,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { useLabels } from '../labels/use-labels';\nimport { usePresence } from '../../primitives/use-presence';\nimport { announcePolite, LIVE_REGION_DELAY } from '../../utils/announce';\nimport { cn } from '../../utils/cn';\nimport { firstTabbable, tabbables } from '../../utils/focus';\nimport { matchesShortcut } from '../../utils/shortcut';\nimport { Button } from '../button/Button';\nimport { IconButton } from '../icon-button/IconButton';\nimport { durationMillis, SnackbarHostState, type SnackbarData } from './snackbar-state';\nimport { snackbarHostStyles, snackbarStyles } from './snackbar-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Whether a snackbar is inside a {@link SnackbarHost}, whose live region announces it. */\nconst InSnackbarHost = createContext(false);\n\n/** Material Symbols \"close\" (Apache-2.0). */\nfunction CloseIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z\" />\n </svg>\n );\n}\n\nexport interface SnackbarClassNames {\n /** The snackbar's container. */\n root?: string;\n /** The message text. */\n message?: string;\n /** The row holding the action and dismiss buttons. */\n actions?: string;\n /** The action button. */\n action?: string;\n /** The dismiss (×) button. */\n dismiss?: string;\n}\n\nexport interface SnackbarLabels {\n /** Name of the dismiss (×) button. @default \"Dismiss\" */\n dismiss: string;\n}\n\nconst DEFAULT_LABELS: SnackbarLabels = { dismiss: 'Dismiss' };\n\ninterface SnackbarOwnProps {\n /** The message. */\n children: React.ReactNode;\n /** Label of the action button. */\n actionLabel?: string;\n /** Called when the action button is pressed. */\n onAction?: () => void;\n /** Shows a dismiss (×) button that calls this. Escape calls it too. */\n onDismiss?: () => void;\n /**\n * Called on Escape while focus is inside, which dismisses a snackbar with or without the ×\n * button. @default onDismiss\n */\n onEscape?: () => void;\n /**\n * Words the snackbar announces, with English defaults. A {@link LabelsProvider} sets them for the\n * whole app.\n */\n labels?: Partial<SnackbarLabels>;\n /** Put the actions under the message, for long action labels. @default false */\n actionOnNewLine?: boolean;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SnackbarClassNames;\n}\n\nexport type SnackbarProps = SnackbarOwnProps &\n Omit<ComponentPropsWithRef<'div'>, keyof SnackbarOwnProps>;\n\n/**\n * M3 snackbar: a brief message about an app process, with an optional action and dismiss\n * button. Usually shown through a {@link SnackbarHost}; render it directly for custom\n * placement. Escape dismisses it while focus is inside (`onEscape`, or `onDismiss`).\n *\n * Outside a host it announces its message politely itself, when it appears and when the\n * message changes, without moving focus (the host's live region does this inside one).\n *\n * @example\n * <Snackbar actionLabel=\"Undo\" onAction={undo} onDismiss={close}>Message archived</Snackbar>\n */\nexport function Snackbar({\n children,\n actionLabel,\n onAction,\n onDismiss,\n onEscape = onDismiss,\n labels: labelsProp,\n actionOnNewLine = false,\n classNames,\n className,\n onKeyDown,\n ...rest\n}: SnackbarProps) {\n const styles = snackbarStyles({\n newLine: actionOnNewLine && actionLabel !== undefined,\n dismissible: onDismiss !== undefined,\n });\n const labels = useLabels('Snackbar', DEFAULT_LABELS, labelsProp);\n const hasActions = actionLabel !== undefined || onDismiss !== undefined;\n const inHost = useContext(InSnackbarHost);\n const messageRef = useRef<HTMLDivElement>(null);\n const announced = useRef('');\n // Runs after every render, but announces only a new message.\n useEffect(() => {\n if (inHost) return;\n const text = messageRef.current?.textContent?.trim() ?? '';\n if (!text || text === announced.current) return;\n announced.current = text;\n announcePolite(text);\n });\n return (\n <div\n {...rest}\n onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key === 'Escape' && onEscape && !event.defaultPrevented) {\n event.stopPropagation();\n onEscape();\n }\n }}\n // The component tokens' hook (snackbar-tokens.ts).\n data-tokens={mergeDataTokens('md-comp-snackbar', rest)}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div ref={messageRef} className={styles.message({ class: classNames?.message })}>\n {children}\n </div>\n {hasActions && (\n <div className={styles.actions({ class: classNames?.actions })}>\n {actionLabel !== undefined && (\n <Button\n variant=\"text\"\n onPress={onAction}\n data-slot=\"action\"\n className={styles.action({ class: classNames?.action })}\n >\n {actionLabel}\n </Button>\n )}\n {onDismiss && (\n <IconButton\n variant=\"standard\"\n // The snackbar keeps its 48px height at every density, as a dialog header does.\n density={0}\n icon={<CloseIcon />}\n aria-label={labels.dismiss}\n onPress={onDismiss}\n data-slot=\"dismiss\"\n className={styles.dismiss({ class: classNames?.dismiss })}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** A {@link SnackbarHostState} that lives as long as the component. */\nexport function useSnackbarHostState(): SnackbarHostState {\n const [state] = useState(() => new SnackbarHostState());\n return state;\n}\n\nexport interface SnackbarHostProps extends Omit<ComponentPropsWithRef<'div'>, 'children'> {\n /** The queue of snackbars to show, from `useSnackbarHostState()`. */\n state: SnackbarHostState;\n /** Renders each snackbar; defaults to {@link Snackbar}. */\n snackbar?: (data: SnackbarData) => React.ReactNode;\n /**\n * Words each default snackbar announces, with English defaults. A {@link LabelsProvider} sets\n * them for the whole app.\n */\n labels?: Partial<SnackbarLabels>;\n /**\n * The key combination that moves focus to a snackbar with actions, as the spec asks for on\n * the web. F6 by default, the key React Aria's toasts take (browsers use it to move between\n * the window's panes): while a snackbar with a control shows, F6 goes to it from anywhere on\n * the page, and pressed again there it stays. Document it where your app lists its\n * shortcuts. `false` turns it off.\n * @default \"F6\"\n */\n focusShortcut?: string | false;\n}\n\n/**\n * Shows the snackbars of a {@link SnackbarHostState} one at a time, in a polite live\n * region. Position it with `className` (e.g. `fixed inset-x-0 bottom-0`); a snackbar\n * expands upward from the bottom edge as it enters.\n *\n * - Snackbars without an action leave after 4s (`short`) or 10s (`long`); the timer pauses\n * while the pointer or focus is on the snackbar. On the web, use an auto-dismissing\n * snackbar only alongside inline feedback for the same event: it may be gone before\n * someone reads it. Snackbars with an action stay until acted on or dismissed.\n * - The live region is rendered empty first: a snackbar shown before the host mounts (one\n * at launch) appears once the region has been registered, so it's announced.\n * - `focusShortcut` (F6) moves focus to a snackbar with actions; Escape dismisses it.\n * When it leaves, focus returns to where it was before it entered the snackbar; if that\n * element is gone (an undone row was removed, say), to the control that followed it in\n * the Tab order, or else the one before it.\n *\n * @example\n * const snackbar = useSnackbarHostState();\n * <SnackbarHost state={snackbar} className=\"fixed inset-x-0 bottom-0\" />\n * snackbar.showSnackbar({ message: 'Saved' });\n */\nexport function SnackbarHost({\n state,\n snackbar,\n labels,\n focusShortcut = 'F6',\n className,\n ref,\n ...rest\n}: SnackbarHostProps) {\n const current = useSyncExternalStore(state.subscribe, state.getSnapshot, () => null);\n // Keep a leaving snackbar rendered while the next one fades in (Compose cross-fades).\n const [items, setItems] = useState<SnackbarData[]>([]);\n const [shown, setShown] = useState<SnackbarData | null>(null);\n if (current !== shown) {\n setShown(current);\n if (current && !items.includes(current)) setItems([...items, current]);\n }\n const rootRef = useObjectRef(ref);\n // Screen readers often skip content that arrives with its live region, so snackbars go in\n // only once the empty region has been on the page for a moment.\n const [regionReady, setRegionReady] = useState(false);\n useEffect(() => {\n const timer = setTimeout(() => setRegionReady(true), LIVE_REGION_DELAY);\n return () => clearTimeout(timer);\n }, []);\n\n // The shortcut focuses the shown snackbar's first control, if it has one.\n useEffect(() => {\n if (!focusShortcut || !current) return;\n const onKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.defaultPrevented || !matchesShortcut(event, focusShortcut)) return;\n const item = rootRef.current?.querySelector(`[data-snackbar-id=\"${current.id}\"]`);\n const target = item ? firstTabbable(item) : null;\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [focusShortcut, current, rootRef]);\n\n const styles = snackbarHostStyles();\n return (\n <div\n {...rest}\n ref={rootRef}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n className={styles.root({ class: className })}\n >\n {(regionReady ? items : []).map((data) => (\n <HostItem\n key={data.id}\n data={data}\n visible={data === current}\n shortcut={\n focusShortcut &&\n (data.visuals.actionLabel !== undefined || data.visuals.withDismissAction)\n ? focusShortcut\n : undefined\n }\n onExited={() => setItems((all) => all.filter((item) => item !== data))}\n className={styles.item()}\n >\n {snackbar ? (\n snackbar(data)\n ) : (\n <Snackbar\n actionLabel={data.visuals.actionLabel}\n onAction={data.performAction}\n onDismiss={data.visuals.withDismissAction ? data.dismiss : undefined}\n onEscape={data.dismiss}\n labels={labels}\n actionOnNewLine={data.visuals.actionOnNewLine}\n >\n {data.visuals.message}\n </Snackbar>\n )}\n </HostItem>\n ))}\n </div>\n );\n}\n\nfunction HostItem({\n data,\n visible,\n shortcut,\n onExited,\n className,\n children,\n}: {\n data: SnackbarData;\n visible: boolean;\n shortcut: string | undefined;\n onExited: () => void;\n className: string;\n children: React.ReactNode;\n}) {\n const { isPresent, isExiting, exitProps } = usePresence(visible);\n const [paused, setPaused] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n // Where focus was before it entered, and whether it's still inside: when the snackbar\n // leaves with focus in it, focus goes back there rather than falling to the page.\n const returnTo = useRef<FocusReturn | null>(null);\n const focusInside = useRef(false);\n\n useLayoutEffect(() => {\n if (visible || !focusInside.current) return;\n focusInside.current = false;\n const record = returnTo.current;\n returnTo.current = null;\n if (record) focusFirstAvailable(record);\n }, [visible]);\n\n useEffect(() => {\n if (!isPresent) onExited();\n }, [isPresent, onExited]);\n\n // Auto-dismiss after the duration, paused while hovered or focused within.\n useEffect(() => {\n const ms = durationMillis(data.visuals);\n if (!visible || paused || !Number.isFinite(ms)) return;\n const timer = setTimeout(data.dismiss, ms);\n return () => clearTimeout(timer);\n }, [data, visible, paused]);\n\n return (\n <div\n {...exitProps}\n ref={ref}\n data-exiting={isExiting || undefined}\n data-snackbar-id={data.id}\n aria-keyshortcuts={visible ? shortcut : undefined}\n inert={!visible}\n onPointerEnter={() => setPaused(true)}\n onPointerLeave={() => setPaused(ref.current?.contains(document.activeElement) ?? false)}\n onFocus={(event) => {\n const from = event.relatedTarget;\n if (!focusInside.current && !event.currentTarget.contains(from as Node | null)) {\n returnTo.current =\n from instanceof HTMLElement ? focusReturn(from, event.currentTarget) : null;\n }\n focusInside.current = true;\n setPaused(true);\n }}\n onBlur={(event) => {\n if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n // A snackbar that is leaving keeps the record, to hand focus back.\n if (visible) focusInside.current = false;\n setPaused(false);\n }}\n className={className}\n >\n <InSnackbarHost value>{children}</InSnackbarHost>\n </div>\n );\n}\n\n/** Where focus goes back to when a snackbar leaves, with fallbacks in Tab order. */\ninterface FocusReturn {\n target: HTMLElement;\n next: HTMLElement | null;\n previous: HTMLElement | null;\n}\n\n/**\n * Records `target` with its neighbours in the page's Tab order (leaving out the snackbar),\n * taken as focus enters the snackbar, so focus has somewhere sensible to go if `target` is\n * removed while the snackbar is open (APG: when the element that had focus is gone, move\n * focus to a logical place rather than letting it fall to the page).\n */\nfunction focusReturn(target: HTMLElement, snackbar: Element): FocusReturn {\n const order = tabbables(target.ownerDocument.body, (el) => snackbar.contains(el));\n const isOutside = (el: HTMLElement) => !el.contains(target) && !target.contains(el);\n const next =\n order.find(\n (el) =>\n isOutside(el) && target.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING,\n ) ?? null;\n const previous =\n order.findLast(\n (el) =>\n isOutside(el) && target.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING,\n ) ?? null;\n return { target, next, previous };\n}\n\nfunction focusFirstAvailable({ target, next, previous }: FocusReturn) {\n const available = [target, next, previous].find(\n (el) => el?.isConnected && !el.closest('[inert]') && !el.matches(':disabled'),\n );\n // The element may have scrolled out of view while the snackbar had focus; returning\n // focus never scrolls the page back to it (2026-10-15).\n available?.focus({ preventScroll: true });\n}\n"],"mappings":"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"}
|
|
@@ -2,6 +2,6 @@ import { ComponentTokenSet } from "../../tokens/component.js";
|
|
|
2
2
|
import "../../index.js";
|
|
3
3
|
//#region src/components/snackbar/snackbar-tokens.d.ts
|
|
4
4
|
/** The tokens of {@link Snackbar}: `--md-comp-snackbar-<part>`. */
|
|
5
|
-
export declare const snackbarTokenSet: ComponentTokenSet<"container-color" | "container-
|
|
5
|
+
export declare const snackbarTokenSet: ComponentTokenSet<"container-color" | "container-elevation" | "icon-color" | "container-shape" | "supporting-text-color" | "action-label-text-color">;
|
|
6
6
|
//#endregion
|
|
7
7
|
//# sourceMappingURL=snackbar-tokens.d.ts.map
|
|
@@ -37,7 +37,10 @@ interface SplitButtonOwnProps {
|
|
|
37
37
|
size?: SplitButtonSize;
|
|
38
38
|
/** Icon before the label, or on its own with `leadingLabel`. Hidden from assistive tech. */
|
|
39
39
|
leadingIcon?: ReactElement;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Words the split button announces, with English defaults. A {@link LabelsProvider} sets them for
|
|
42
|
+
* the whole app.
|
|
43
|
+
*/
|
|
41
44
|
labels?: Partial<SplitButtonLabels>;
|
|
42
45
|
/**
|
|
43
46
|
* The `<Menu>` the trailing button opens, 4px below it and aligned with its end edge
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
3
4
|
import { TriggerContext } from "../../primitives/TriggerContext.js";
|
|
4
5
|
import { ButtonBase } from "../../primitives/ButtonBase.js";
|
|
5
6
|
import { TouchTarget } from "../../primitives/TouchTarget.js";
|
|
@@ -37,8 +38,9 @@ const DEFAULT_LABELS = { menu: "More options" };
|
|
|
37
38
|
* </SplitButton>
|
|
38
39
|
*/
|
|
39
40
|
function SplitButton(props) {
|
|
40
|
-
const { variant = "filled", size = "sm", children, leadingIcon, leadingLabel, labels, menu, onTrailingPress, trailingRef, open, defaultOpen, onOpenChange, disabled, classNames, className, onPress, ...rest } = props;
|
|
41
|
+
const { variant = "filled", size = "sm", children, leadingIcon, leadingLabel, labels: labelsProp, menu, onTrailingPress, trailingRef, open, defaultOpen, onOpenChange, disabled, classNames, className, onPress, ...rest } = props;
|
|
41
42
|
const { href, target, rel, type, onPressStart, onPressEnd, ...root } = rest;
|
|
43
|
+
const labels = useLabels("SplitButton", DEFAULT_LABELS, labelsProp);
|
|
42
44
|
const outerTrigger = useContext(TriggerContext);
|
|
43
45
|
const [isOpen, setOpen] = useControlledState(open, defaultOpen ?? false, onOpenChange);
|
|
44
46
|
const styles = splitButtonStyles({
|
|
@@ -67,7 +69,7 @@ function SplitButton(props) {
|
|
|
67
69
|
};
|
|
68
70
|
const trailingProps = {
|
|
69
71
|
tokenAttributes: halfTokens,
|
|
70
|
-
label: labels
|
|
72
|
+
label: labels.menu,
|
|
71
73
|
disabled,
|
|
72
74
|
hasTouchTarget,
|
|
73
75
|
buttonRef: trailingRef,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SplitButton.js","names":[],"sources":["../../../src/components/split-button/SplitButton.tsx"],"sourcesContent":["'use client';\n\nimport {\n cloneElement,\n useContext,\n useMemo,\n type ComponentPropsWithRef,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport type { PressEvent } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { TriggerContext, type TriggerContextValue } from '../../primitives/TriggerContext';\nimport { cn } from '../../utils/cn';\nimport type { MenuProps } from '../menu/Menu';\nimport { MenuTrigger } from '../menu/menu-trigger';\nimport {\n splitButtonStyles,\n type SplitButtonSize,\n type SplitButtonVariant,\n} from './split-button-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Material Symbols \"keyboard_arrow_down\" (Apache-2.0). */\nfunction ArrowDownIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z\" />\n </svg>\n );\n}\n\nexport interface SplitButtonClassNames {\n /** The group holding both buttons; merged with `className`. */\n root?: string;\n /** The leading (main action) button. */\n leading?: string;\n /** Inner wrapper of the leading button, around its icon and label. */\n content?: string;\n /** The leading button's label text. */\n label?: string;\n /** The leading button's icon. */\n icon?: string;\n /** The trailing (menu) button. */\n trailing?: string;\n /** Inner wrapper of the trailing button, around its icon. */\n trailingContent?: string;\n /** The trailing button's chevron, which turns while the menu is open. */\n menuIcon?: string;\n}\n\n/** Words a {@link SplitButton} announces. */\nexport interface SplitButtonLabels {\n /**\n * The trailing button's name; name it for what the menu holds (\"More send options\").\n * @default \"More options\"\n */\n menu: string;\n}\n\nconst DEFAULT_LABELS: SplitButtonLabels = { menu: 'More options' };\n\ninterface SplitButtonOwnProps {\n /** The M3 style of both buttons: filled, tonal, outlined or elevated. @default \"filled\" */\n variant?: SplitButtonVariant;\n /** Height, padding and label size of both buttons, from extra small to extra large. @default \"sm\" */\n size?: SplitButtonSize;\n /** Icon before the label, or on its own with `leadingLabel`. Hidden from assistive tech. */\n leadingIcon?: ReactElement;\n /** Words the split button announces, with English defaults. */\n labels?: Partial<SplitButtonLabels>;\n /**\n * The `<Menu>` the trailing button opens, 4px below it and aligned with its end edge\n * unless the menu sets its own `placement` or `offset`. Leave it out to open something\n * else: the trailing button then takes an enclosing trigger (a `DialogTrigger`, a\n * `RichTooltipTrigger`, …), or reports presses through `onTrailingPress` and `onOpenChange`.\n */\n menu?: ReactElement<MenuProps>;\n /** Called when the trailing button is pressed. Without a menu or trigger it also toggles `open`. */\n onTrailingPress?: (event: PressEvent) => void;\n /** Ref to the trailing button, e.g. to anchor a custom overlay to it. */\n trailingRef?: Ref<HTMLButtonElement>;\n /** Whether the trailing button's menu or overlay is open (controlled). */\n open?: boolean;\n /** Whether the menu starts open (uncontrolled). @default false */\n defaultOpen?: boolean;\n /** Called with the new state when the menu or overlay opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** Disables both buttons. @default false */\n disabled?: boolean;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SplitButtonClassNames;\n}\n\n/** The leading button shows a label, an icon and a label, or an icon alone. */\ntype LeadingContent =\n | {\n /** The leading button's label. */\n children: ReactNode;\n leadingLabel?: undefined;\n }\n | {\n children?: undefined;\n leadingIcon: ReactElement;\n /** Accessible name of an icon-only leading button. */\n leadingLabel: string;\n };\n\n/** The leading button's own attributes; the rest of the DOM props go to the wrapper. */\ninterface LeadingActionProps extends Pick<\n ButtonBaseActionProps,\n 'onPress' | 'onPressStart' | 'onPressEnd'\n> {\n /** The leading button's `type`, e.g. `submit` to send the form it's in. @default \"button\" */\n type?: ButtonBaseActionProps['type'];\n}\n\ninterface LeadingLinkProps extends Pick<ButtonBaseLinkProps, 'onPress'> {\n /** Makes the leading button a link (`<a>`) to this URL. */\n href: string;\n /** The leading link's `target`, e.g. `_blank`. */\n target?: ButtonBaseLinkProps['target'];\n /** The leading link's `rel`, e.g. `noopener`. */\n rel?: ButtonBaseLinkProps['rel'];\n}\n\ntype LeadingProps = LeadingActionProps | LeadingLinkProps;\n\nexport type SplitButtonProps = SplitButtonOwnProps &\n LeadingContent &\n LeadingProps &\n Omit<\n ComponentPropsWithRef<'div'>,\n | keyof SplitButtonOwnProps\n | 'children'\n | 'leadingLabel'\n | 'onPress'\n | 'onPressStart'\n | 'onPressEnd'\n | 'type'\n >;\n\n/**\n * M3 Expressive split button: a leading action and a trailing button that opens a menu\n * of related actions (or another overlay). Five sizes (`xs`–`xl`) in the filled, tonal,\n * outlined and elevated styles. While the menu is open the trailing button becomes round\n * and its chevron turns over. The wrapper is a `group`; `className`, `style`, `data-*` and\n * `aria-*` go on it, and leading-button props (`onPress`, `href`, …) go to the leading button.\n *\n * @example\n * <SplitButton\n * leadingIcon={<SendIcon />}\n * onPress={send}\n * labels={{ menu: 'More send options' }}\n * menu={<Menu onAction={schedule}><MenuItem key=\"later\">Send later</MenuItem></Menu>}\n * >\n * Send\n * </SplitButton>\n */\nexport function SplitButton(props: SplitButtonProps) {\n const {\n variant = 'filled',\n size = 'sm',\n children,\n leadingIcon,\n leadingLabel,\n labels,\n menu,\n onTrailingPress,\n trailingRef,\n open,\n defaultOpen,\n onOpenChange,\n disabled,\n classNames,\n className,\n onPress,\n ...rest\n } = props;\n const { href, target, rel, type, onPressStart, onPressEnd, ...root } = rest as typeof rest & {\n href?: string;\n target?: string;\n rel?: string;\n type?: ButtonBaseActionProps['type'];\n onPressStart?: ButtonBaseActionProps['onPressStart'];\n onPressEnd?: ButtonBaseActionProps['onPressEnd'];\n };\n const outerTrigger = useContext(TriggerContext);\n const [isOpen, setOpen] = useControlledState(open, defaultOpen ?? false, onOpenChange);\n const styles = splitButtonStyles({ variant, size });\n const hasTouchTarget = size === 'xs' || size === 'sm';\n // The halves are buttons: they read Button's colour tokens (button-tokens.ts).\n const halfTokens = { 'data-tokens': 'md-comp-button', 'data-variant': variant };\n const leadingAttributes = { ...halfTokens, 'data-slot': 'leading' };\n const leadingProps = {\n ...leadingAttributes,\n disabled,\n onPress,\n onPressStart,\n onPressEnd,\n 'aria-label': leadingLabel,\n className: styles.leading({ class: classNames?.leading }),\n ...(href !== undefined ? { href, target, rel } : { type }),\n } as ButtonBaseActionProps | ButtonBaseLinkProps;\n\n const trailingProps = {\n tokenAttributes: halfTokens,\n label: labels?.menu ?? DEFAULT_LABELS.menu,\n disabled,\n hasTouchTarget,\n buttonRef: trailingRef,\n className: styles.trailing({ class: classNames?.trailing }),\n contentClassName: styles.trailingContent({ class: classNames?.trailingContent }),\n iconClassName: styles.menuIcon({ class: classNames?.menuIcon }),\n };\n\n let trailing: ReactNode;\n if (menu) {\n trailing = (\n <MenuTrigger open={isOpen} onOpenChange={setOpen}>\n <OpenOnMouseRelease>\n <TrailingButton {...trailingProps} open={isOpen} onPress={onTrailingPress} />\n </OpenOnMouseRelease>\n {cloneElement(menu, {\n placement: menu.props.placement ?? 'bottom-end',\n offset: menu.props.offset ?? 4,\n // Aligned with the trailing button even against the window's edge.\n containerPadding: menu.props.containerPadding ?? 0,\n })}\n </MenuTrigger>\n );\n } else if (outerTrigger) {\n // An enclosing trigger (dialog, sheet, rich tooltip) opens its overlay from here.\n trailing = (\n <TrailingButton\n {...trailingProps}\n open={outerTrigger['aria-expanded'] === true}\n onPress={onTrailingPress}\n />\n );\n } else {\n trailing = (\n <TrailingButton\n {...trailingProps}\n open={isOpen}\n aria-expanded={isOpen}\n onPress={(event) => {\n setOpen(!isOpen);\n onTrailingPress?.(event);\n }}\n />\n );\n }\n\n return (\n <div\n // A group of its two buttons, so assistive tech reads them together and an app may name\n // it (`aria-label`, which ARIA prohibits on a generic `div`).\n role=\"group\"\n {...root}\n // The component tokens' hooks (split-button-tokens.ts); `data-variant` and `data-size`\n // are also public styling hooks for apps.\n data-tokens={mergeDataTokens('md-comp-split-button', root)}\n data-variant={variant}\n data-size={size}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n {/* The leading button must not pick up the trailing button's trigger props. */}\n <TriggerContext value={null}>\n <ButtonBase {...leadingProps}>\n <span className={styles.content({ class: classNames?.content })}>\n {hasTouchTarget && <TouchTarget />}\n {leadingIcon && (\n <span aria-hidden=\"true\" className={styles.icon({ class: classNames?.icon })}>\n {leadingIcon}\n </span>\n )}\n {children != null && (\n <span className={styles.label({ class: classNames?.label })}>{children}</span>\n )}\n </span>\n </ButtonBase>\n </TriggerContext>\n {trailing}\n </div>\n );\n}\n\n/**\n * A menu trigger opens on pointer down for a mouse (React Aria, like native menus). The\n * split button opens it on release instead, so the trailing button's pressed shape shows\n * while it's held, as the spec draws it (\"Pressed\" before \"Selected\"; 2026-10-13). A\n * release outside the button doesn't open it. Keyboard and touch are unchanged.\n */\nfunction OpenOnMouseRelease({ children }: { children: ReactNode }) {\n const trigger = useContext(TriggerContext);\n const value = useMemo<TriggerContextValue | null>(() => {\n if (!trigger) return null;\n const { onPressStart, onPress } = trigger;\n return {\n ...trigger,\n onPressStart: (event) => {\n if (event.pointerType !== 'mouse') onPressStart?.(event);\n },\n onPress: (event) => {\n if (event.pointerType === 'mouse') {\n // Focus the trigger first, so closing the menu returns focus to it.\n (event.target as Element).closest<HTMLElement>('button')?.focus({ preventScroll: true });\n onPressStart?.(event);\n }\n onPress?.(event);\n },\n };\n }, [trigger]);\n return <TriggerContext value={value}>{children}</TriggerContext>;\n}\n\ninterface TrailingButtonProps {\n /** Button's token hooks: `data-tokens` and `data-variant`. */\n tokenAttributes: { 'data-tokens': string; 'data-variant': string };\n label: string;\n open: boolean;\n disabled?: boolean;\n hasTouchTarget: boolean;\n buttonRef?: Ref<HTMLButtonElement>;\n onPress?: (event: PressEvent) => void;\n 'aria-expanded'?: boolean;\n className: string;\n contentClassName: string;\n iconClassName: string;\n}\n\n/**\n * The trailing button. An enclosing trigger (the menu's or another overlay's) adds its\n * press handler and `aria-expanded` through `TriggerContext`.\n */\nfunction TrailingButton({\n tokenAttributes,\n label,\n open,\n disabled,\n hasTouchTarget,\n buttonRef,\n onPress,\n 'aria-expanded': ariaExpanded,\n className,\n contentClassName,\n iconClassName,\n}: TrailingButtonProps) {\n return (\n <ButtonBase\n {...tokenAttributes}\n data-slot=\"trailing\"\n ref={buttonRef}\n aria-label={label}\n aria-expanded={ariaExpanded}\n data-open={open || undefined}\n disabled={disabled}\n onPress={onPress}\n className={className}\n >\n <span className={contentClassName}>\n {hasTouchTarget && <TouchTarget />}\n {/* M3: the trailing button always shows this icon, since it turns over when open. */}\n <span aria-hidden=\"true\" data-motion=\"standard\" className={iconClassName}>\n <ArrowDownIcon />\n </span>\n </span>\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SplitButton.js","names":[],"sources":["../../../src/components/split-button/SplitButton.tsx"],"sourcesContent":["'use client';\n\nimport {\n cloneElement,\n useContext,\n useMemo,\n type ComponentPropsWithRef,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport type { PressEvent } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { useLabels } from '../labels/use-labels';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { TriggerContext, type TriggerContextValue } from '../../primitives/TriggerContext';\nimport { cn } from '../../utils/cn';\nimport type { MenuProps } from '../menu/Menu';\nimport { MenuTrigger } from '../menu/menu-trigger';\nimport {\n splitButtonStyles,\n type SplitButtonSize,\n type SplitButtonVariant,\n} from './split-button-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Material Symbols \"keyboard_arrow_down\" (Apache-2.0). */\nfunction ArrowDownIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z\" />\n </svg>\n );\n}\n\nexport interface SplitButtonClassNames {\n /** The group holding both buttons; merged with `className`. */\n root?: string;\n /** The leading (main action) button. */\n leading?: string;\n /** Inner wrapper of the leading button, around its icon and label. */\n content?: string;\n /** The leading button's label text. */\n label?: string;\n /** The leading button's icon. */\n icon?: string;\n /** The trailing (menu) button. */\n trailing?: string;\n /** Inner wrapper of the trailing button, around its icon. */\n trailingContent?: string;\n /** The trailing button's chevron, which turns while the menu is open. */\n menuIcon?: string;\n}\n\n/** Words a {@link SplitButton} announces. */\nexport interface SplitButtonLabels {\n /**\n * The trailing button's name; name it for what the menu holds (\"More send options\").\n * @default \"More options\"\n */\n menu: string;\n}\n\nconst DEFAULT_LABELS: SplitButtonLabels = { menu: 'More options' };\n\ninterface SplitButtonOwnProps {\n /** The M3 style of both buttons: filled, tonal, outlined or elevated. @default \"filled\" */\n variant?: SplitButtonVariant;\n /** Height, padding and label size of both buttons, from extra small to extra large. @default \"sm\" */\n size?: SplitButtonSize;\n /** Icon before the label, or on its own with `leadingLabel`. Hidden from assistive tech. */\n leadingIcon?: ReactElement;\n /**\n * Words the split button announces, with English defaults. A {@link LabelsProvider} sets them for\n * the whole app.\n */\n labels?: Partial<SplitButtonLabels>;\n /**\n * The `<Menu>` the trailing button opens, 4px below it and aligned with its end edge\n * unless the menu sets its own `placement` or `offset`. Leave it out to open something\n * else: the trailing button then takes an enclosing trigger (a `DialogTrigger`, a\n * `RichTooltipTrigger`, …), or reports presses through `onTrailingPress` and `onOpenChange`.\n */\n menu?: ReactElement<MenuProps>;\n /** Called when the trailing button is pressed. Without a menu or trigger it also toggles `open`. */\n onTrailingPress?: (event: PressEvent) => void;\n /** Ref to the trailing button, e.g. to anchor a custom overlay to it. */\n trailingRef?: Ref<HTMLButtonElement>;\n /** Whether the trailing button's menu or overlay is open (controlled). */\n open?: boolean;\n /** Whether the menu starts open (uncontrolled). @default false */\n defaultOpen?: boolean;\n /** Called with the new state when the menu or overlay opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** Disables both buttons. @default false */\n disabled?: boolean;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SplitButtonClassNames;\n}\n\n/** The leading button shows a label, an icon and a label, or an icon alone. */\ntype LeadingContent =\n | {\n /** The leading button's label. */\n children: ReactNode;\n leadingLabel?: undefined;\n }\n | {\n children?: undefined;\n leadingIcon: ReactElement;\n /** Accessible name of an icon-only leading button. */\n leadingLabel: string;\n };\n\n/** The leading button's own attributes; the rest of the DOM props go to the wrapper. */\ninterface LeadingActionProps extends Pick<\n ButtonBaseActionProps,\n 'onPress' | 'onPressStart' | 'onPressEnd'\n> {\n /** The leading button's `type`, e.g. `submit` to send the form it's in. @default \"button\" */\n type?: ButtonBaseActionProps['type'];\n}\n\ninterface LeadingLinkProps extends Pick<ButtonBaseLinkProps, 'onPress'> {\n /** Makes the leading button a link (`<a>`) to this URL. */\n href: string;\n /** The leading link's `target`, e.g. `_blank`. */\n target?: ButtonBaseLinkProps['target'];\n /** The leading link's `rel`, e.g. `noopener`. */\n rel?: ButtonBaseLinkProps['rel'];\n}\n\ntype LeadingProps = LeadingActionProps | LeadingLinkProps;\n\nexport type SplitButtonProps = SplitButtonOwnProps &\n LeadingContent &\n LeadingProps &\n Omit<\n ComponentPropsWithRef<'div'>,\n | keyof SplitButtonOwnProps\n | 'children'\n | 'leadingLabel'\n | 'onPress'\n | 'onPressStart'\n | 'onPressEnd'\n | 'type'\n >;\n\n/**\n * M3 Expressive split button: a leading action and a trailing button that opens a menu\n * of related actions (or another overlay). Five sizes (`xs`–`xl`) in the filled, tonal,\n * outlined and elevated styles. While the menu is open the trailing button becomes round\n * and its chevron turns over. The wrapper is a `group`; `className`, `style`, `data-*` and\n * `aria-*` go on it, and leading-button props (`onPress`, `href`, …) go to the leading button.\n *\n * @example\n * <SplitButton\n * leadingIcon={<SendIcon />}\n * onPress={send}\n * labels={{ menu: 'More send options' }}\n * menu={<Menu onAction={schedule}><MenuItem key=\"later\">Send later</MenuItem></Menu>}\n * >\n * Send\n * </SplitButton>\n */\nexport function SplitButton(props: SplitButtonProps) {\n const {\n variant = 'filled',\n size = 'sm',\n children,\n leadingIcon,\n leadingLabel,\n labels: labelsProp,\n menu,\n onTrailingPress,\n trailingRef,\n open,\n defaultOpen,\n onOpenChange,\n disabled,\n classNames,\n className,\n onPress,\n ...rest\n } = props;\n const { href, target, rel, type, onPressStart, onPressEnd, ...root } = rest as typeof rest & {\n href?: string;\n target?: string;\n rel?: string;\n type?: ButtonBaseActionProps['type'];\n onPressStart?: ButtonBaseActionProps['onPressStart'];\n onPressEnd?: ButtonBaseActionProps['onPressEnd'];\n };\n const labels = useLabels('SplitButton', DEFAULT_LABELS, labelsProp);\n const outerTrigger = useContext(TriggerContext);\n const [isOpen, setOpen] = useControlledState(open, defaultOpen ?? false, onOpenChange);\n const styles = splitButtonStyles({ variant, size });\n const hasTouchTarget = size === 'xs' || size === 'sm';\n // The halves are buttons: they read Button's colour tokens (button-tokens.ts).\n const halfTokens = { 'data-tokens': 'md-comp-button', 'data-variant': variant };\n const leadingAttributes = { ...halfTokens, 'data-slot': 'leading' };\n const leadingProps = {\n ...leadingAttributes,\n disabled,\n onPress,\n onPressStart,\n onPressEnd,\n 'aria-label': leadingLabel,\n className: styles.leading({ class: classNames?.leading }),\n ...(href !== undefined ? { href, target, rel } : { type }),\n } as ButtonBaseActionProps | ButtonBaseLinkProps;\n\n const trailingProps = {\n tokenAttributes: halfTokens,\n label: labels.menu,\n disabled,\n hasTouchTarget,\n buttonRef: trailingRef,\n className: styles.trailing({ class: classNames?.trailing }),\n contentClassName: styles.trailingContent({ class: classNames?.trailingContent }),\n iconClassName: styles.menuIcon({ class: classNames?.menuIcon }),\n };\n\n let trailing: ReactNode;\n if (menu) {\n trailing = (\n <MenuTrigger open={isOpen} onOpenChange={setOpen}>\n <OpenOnMouseRelease>\n <TrailingButton {...trailingProps} open={isOpen} onPress={onTrailingPress} />\n </OpenOnMouseRelease>\n {cloneElement(menu, {\n placement: menu.props.placement ?? 'bottom-end',\n offset: menu.props.offset ?? 4,\n // Aligned with the trailing button even against the window's edge.\n containerPadding: menu.props.containerPadding ?? 0,\n })}\n </MenuTrigger>\n );\n } else if (outerTrigger) {\n // An enclosing trigger (dialog, sheet, rich tooltip) opens its overlay from here.\n trailing = (\n <TrailingButton\n {...trailingProps}\n open={outerTrigger['aria-expanded'] === true}\n onPress={onTrailingPress}\n />\n );\n } else {\n trailing = (\n <TrailingButton\n {...trailingProps}\n open={isOpen}\n aria-expanded={isOpen}\n onPress={(event) => {\n setOpen(!isOpen);\n onTrailingPress?.(event);\n }}\n />\n );\n }\n\n return (\n <div\n // A group of its two buttons, so assistive tech reads them together and an app may name\n // it (`aria-label`, which ARIA prohibits on a generic `div`).\n role=\"group\"\n {...root}\n // The component tokens' hooks (split-button-tokens.ts); `data-variant` and `data-size`\n // are also public styling hooks for apps.\n data-tokens={mergeDataTokens('md-comp-split-button', root)}\n data-variant={variant}\n data-size={size}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n {/* The leading button must not pick up the trailing button's trigger props. */}\n <TriggerContext value={null}>\n <ButtonBase {...leadingProps}>\n <span className={styles.content({ class: classNames?.content })}>\n {hasTouchTarget && <TouchTarget />}\n {leadingIcon && (\n <span aria-hidden=\"true\" className={styles.icon({ class: classNames?.icon })}>\n {leadingIcon}\n </span>\n )}\n {children != null && (\n <span className={styles.label({ class: classNames?.label })}>{children}</span>\n )}\n </span>\n </ButtonBase>\n </TriggerContext>\n {trailing}\n </div>\n );\n}\n\n/**\n * A menu trigger opens on pointer down for a mouse (React Aria, like native menus). The\n * split button opens it on release instead, so the trailing button's pressed shape shows\n * while it's held, as the spec draws it (\"Pressed\" before \"Selected\"; 2026-10-13). A\n * release outside the button doesn't open it. Keyboard and touch are unchanged.\n */\nfunction OpenOnMouseRelease({ children }: { children: ReactNode }) {\n const trigger = useContext(TriggerContext);\n const value = useMemo<TriggerContextValue | null>(() => {\n if (!trigger) return null;\n const { onPressStart, onPress } = trigger;\n return {\n ...trigger,\n onPressStart: (event) => {\n if (event.pointerType !== 'mouse') onPressStart?.(event);\n },\n onPress: (event) => {\n if (event.pointerType === 'mouse') {\n // Focus the trigger first, so closing the menu returns focus to it.\n (event.target as Element).closest<HTMLElement>('button')?.focus({ preventScroll: true });\n onPressStart?.(event);\n }\n onPress?.(event);\n },\n };\n }, [trigger]);\n return <TriggerContext value={value}>{children}</TriggerContext>;\n}\n\ninterface TrailingButtonProps {\n /** Button's token hooks: `data-tokens` and `data-variant`. */\n tokenAttributes: { 'data-tokens': string; 'data-variant': string };\n label: string;\n open: boolean;\n disabled?: boolean;\n hasTouchTarget: boolean;\n buttonRef?: Ref<HTMLButtonElement>;\n onPress?: (event: PressEvent) => void;\n 'aria-expanded'?: boolean;\n className: string;\n contentClassName: string;\n iconClassName: string;\n}\n\n/**\n * The trailing button. An enclosing trigger (the menu's or another overlay's) adds its\n * press handler and `aria-expanded` through `TriggerContext`.\n */\nfunction TrailingButton({\n tokenAttributes,\n label,\n open,\n disabled,\n hasTouchTarget,\n buttonRef,\n onPress,\n 'aria-expanded': ariaExpanded,\n className,\n contentClassName,\n iconClassName,\n}: TrailingButtonProps) {\n return (\n <ButtonBase\n {...tokenAttributes}\n data-slot=\"trailing\"\n ref={buttonRef}\n aria-label={label}\n aria-expanded={ariaExpanded}\n data-open={open || undefined}\n disabled={disabled}\n onPress={onPress}\n className={className}\n >\n <span className={contentClassName}>\n {hasTouchTarget && <TouchTarget />}\n {/* M3: the trailing button always shows this icon, since it turns over when open. */}\n <span aria-hidden=\"true\" data-motion=\"standard\" className={iconClassName}>\n <ArrowDownIcon />\n </span>\n </span>\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
@@ -6,8 +6,8 @@ export declare const switchStyles: import("tailwind-variants").TVReturnType<{
|
|
|
6
6
|
[key: string]: import("tailwind-variants").ClassValue | {
|
|
7
7
|
control?: import("tailwind-variants").ClassValue;
|
|
8
8
|
icon?: import("tailwind-variants").ClassValue;
|
|
9
|
-
stateLayer?: import("tailwind-variants").ClassValue;
|
|
10
9
|
thumb?: import("tailwind-variants").ClassValue;
|
|
10
|
+
stateLayer?: import("tailwind-variants").ClassValue;
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
13
|
} | {
|
|
@@ -15,14 +15,14 @@ export declare const switchStyles: import("tailwind-variants").TVReturnType<{
|
|
|
15
15
|
circle: import("tailwind-variants").ClassValue | {
|
|
16
16
|
control?: import("tailwind-variants").ClassValue;
|
|
17
17
|
icon?: import("tailwind-variants").ClassValue;
|
|
18
|
-
stateLayer?: import("tailwind-variants").ClassValue;
|
|
19
18
|
thumb?: import("tailwind-variants").ClassValue;
|
|
19
|
+
stateLayer?: import("tailwind-variants").ClassValue;
|
|
20
20
|
};
|
|
21
21
|
track: import("tailwind-variants").ClassValue | {
|
|
22
22
|
control?: import("tailwind-variants").ClassValue;
|
|
23
23
|
icon?: import("tailwind-variants").ClassValue;
|
|
24
|
-
stateLayer?: import("tailwind-variants").ClassValue;
|
|
25
24
|
thumb?: import("tailwind-variants").ClassValue;
|
|
25
|
+
stateLayer?: import("tailwind-variants").ClassValue;
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
28
|
}, {
|
|
@@ -131,7 +131,10 @@ interface TextFieldOwnProps extends Omit<AriaTextFieldProps<HTMLInputElement>, R
|
|
|
131
131
|
maxRows?: number;
|
|
132
132
|
/** Shows `count/maxLength` below the field. Defaults to `true` when `maxLength` is set. */
|
|
133
133
|
counter?: boolean;
|
|
134
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Words the field announces, for other languages. A {@link LabelsProvider} sets them for the
|
|
136
|
+
* whole app.
|
|
137
|
+
*/
|
|
135
138
|
labels?: Partial<TextFieldLabels>;
|
|
136
139
|
/** Classes for the root element, merged with `classNames.root`; consumer classes win. */
|
|
137
140
|
className?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { densityAttribute } from "../../tokens/density.js";
|
|
3
3
|
import { cn } from "../../utils/cn.js";
|
|
4
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
4
5
|
import { mergeDataTokens } from "../../utils/merge-data-tokens.js";
|
|
5
6
|
import { pickWidgetAria, splitRootProps } from "../../utils/root-props.js";
|
|
6
7
|
import { CharacterCounter, ErrorText, FieldErrorIcon, FieldLabel, SupportingText } from "../../primitives/Field.js";
|
|
@@ -39,10 +40,7 @@ const INTERACTIVE = "input, textarea, button, a, [role=\"button\"]";
|
|
|
39
40
|
function TextField(props) {
|
|
40
41
|
const { variant = "filled", density, label, value: valueProp, defaultValue, onChange, supportingText, supportingTextOnFocus = false, errorMessage, invalid, required, disabled = false, readOnly, leadingIcon, trailingIcon, errorIcon, prefix, prefixLabel, suffix, suffixLabel, multiline = false, autoGrow = false, rows = autoGrow ? 1 : 4, maxRows, counter, labels: labelsProp, className, classNames, style, ref, inputRef, ...otherProps } = props;
|
|
41
42
|
const { root: rootData, rest: ariaProps } = splitRootProps(otherProps, { wrapper: true });
|
|
42
|
-
const labels =
|
|
43
|
-
...DEFAULT_LABELS,
|
|
44
|
-
...labelsProp
|
|
45
|
-
};
|
|
43
|
+
const labels = useLabels("TextField", DEFAULT_LABELS, labelsProp);
|
|
46
44
|
const prefixId = useId();
|
|
47
45
|
const suffixId = useId();
|
|
48
46
|
const counterId = useId();
|
|
@@ -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\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 const fit = () => {\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 };\n if (element.getClientRects().length > 0 || typeof ResizeObserver === 'undefined') {\n fit();\n return;\n }\n // Out of sight (`display: none` above it: a closed tab or panel), it has no height to read,\n // and set to its 0px it stayed invisible once shown. It keeps its rows until it shows.\n element.style.height = '';\n const observer = new ResizeObserver(() => {\n if (element.getClientRects().length === 0) return;\n observer.disconnect();\n fit();\n });\n observer.observe(element);\n return () => observer.disconnect();\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":";;;;;;;;;;;;;AA8EA,MAAM,iBAAkC;CACtC,OAAO;CACP,iBAAiB,OAAO,QAAQ,oBAAoB,MAAM,MAAM;AAClE;;AAGA,MAAM,uCAAuB,IAAI,IAAI;CAAC;CAAQ;CAAQ;CAAkB;CAAS;AAAM,CAAC;AA0JxF,MAAM,cAAc;;;;;;;;;;;;;AAcpB,SAAgB,UAAU,OAAuB;CAC/C,MAAM,EACJ,UAAU,UACV,SACA,OACA,OAAO,WACP,cACA,UACA,gBACA,wBAAwB,OACxB,cACA,SACA,UACA,WAAW,OACX,UACA,aACA,cACA,WACA,QACA,aACA,QACA,aACA,YAAY,OACZ,WAAW,OACX,OAAO,WAAW,IAAI,GACtB,SACA,SACA,QAAQ,YACR,WACA,YACA,OACA,KACA,UACA,GAAG,eACD;CAKJ,MAAM,EAAE,MAAM,UAAU,MAAM,cAAc,eAAe,YAAY,EAAE,SAAS,KAAK,CAAC;CACxF,MAAM,SAAS;EAAE,GAAG;EAAgB,GAAG;CAAW;CAClD,MAAM,WAAW,MAAM;CACvB,MAAM,WAAW,MAAM;CACvB,MAAM,YAAY,MAAM;CAExB,MAAM,CAAC,OAAO,YAAY,mBAAmB,WAAW,gBAAgB,IAAI,QAAQ;CACpF,MAAM,cAAc,aAAa,QAAiC;CAClE,MAAM,EACJ,YACA,YACA,kBACA,mBACA,WACA,kBACA,sBACE,aACF;EACE,GAAG;EACH;EACA,aAAa;EACb;EACA;EACA,UAAU;EACV,WAAW;EACX,YAAY;EACZ,YAAY;EACZ,YAAY;EACZ,kBAAkB,YAAY,aAAa;CAC7C,GACA,WACF;CACA,MAAM,EAAE,YAAY,cAAc,aAAa,EAAE,aAAa,KAAK,CAAC;CACpE,MAAM,EAAE,YAAY,cAAc,SAAS,EAAE,YAAY,SAAS,CAAC;CAEnE,MAAM,UAAU,aAAa;CAE7B,sBAAsB;EACpB,MAAM,UAAU,YAAY;EAC5B,IAAI,CAAC,WAAW,CAAC,SAAS;EAC1B,MAAM,YAAY;GAChB,QAAQ,MAAM,SAAS;GACvB,MAAM,aAAa,OAAO,WAAW,iBAAiB,OAAO,CAAC,CAAC,UAAU,KAAK;GAC9E,MAAM,MAAM,UAAU,aAAa,UAAU,OAAO;GACpD,QAAQ,MAAM,SAAS,GAAG,KAAK,IAAI,QAAQ,cAAc,GAAG,EAAE;GAC9D,QAAQ,MAAM,YAAY,QAAQ,eAAe,MAAM,SAAS;EAClE;EACA,IAAI,QAAQ,eAAe,CAAC,CAAC,SAAS,KAAK,OAAO,mBAAmB,aAAa;GAChF,IAAI;GACJ;EACF;EAGA,QAAQ,MAAM,SAAS;EACvB,MAAM,WAAW,IAAI,qBAAqB;GACxC,IAAI,QAAQ,eAAe,CAAC,CAAC,WAAW,GAAG;GAC3C,SAAS,WAAW;GACpB,IAAI;EACN,CAAC;EACD,SAAS,QAAQ,OAAO;EACxB,aAAa,SAAS,WAAW;CACnC,GAAG;EAAC;EAAO;EAAS;EAAS;CAAW,CAAC;CAEzC,MAAM,WAAW,UAAU,KAAA,KAAa,UAAU;CAClD,MAAM,UACJ,aAAa,aAAa,MAAM,SAAS,KAAK,qBAAqB,IAAI,UAAU,QAAQ,EAAE;CAC7F,MAAM,QAAoB,WACtB,aACA,YACE,YACE,gBACA,YACE,gBACA,UACJ,YACE,UACA,YACE,UACA;CAEV,MAAM,gBACJ,OAAO,iBAAiB,aACpB,aAAa;EAAE;EAAW;EAAkB;CAAkB,CAAC,IAC9D,gBAAgB,iBAAiB,KAAK,GAAG;CAChD,MAAM,YAAY,aAAa,QAAQ,aAAa;CACpD,MAAM,YAAY,UAAU;CAC5B,MAAM,cAAc,cAAc,KAAA,KAAa,YAAY;CAC3D,MAAM,gBAAgB,aAAa,CAAC,gBAAgB,cAAc;CAElE,MAAM,SAAS,gBAAgB;EAC7B;EACA;EACA,gBAAgB,QAAQ,WAAW;EACnC,iBAAiB,QAAQ,YAAY,KAAK;EAC1C;EACA,aAAa,aAAa,CAAC;EAC3B;CACF,CAAC;CAED,MAAM,cACJ;EACE,SAAS,WAAW;EACpB,SAAS,WAAW;EACpB,oBAAoB,WAAW,qBAAqB,CAClD,CAAC,kBAAkB,iBAAiB,IACpC,CAAC,aAAa,kBAAkB,EAClC,CAAC;EACD,cAAc,YAAY;CAC5B,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG,KAAK,KAAA;CAGlB,MAAM,cAAc,UAAwC;EAC1D,IAAK,MAAM,OAAmB,QAAQ,WAAW,KAAK,UAAU;EAChE,MAAM,eAAe;EAErB,YAAY,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;CACpD;CAKA,MAAM,qBAAqB,UAAsC;EAC/D,MAAM,SAAS,MAAM;EACrB,IACG,MAAM,YAAwC,gBAAgB,WAC/D,CAAC,MAAM,cAAc,SAAS,MAAM,KACpC,OAAO,QAAQ,WAAW,KAC1B,YACA,SAAS,kBAAkB,YAAY,SAEvC;EAEF,YAAY,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;CACpD;CAIA,MAAM,eAAe,WAAW,OAAO;CACvC,MAAM,QAAQ,YAAY,aAAa;CAEvC,OACE,qBAAC,OAAD;EACE,GAAI;EACC;EACE;EACP,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;EACjE,eAAa,gBAAgB,sBAAsB,QAAQ;EAC3D,gBAAc;EACd,GAAI,iBAAiB,OAAO;EAC5B,oBAAkB;EAClB,gBAAc,aAAa,KAAA;EAC3B,gBAAe,aAAa,CAAC,YAAa,KAAA;EAC1C,gBAAc,aAAa,KAAA;EAC3B,iBAAe,YAAY,KAAA;EAC3B,iBAAe,YAAY,KAAA;EAC3B,gBAAc,WAAW,KAAA;EAd3B,UAAA,CAgBE,qBAAC,OAAD;GACE,GAAI;GACJ,eAAe;GACf,SAAS;GACT,aAAU;GACV,WAAW,OAAO,UAAU,EAAE,OAAO,YAAY,UAAU,CAAC;GAL9D,UAAA;IAOG,YAAY,cACX,oBAAC,YAAD;KACE,eAAY;KACZ,aAAU;KACV,WAAW,OAAO,QAAQ,EAAE,OAAO,YAAY,QAAQ,CAAC;KAEvD,UAAA,YACC,oBAAC,UAAD;MAAQ,WAAW,OAAO,MAAM;MAC9B,UAAA,oBAAC,QAAD;OAAM,WAAW,OAAO,UAAU;OAChC,UAAA,qBAAC,QAAD;QAAM,WAAW,OAAO,UAAU;QAAlC,UAAA,CACG,OACA,YACG;;MACF,CAAA;KACA,CAAA;IAEF,CAAA;IAEX,eACC,oBAAC,QAAD;KACE,eAAY;KACZ,aAAU;KACV,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,OAAO,YAAY,GAAG,YAAY,WAAW,EAAE,CAAC;KAElF,UAAA;IACG,CAAA;IAER,qBAAC,OAAD;KAAK,WAAW,OAAO,MAAM;KAA7B,UAAA;MACG,UACC,oBAAC,QAAD;OACE,IAAI;OACJ,WAAW,OAAO,MAAM,EAAE,OAAO,GAAG,OAAO,OAAO,GAAG,YAAY,MAAM,EAAE,CAAC;OAE1E,UAAA,oBAAC,OAAD;QAAO,MAAM;QAAQ,OAAO;OAAc,CAAA;MACtC,CAAA;MAER,oBAAC,OAAD;OACE,GAAK,WAAW,YAAY,eAAe,SAAS,GAAG,UAAU;OACjE,oBAAkB;OAClB,KAAK;OACL,aAAU;OACV,MAAM,YAAY,OAAO,KAAA;OACzB,WAAW,OAAO,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;MACrD,CAAA;MACA,UACC,oBAAC,QAAD;OACE,IAAI;OACJ,WAAW,OAAO,MAAM,EAAE,OAAO,GAAG,OAAO,OAAO,GAAG,YAAY,MAAM,EAAE,CAAC;OAE1E,UAAA,oBAAC,OAAD;QAAO,MAAM;QAAQ,OAAO;OAAc,CAAA;MACtC,CAAA;KAEL;;IACJ,eACC,oBAAC,QAAD;KACE,aAAU;KACV,WAAW,OAAO,KAAK,EACrB,OAAO,GAAG,OAAO,aAAa,GAAG,YAAY,YAAY,EAC3D,CAAC;KAEA,UAAA;IACG,CAAA,IACJ,gBACF,oBAAC,QAAD;KAAM,WAAW,OAAO,KAAK,EAAE,OAAO,OAAO,aAAa,EAAE,CAAC;KAC3D,UAAA,oBAAC,gBAAD;MACE,OAAO,OAAO;MACd,MAAM;MACN,WAAW,OAAO,UAAU,EAAE,OAAO,YAAY,UAAU,CAAC;KAC7D,CAAA;IACG,CAAA,IACJ;IACH,YACC,qBAAC,YAAD;KACE,GAAI;KACJ,aAAU;KACV,WAAW,OAAO,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;KAHtD,UAAA,CAKG,OACA,YACS;;IAEd,oBAAC,QAAD;KACE,eAAY;KACZ,aAAU;KACV,WAAW,OAAO,UAAU,EAAE,OAAO,YAAY,gBAAgB,CAAC;IACnE,CAAA;GACE;EACH,CAAA,IAAA,kBAAkB,aAAa,gBAC/B,qBAAC,OAAD;GAAK,WAAW,OAAO,WAAW;GAAlC,UAAA;IACG,kBACC,oBAAC,gBAAD;KACE,GAAI;KACJ,aAAU;KACV,WAAW,GACT,OAAO,eAAe,EACpB,OAAO,GAAG,OAAO,oBAAoB,GAAG,YAAY,cAAc,EACpE,CAAC,GACD,aAAa,SACf;KAEC,UAAA;IACa,CAAA;IAEjB,aACC,oBAAC,WAAD;KACE,GAAI;KACJ,WAAW,OAAO,eAAe;MAC/B,uBAAuB;MACvB,OAAO,YAAY;KACrB,CAAC;KAEA,UAAA;IACQ,CAAA;IAEZ,eACC,oBAAC,kBAAD;KACE,IAAI;KACJ,OAAO,MAAM;KACb,KAAK;KACL,OAAO,OAAO,eAAe,MAAM,QAAQ,SAAS;KACpD,WAAW,OAAO,QAAQ,EAAE,OAAO,YAAY,QAAQ,CAAC;IACzD,CAAA;GAEA;EAEJ,CAAA,CAAA;;AAET;;AAGA,SAAS,MAAM,EAAE,MAAM,SAAyD;CAC9E,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD;EAAM,eAAY;EAAQ,UAAA;CAAW,CAAA,GACrC,oBAAC,QAAD;EAAM,WAAU;EAAW,UAAA;CAAY,CAAA,CACvC,EAAA,CAAA;AAEN"}
|
|
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 { useLabels } from '../labels/use-labels';\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 /**\n * Words the field announces, for other languages. A {@link LabelsProvider} sets them for the\n * whole app.\n */\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 = useLabels('TextField', 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 const fit = () => {\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 };\n if (element.getClientRects().length > 0 || typeof ResizeObserver === 'undefined') {\n fit();\n return;\n }\n // Out of sight (`display: none` above it: a closed tab or panel), it has no height to read,\n // and set to its 0px it stayed invisible once shown. It keeps its rows until it shows.\n element.style.height = '';\n const observer = new ResizeObserver(() => {\n if (element.getClientRects().length === 0) return;\n observer.disconnect();\n fit();\n });\n observer.observe(element);\n return () => observer.disconnect();\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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"}
|
|
@@ -78,9 +78,9 @@ const textFieldStyles = tv({
|
|
|
78
78
|
],
|
|
79
79
|
field: "flex min-w-0 flex-1 items-center",
|
|
80
80
|
input: [
|
|
81
|
-
"min-w-0 flex-1 bg-transparent p-0 text-body-large outline-none",
|
|
81
|
+
"min-w-0 flex-1 bg-transparent p-0 text-body-large outline-none text-ellipsis",
|
|
82
82
|
"text-[color:var(--_md-comp-text-field-input-text-color)] caret-[color:var(--_md-comp-text-field-caret-color)]",
|
|
83
|
-
"placeholder:text-on-surface-variant placeholder:opacity-100",
|
|
83
|
+
"placeholder:text-on-surface-variant placeholder:opacity-100 placeholder:text-ellipsis",
|
|
84
84
|
"disabled:cursor-default group-data-[field-state=disabled]/field:text-on-surface/38",
|
|
85
85
|
"group-data-invalid/field:caret-error",
|
|
86
86
|
"resize-none"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-field-styles.js","names":[],"sources":["../../../src/components/text-field/text-field-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (TextField.kt, OutlinedTextField.kt,\n * internal/TextFieldImpl.kt and the Filled/OutlinedTextFieldTokens files):\n * 56px minimum height, 16px horizontal padding (4px next to a 48px icon box), 8px\n * vertical padding with a label, 4px above supporting text, 2px around prefix/suffix,\n * a 4px gap around the outlined label notch, label float on the fast spatial spring.\n *\n * Web density (`--md-sys-density`, 0 to -3; M3: \"decreases the top and bottom padding\"): each\n * step takes 4px off the container (56 → 52 → 48 → 44px), 2px from the padding above and below\n * the value, and moves the resting and the filled field's floated label up 2px, so they stay\n * centred and clear of the value. The icon boxes are at most as tall as the container (the\n * 48px touch targets of icon buttons in them extend past it), and at -3 the container's\n * `::before` / `::after` add 2px of hit area above and below, so pressing within 48px of its\n * centre still focuses the field.\n *\n * Colours depend on one mutually exclusive `data-field-state` on the root\n * (disabled > error-focus > error-hover > error > focus > hover > rest), so part styles\n * never compete.\n *\n * The resting container colour and shape, outline and indicator colours, the focused and\n * hovered outline, the focused indicator and label, and the label, input, caret, supporting\n * text and icon colours come from the text field's component tokens (`text-field-tokens.ts`,\n * `--md-comp-<variant>-text-field-*`); the root carries `data-tokens=\"md-comp-text-field\"`\n * and `data-variant`. A consumer class (`classNames.label`, …) still wins in `cn()`. Every\n * class is written out in full so Tailwind can find it.\n */\n\n// The label's top, start, font size, line height and letter spacing on the fast spatial\n// spring; its colour and opacity on the fast effects spring.\nconst geometryTransition =\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-effects-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-effects-fast-easing)]';\n\n/*\n * Colour on the fast effects spring, size on the fast spatial spring (Compose animates the\n * indicator thickness on FastSpatial and its colour on FastEffects).\n */\nconst colourAndSizeTransition =\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]';\n\nconst spatialTransition =\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]';\n\nconst effectsTransition =\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]';\n\n/** Variant definitions for {@link TextField}. */\nexport const textFieldStyles = tv({\n slots: {\n root: 'group/field inline-flex w-[280px] max-w-full flex-col align-top text-start',\n // Library-owned inner wrapper: positions the label, outline and indicator.\n container: [\n 'relative flex min-h-[calc(56px+var(--md-sys-density,0)*4px)] cursor-text items-stretch',\n // Dense fields keep a 48px hit area: at -3 (44px) 2px more above and below.\n \"before:absolute before:inset-x-0 before:bottom-full before:h-[max(0px,calc(-4px-var(--md-sys-density,0)*2px))] before:content-['']\",\n \"after:absolute after:inset-x-0 after:top-full after:h-[max(0px,calc(-4px-var(--md-sys-density,0)*2px))] after:content-['']\",\n 'bg-[var(--_md-comp-text-field-container-color)]',\n ],\n outline: [\n 'pointer-events-none absolute inset-x-0 bottom-0 m-0 min-w-0 px-[12px]',\n 'rounded-[var(--_md-comp-text-field-container-shape)] border border-solid',\n 'border-[color:var(--_md-comp-text-field-outline-color)]',\n // Colour and width both on the fast effects spring. Compose animates the width on\n // FastSpatial, but a browser draws a border in whole device pixels, so a width that\n // overshoots 2px and settles back draws 2 → 1 → 2px. The effects spring never\n // overshoots, so the width only grows (and never dips below 1px on the way back).\n // The filled indicator keeps the spatial spring: its `height` draws fractions.\n '[transition-property:border-color,border-width]',\n effectsTransition,\n 'group-data-[field-state=hover]/field:border-[color:var(--_md-comp-text-field-hover-outline-color)]',\n 'group-data-[field-state=focus]/field:border-2',\n 'group-data-[field-state=focus]/field:border-[color:var(--_md-comp-text-field-focus-outline-color)]',\n 'group-data-[field-state=error]/field:border-error',\n 'group-data-[field-state=error-hover]/field:border-on-error-container',\n 'group-data-[field-state=error-focus]/field:border-2 group-data-[field-state=error-focus]/field:border-error',\n 'group-data-[field-state=disabled]/field:border-on-surface/12',\n ],\n // Hidden text the width of the floated label: cuts the notch in the outline. It opens\n // from 0fr to 1fr of the label's own width on the label's spring, so the gap keeps pace\n // with the label as it moves up.\n notch: [\n 'invisible grid h-[16px] grid-cols-[0fr] p-0 text-body-small whitespace-nowrap',\n 'transition-[grid-template-columns]',\n spatialTransition,\n 'group-data-floated/field:grid-cols-[1fr] group-has-[:autofill]/field:grid-cols-[1fr]',\n ],\n notchClip: 'min-w-0 overflow-hidden',\n notchText: 'inline-block px-[4px]',\n indicator: [\n 'pointer-events-none absolute inset-x-0 bottom-0 h-px',\n 'bg-[var(--_md-comp-text-field-active-indicator-color)]',\n '[transition-property:background-color,height]',\n colourAndSizeTransition,\n 'group-data-[field-state=hover]/field:bg-on-surface',\n 'group-data-[field-state=focus]/field:h-[2px]',\n 'group-data-[field-state=focus]/field:bg-[var(--_md-comp-text-field-focus-active-indicator-color)]',\n 'group-data-[field-state=error]/field:bg-error',\n 'group-data-[field-state=error-hover]/field:bg-on-error-container',\n 'group-data-[field-state=error-focus]/field:h-[2px] group-data-[field-state=error-focus]/field:bg-error',\n 'group-data-[field-state=disabled]/field:bg-on-surface/38',\n // Forced colours paint backgrounds as Canvas, which hid the filled field's only edge:\n // the line takes system colours instead (2026-10-15).\n 'forced-colors:bg-[CanvasText]! forced-colors:forced-color-adjust-none',\n 'forced-colors:group-data-[field-state=focus]/field:bg-[Highlight]! forced-colors:group-data-[field-state=error-focus]/field:bg-[Highlight]!',\n 'forced-colors:group-data-[field-state=disabled]/field:bg-[GrayText]!',\n ],\n label: [\n 'pointer-events-none absolute top-[calc(16px+var(--md-sys-density,0)*2px)] max-w-[calc(100%-32px)] truncate',\n 'text-body-large text-[color:var(--_md-comp-text-field-label-text-color)]',\n '[transition-property:top,inset-inline-start,font-size,line-height,letter-spacing,color,opacity]',\n geometryTransition,\n 'group-data-[field-state=focus]/field:text-[color:var(--_md-comp-text-field-focus-label-text-color)]',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-on-error-container',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n icon: [\n 'flex h-[min(48px,calc(56px+var(--md-sys-density,0)*4px))] w-[48px] shrink-0 items-center justify-center self-center',\n // Icon buttons here keep their size at every density: the box is a fixed 48px column\n // that density only shortens. One with its own `density` still follows it.\n '[&_:not([data-density])]:[--md-sys-density:0]',\n '[&>svg]:size-[24px]',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n leadingIcon: 'text-[color:var(--_md-comp-text-field-leading-icon-color)]',\n trailingIcon: [\n 'text-[color:var(--_md-comp-text-field-trailing-icon-color)]',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-on-error-container',\n ],\n field: 'flex min-w-0 flex-1 items-center',\n input: [\n 'min-w-0 flex-1 bg-transparent p-0 text-body-large outline-none',\n 'text-[color:var(--_md-comp-text-field-input-text-color)] caret-[color:var(--_md-comp-text-field-caret-color)]',\n 'placeholder:text-on-surface-variant placeholder:opacity-100',\n 'disabled:cursor-default group-data-[field-state=disabled]/field:text-on-surface/38',\n 'group-data-invalid/field:caret-error',\n 'resize-none',\n ],\n affix: [\n 'text-body-large text-on-surface-variant whitespace-nowrap',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n prefix: 'pe-[2px]',\n suffix: 'ps-[2px]',\n supporting: 'flex gap-[16px] px-[16px] pt-[4px]',\n // The supporting text's resting colour; the error message keeps its own.\n supportingTextColor: 'text-[color:var(--_md-comp-text-field-supporting-text-color)]',\n // Error colour whenever the field is invalid, with or without a message (Compose's\n // `ErrorSupportingColor`), so the supporting text reads as the error's explanation.\n supportingText: [\n 'min-w-0 flex-1',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n counter: [\n 'ms-auto',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n // The built-in error icon's own box; the trailing slot colours it.\n errorIcon: '[&>svg]:size-[24px]',\n required: 'text-inherit',\n },\n variants: {\n variant: {\n filled: {\n container: [\n 'rounded-t-[var(--_md-comp-text-field-container-shape)]',\n 'group-data-[field-state=disabled]/field:bg-on-surface/4',\n ],\n outline: 'hidden',\n },\n outlined: {\n container: 'rounded-[var(--_md-comp-text-field-container-shape)]',\n indicator: 'hidden',\n // OutlinedTextFieldTokens.HoverLabelColor (filled keeps on-surface-variant).\n label: 'group-data-[field-state=hover]/field:text-on-surface',\n },\n },\n // With a label the outline starts 8px higher, so its border runs through the middle of\n // the 16px notch legend and lands on the container's top edge.\n hasLabel: { true: { outline: '-top-[8px]' }, false: { outline: 'top-0' } },\n hasLeadingIcon: {\n true: { field: 'ps-[4px]', label: 'start-[52px]' },\n false: { field: 'ps-[16px]', label: 'start-[16px]' },\n },\n hasTrailingIcon: {\n true: { field: 'pe-[4px]' },\n false: { field: 'pe-[16px]' },\n },\n multiline: {\n true: { field: 'items-start', input: 'block' },\n false: {},\n },\n // A text area keeps its height and scrolls (M3: \"a fixed height and scroll vertically\").\n fixedHeight: {\n true: { input: 'overflow-y-auto' },\n false: {},\n },\n // M3: supporting text can be \"visible only on focus\". It keeps its space, so nothing\n // jumps, and stays in the accessible description.\n supportingTextOnFocus: {\n true: {\n supportingText: [\n 'opacity-0 transition-opacity group-data-focused/field:opacity-100',\n effectsTransition,\n ],\n },\n false: {},\n },\n },\n compoundVariants: [\n /*\n * Input padding: filled fields with a label leave room above for the floated label.\n * Below 52px (density -2 and -3) there is no room for it: as MDC's text field\n * (`$minimum-height-for-filled-label: 52px`), the floated label is hidden and the value\n * is centred like a field without a label. The resting label still shows in the empty\n * field, and still names the input. `--m3-field-label-room` is 1 down to density -1\n * and 0 from -2 (a clamped step of the density, so CSS alone switches the layout).\n */\n {\n variant: 'filled',\n hasLabel: true,\n class: {\n container: '[--m3-field-label-room:clamp(0,calc((var(--md-sys-density,0)+1.5)*100),1)]',\n field: [\n 'pt-[calc(16px+var(--md-sys-density,0)*2px+8px*var(--m3-field-label-room))]',\n 'pb-[calc(16px+var(--md-sys-density,0)*2px-8px*var(--m3-field-label-room))]',\n ],\n },\n },\n {\n variant: 'filled',\n hasLabel: false,\n class: { field: 'py-[calc(16px+var(--md-sys-density,0)*2px)]' },\n },\n { variant: 'outlined', class: { field: 'py-[calc(16px+var(--md-sys-density,0)*2px)]' } },\n // Floated label: filled → 8px from the top (less 2px per density step); outlined → centred on the border, at 16px.\n {\n variant: 'filled',\n hasLabel: true,\n class: {\n label: [\n 'group-data-floated/field:top-[calc(8px+var(--md-sys-density,0)*2px)] group-data-floated/field:text-body-small',\n 'group-has-[:autofill]/field:top-[calc(8px+var(--md-sys-density,0)*2px)] group-has-[:autofill]/field:text-body-small',\n // Hidden once floated below 52px (see the padding above).\n 'group-data-floated/field:opacity-[var(--m3-field-label-room)] group-has-[:autofill]/field:opacity-[var(--m3-field-label-room)]',\n ],\n },\n },\n {\n variant: 'outlined',\n hasLabel: true,\n class: {\n label: [\n 'group-data-floated/field:top-[-8px] group-data-floated/field:start-[16px] group-data-floated/field:text-body-small',\n 'group-has-[:autofill]/field:top-[-8px] group-has-[:autofill]/field:start-[16px] group-has-[:autofill]/field:text-body-small',\n ],\n },\n },\n // With a label, the placeholder and prefix/suffix only show once the label floats.\n {\n hasLabel: true,\n class: {\n // The placeholder fades in as the label floats (Compose fades it on FastEffects).\n input: [\n 'placeholder:opacity-0 group-data-floated/field:placeholder:opacity-100',\n 'placeholder:transition-opacity placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n affix: [\n 'opacity-0 transition-opacity group-data-floated/field:opacity-100 group-has-[:autofill]/field:opacity-100',\n effectsTransition,\n ],\n },\n },\n ],\n defaultVariants: {\n variant: 'filled',\n hasLabel: true,\n hasLeadingIcon: false,\n hasTrailingIcon: false,\n multiline: false,\n fixedHeight: false,\n supportingTextOnFocus: false,\n },\n});\n\n/**\n * The variants of {@link textFieldStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type TextFieldStyleProps = VariantProps<typeof textFieldStyles>;\nexport type TextFieldVariant = NonNullable<TextFieldStyleProps['variant']>;\n"],"mappings":";;AA+BA,MAAM,qBACJ;AAMF,MAAM,0BACJ;AAEF,MAAM,oBACJ;AAEF,MAAM,oBACJ;;AAGF,MAAa,kBAAkB,GAAG;CAChC,OAAO;EACL,MAAM;EAEN,WAAW;GACT;GAEA;GACA;GACA;EACF;EACA,SAAS;GACP;GACA;GACA;GAMA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAIA,OAAO;GACL;GACA;GACA;GACA;EACF;EACA,WAAW;EACX,WAAW;EACX,WAAW;GACT;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAGA;GACA;GACA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EACA,MAAM;GACJ;GAGA;GACA;GACA;EACF;EACA,aAAa;EACb,cAAc;GACZ;GACA;GACA;GACA;EACF;EACA,OAAO;EACP,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;EACF;EACA,OAAO,CACL,6DACA,4DACF;EACA,QAAQ;EACR,QAAQ;EACR,YAAY;EAEZ,qBAAqB;EAGrB,gBAAgB;GACd;GACA;GACA;GACA;GACA;EACF;EACA,SAAS;GACP;GACA;GACA;GACA;GACA;EACF;EAEA,WAAW;EACX,UAAU;CACZ;CACA,UAAU;EACR,SAAS;GACP,QAAQ;IACN,WAAW,CACT,0DACA,yDACF;IACA,SAAS;GACX;GACA,UAAU;IACR,WAAW;IACX,WAAW;IAEX,OAAO;GACT;EACF;EAGA,UAAU;GAAE,MAAM,EAAE,SAAS,aAAa;GAAG,OAAO,EAAE,SAAS,QAAQ;EAAE;EACzE,gBAAgB;GACd,MAAM;IAAE,OAAO;IAAY,OAAO;GAAe;GACjD,OAAO;IAAE,OAAO;IAAa,OAAO;GAAe;EACrD;EACA,iBAAiB;GACf,MAAM,EAAE,OAAO,WAAW;GAC1B,OAAO,EAAE,OAAO,YAAY;EAC9B;EACA,WAAW;GACT,MAAM;IAAE,OAAO;IAAe,OAAO;GAAQ;GAC7C,OAAO,CAAC;EACV;EAEA,aAAa;GACX,MAAM,EAAE,OAAO,kBAAkB;GACjC,OAAO,CAAC;EACV;EAGA,uBAAuB;GACrB,MAAM,EACJ,gBAAgB,CACd,qEACA,iBACF,EACF;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB;EAShB;GACE,SAAS;GACT,UAAU;GACV,OAAO;IACL,WAAW;IACX,OAAO,CACL,8EACA,4EACF;GACF;EACF;EACA;GACE,SAAS;GACT,UAAU;GACV,OAAO,EAAE,OAAO,8CAA8C;EAChE;EACA;GAAE,SAAS;GAAY,OAAO,EAAE,OAAO,8CAA8C;EAAE;EAEvF;GACE,SAAS;GACT,UAAU;GACV,OAAO,EACL,OAAO;IACL;IACA;IAEA;GACF,EACF;EACF;EACA;GACE,SAAS;GACT,UAAU;GACV,OAAO,EACL,OAAO,CACL,sHACA,6HACF,EACF;EACF;EAEA;GACE,UAAU;GACV,OAAO;IAEL,OAAO,CACL,0EACA,6MACF;IACA,OAAO,CACL,6GACA,iBACF;GACF;EACF;CACF;CACA,iBAAiB;EACf,SAAS;EACT,UAAU;EACV,gBAAgB;EAChB,iBAAiB;EACjB,WAAW;EACX,aAAa;EACb,uBAAuB;CACzB;AACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"text-field-styles.js","names":[],"sources":["../../../src/components/text-field/text-field-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (TextField.kt, OutlinedTextField.kt,\n * internal/TextFieldImpl.kt and the Filled/OutlinedTextFieldTokens files):\n * 56px minimum height, 16px horizontal padding (4px next to a 48px icon box), 8px\n * vertical padding with a label, 4px above supporting text, 2px around prefix/suffix,\n * a 4px gap around the outlined label notch, label float on the fast spatial spring.\n *\n * Web density (`--md-sys-density`, 0 to -3; M3: \"decreases the top and bottom padding\"): each\n * step takes 4px off the container (56 → 52 → 48 → 44px), 2px from the padding above and below\n * the value, and moves the resting and the filled field's floated label up 2px, so they stay\n * centred and clear of the value. The icon boxes are at most as tall as the container (the\n * 48px touch targets of icon buttons in them extend past it), and at -3 the container's\n * `::before` / `::after` add 2px of hit area above and below, so pressing within 48px of its\n * centre still focuses the field.\n *\n * Colours depend on one mutually exclusive `data-field-state` on the root\n * (disabled > error-focus > error-hover > error > focus > hover > rest), so part styles\n * never compete.\n *\n * The resting container colour and shape, outline and indicator colours, the focused and\n * hovered outline, the focused indicator and label, and the label, input, caret, supporting\n * text and icon colours come from the text field's component tokens (`text-field-tokens.ts`,\n * `--md-comp-<variant>-text-field-*`); the root carries `data-tokens=\"md-comp-text-field\"`\n * and `data-variant`. A consumer class (`classNames.label`, …) still wins in `cn()`. Every\n * class is written out in full so Tailwind can find it.\n */\n\n// The label's top, start, font size, line height and letter spacing on the fast spatial\n// spring; its colour and opacity on the fast effects spring.\nconst geometryTransition =\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration),var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-effects-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing),var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-effects-fast-easing)]';\n\n/*\n * Colour on the fast effects spring, size on the fast spatial spring (Compose animates the\n * indicator thickness on FastSpatial and its colour on FastEffects).\n */\nconst colourAndSizeTransition =\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration),var(--md-sys-motion-spring-spatial-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing),var(--md-sys-motion-spring-spatial-fast-easing)]';\n\nconst spatialTransition =\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]';\n\nconst effectsTransition =\n '[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] [transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]';\n\n/** Variant definitions for {@link TextField}. */\nexport const textFieldStyles = tv({\n slots: {\n root: 'group/field inline-flex w-[280px] max-w-full flex-col align-top text-start',\n // Library-owned inner wrapper: positions the label, outline and indicator.\n container: [\n 'relative flex min-h-[calc(56px+var(--md-sys-density,0)*4px)] cursor-text items-stretch',\n // Dense fields keep a 48px hit area: at -3 (44px) 2px more above and below.\n \"before:absolute before:inset-x-0 before:bottom-full before:h-[max(0px,calc(-4px-var(--md-sys-density,0)*2px))] before:content-['']\",\n \"after:absolute after:inset-x-0 after:top-full after:h-[max(0px,calc(-4px-var(--md-sys-density,0)*2px))] after:content-['']\",\n 'bg-[var(--_md-comp-text-field-container-color)]',\n ],\n outline: [\n 'pointer-events-none absolute inset-x-0 bottom-0 m-0 min-w-0 px-[12px]',\n 'rounded-[var(--_md-comp-text-field-container-shape)] border border-solid',\n 'border-[color:var(--_md-comp-text-field-outline-color)]',\n // Colour and width both on the fast effects spring. Compose animates the width on\n // FastSpatial, but a browser draws a border in whole device pixels, so a width that\n // overshoots 2px and settles back draws 2 → 1 → 2px. The effects spring never\n // overshoots, so the width only grows (and never dips below 1px on the way back).\n // The filled indicator keeps the spatial spring: its `height` draws fractions.\n '[transition-property:border-color,border-width]',\n effectsTransition,\n 'group-data-[field-state=hover]/field:border-[color:var(--_md-comp-text-field-hover-outline-color)]',\n 'group-data-[field-state=focus]/field:border-2',\n 'group-data-[field-state=focus]/field:border-[color:var(--_md-comp-text-field-focus-outline-color)]',\n 'group-data-[field-state=error]/field:border-error',\n 'group-data-[field-state=error-hover]/field:border-on-error-container',\n 'group-data-[field-state=error-focus]/field:border-2 group-data-[field-state=error-focus]/field:border-error',\n 'group-data-[field-state=disabled]/field:border-on-surface/12',\n ],\n // Hidden text the width of the floated label: cuts the notch in the outline. It opens\n // from 0fr to 1fr of the label's own width on the label's spring, so the gap keeps pace\n // with the label as it moves up.\n notch: [\n 'invisible grid h-[16px] grid-cols-[0fr] p-0 text-body-small whitespace-nowrap',\n 'transition-[grid-template-columns]',\n spatialTransition,\n 'group-data-floated/field:grid-cols-[1fr] group-has-[:autofill]/field:grid-cols-[1fr]',\n ],\n notchClip: 'min-w-0 overflow-hidden',\n notchText: 'inline-block px-[4px]',\n indicator: [\n 'pointer-events-none absolute inset-x-0 bottom-0 h-px',\n 'bg-[var(--_md-comp-text-field-active-indicator-color)]',\n '[transition-property:background-color,height]',\n colourAndSizeTransition,\n 'group-data-[field-state=hover]/field:bg-on-surface',\n 'group-data-[field-state=focus]/field:h-[2px]',\n 'group-data-[field-state=focus]/field:bg-[var(--_md-comp-text-field-focus-active-indicator-color)]',\n 'group-data-[field-state=error]/field:bg-error',\n 'group-data-[field-state=error-hover]/field:bg-on-error-container',\n 'group-data-[field-state=error-focus]/field:h-[2px] group-data-[field-state=error-focus]/field:bg-error',\n 'group-data-[field-state=disabled]/field:bg-on-surface/38',\n // Forced colours paint backgrounds as Canvas, which hid the filled field's only edge:\n // the line takes system colours instead (2026-10-15).\n 'forced-colors:bg-[CanvasText]! forced-colors:forced-color-adjust-none',\n 'forced-colors:group-data-[field-state=focus]/field:bg-[Highlight]! forced-colors:group-data-[field-state=error-focus]/field:bg-[Highlight]!',\n 'forced-colors:group-data-[field-state=disabled]/field:bg-[GrayText]!',\n ],\n label: [\n 'pointer-events-none absolute top-[calc(16px+var(--md-sys-density,0)*2px)] max-w-[calc(100%-32px)] truncate',\n 'text-body-large text-[color:var(--_md-comp-text-field-label-text-color)]',\n '[transition-property:top,inset-inline-start,font-size,line-height,letter-spacing,color,opacity]',\n geometryTransition,\n 'group-data-[field-state=focus]/field:text-[color:var(--_md-comp-text-field-focus-label-text-color)]',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-on-error-container',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n icon: [\n 'flex h-[min(48px,calc(56px+var(--md-sys-density,0)*4px))] w-[48px] shrink-0 items-center justify-center self-center',\n // Icon buttons here keep their size at every density: the box is a fixed 48px column\n // that density only shortens. One with its own `density` still follows it.\n '[&_:not([data-density])]:[--md-sys-density:0]',\n '[&>svg]:size-[24px]',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n leadingIcon: 'text-[color:var(--_md-comp-text-field-leading-icon-color)]',\n trailingIcon: [\n 'text-[color:var(--_md-comp-text-field-trailing-icon-color)]',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-on-error-container',\n ],\n field: 'flex min-w-0 flex-1 items-center',\n // A hint or value longer than the field ends in an ellipsis, as the label does, not\n // mid-letter; the value only while the field isn't focused, since focused it scrolls\n // (2026-11-11). A text area wraps instead, so it has no effect there.\n input: [\n 'min-w-0 flex-1 bg-transparent p-0 text-body-large outline-none text-ellipsis',\n 'text-[color:var(--_md-comp-text-field-input-text-color)] caret-[color:var(--_md-comp-text-field-caret-color)]',\n 'placeholder:text-on-surface-variant placeholder:opacity-100 placeholder:text-ellipsis',\n 'disabled:cursor-default group-data-[field-state=disabled]/field:text-on-surface/38',\n 'group-data-invalid/field:caret-error',\n 'resize-none',\n ],\n affix: [\n 'text-body-large text-on-surface-variant whitespace-nowrap',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n prefix: 'pe-[2px]',\n suffix: 'ps-[2px]',\n supporting: 'flex gap-[16px] px-[16px] pt-[4px]',\n // The supporting text's resting colour; the error message keeps its own.\n supportingTextColor: 'text-[color:var(--_md-comp-text-field-supporting-text-color)]',\n // Error colour whenever the field is invalid, with or without a message (Compose's\n // `ErrorSupportingColor`), so the supporting text reads as the error's explanation.\n supportingText: [\n 'min-w-0 flex-1',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n counter: [\n 'ms-auto',\n 'group-data-[field-state=error]/field:text-error',\n 'group-data-[field-state=error-hover]/field:text-error',\n 'group-data-[field-state=error-focus]/field:text-error',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n ],\n // The built-in error icon's own box; the trailing slot colours it.\n errorIcon: '[&>svg]:size-[24px]',\n required: 'text-inherit',\n },\n variants: {\n variant: {\n filled: {\n container: [\n 'rounded-t-[var(--_md-comp-text-field-container-shape)]',\n 'group-data-[field-state=disabled]/field:bg-on-surface/4',\n ],\n outline: 'hidden',\n },\n outlined: {\n container: 'rounded-[var(--_md-comp-text-field-container-shape)]',\n indicator: 'hidden',\n // OutlinedTextFieldTokens.HoverLabelColor (filled keeps on-surface-variant).\n label: 'group-data-[field-state=hover]/field:text-on-surface',\n },\n },\n // With a label the outline starts 8px higher, so its border runs through the middle of\n // the 16px notch legend and lands on the container's top edge.\n hasLabel: { true: { outline: '-top-[8px]' }, false: { outline: 'top-0' } },\n hasLeadingIcon: {\n true: { field: 'ps-[4px]', label: 'start-[52px]' },\n false: { field: 'ps-[16px]', label: 'start-[16px]' },\n },\n hasTrailingIcon: {\n true: { field: 'pe-[4px]' },\n false: { field: 'pe-[16px]' },\n },\n multiline: {\n true: { field: 'items-start', input: 'block' },\n false: {},\n },\n // A text area keeps its height and scrolls (M3: \"a fixed height and scroll vertically\").\n fixedHeight: {\n true: { input: 'overflow-y-auto' },\n false: {},\n },\n // M3: supporting text can be \"visible only on focus\". It keeps its space, so nothing\n // jumps, and stays in the accessible description.\n supportingTextOnFocus: {\n true: {\n supportingText: [\n 'opacity-0 transition-opacity group-data-focused/field:opacity-100',\n effectsTransition,\n ],\n },\n false: {},\n },\n },\n compoundVariants: [\n /*\n * Input padding: filled fields with a label leave room above for the floated label.\n * Below 52px (density -2 and -3) there is no room for it: as MDC's text field\n * (`$minimum-height-for-filled-label: 52px`), the floated label is hidden and the value\n * is centred like a field without a label. The resting label still shows in the empty\n * field, and still names the input. `--m3-field-label-room` is 1 down to density -1\n * and 0 from -2 (a clamped step of the density, so CSS alone switches the layout).\n */\n {\n variant: 'filled',\n hasLabel: true,\n class: {\n container: '[--m3-field-label-room:clamp(0,calc((var(--md-sys-density,0)+1.5)*100),1)]',\n field: [\n 'pt-[calc(16px+var(--md-sys-density,0)*2px+8px*var(--m3-field-label-room))]',\n 'pb-[calc(16px+var(--md-sys-density,0)*2px-8px*var(--m3-field-label-room))]',\n ],\n },\n },\n {\n variant: 'filled',\n hasLabel: false,\n class: { field: 'py-[calc(16px+var(--md-sys-density,0)*2px)]' },\n },\n { variant: 'outlined', class: { field: 'py-[calc(16px+var(--md-sys-density,0)*2px)]' } },\n // Floated label: filled → 8px from the top (less 2px per density step); outlined → centred on the border, at 16px.\n {\n variant: 'filled',\n hasLabel: true,\n class: {\n label: [\n 'group-data-floated/field:top-[calc(8px+var(--md-sys-density,0)*2px)] group-data-floated/field:text-body-small',\n 'group-has-[:autofill]/field:top-[calc(8px+var(--md-sys-density,0)*2px)] group-has-[:autofill]/field:text-body-small',\n // Hidden once floated below 52px (see the padding above).\n 'group-data-floated/field:opacity-[var(--m3-field-label-room)] group-has-[:autofill]/field:opacity-[var(--m3-field-label-room)]',\n ],\n },\n },\n {\n variant: 'outlined',\n hasLabel: true,\n class: {\n label: [\n 'group-data-floated/field:top-[-8px] group-data-floated/field:start-[16px] group-data-floated/field:text-body-small',\n 'group-has-[:autofill]/field:top-[-8px] group-has-[:autofill]/field:start-[16px] group-has-[:autofill]/field:text-body-small',\n ],\n },\n },\n // With a label, the placeholder and prefix/suffix only show once the label floats.\n {\n hasLabel: true,\n class: {\n // The placeholder fades in as the label floats (Compose fades it on FastEffects).\n input: [\n 'placeholder:opacity-0 group-data-floated/field:placeholder:opacity-100',\n 'placeholder:transition-opacity placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n affix: [\n 'opacity-0 transition-opacity group-data-floated/field:opacity-100 group-has-[:autofill]/field:opacity-100',\n effectsTransition,\n ],\n },\n },\n ],\n defaultVariants: {\n variant: 'filled',\n hasLabel: true,\n hasLeadingIcon: false,\n hasTrailingIcon: false,\n multiline: false,\n fixedHeight: false,\n supportingTextOnFocus: false,\n },\n});\n\n/**\n * The variants of {@link textFieldStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type TextFieldStyleProps = VariantProps<typeof textFieldStyles>;\nexport type TextFieldVariant = NonNullable<TextFieldStyleProps['variant']>;\n"],"mappings":";;AA+BA,MAAM,qBACJ;AAMF,MAAM,0BACJ;AAEF,MAAM,oBACJ;AAEF,MAAM,oBACJ;;AAGF,MAAa,kBAAkB,GAAG;CAChC,OAAO;EACL,MAAM;EAEN,WAAW;GACT;GAEA;GACA;GACA;EACF;EACA,SAAS;GACP;GACA;GACA;GAMA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAIA,OAAO;GACL;GACA;GACA;GACA;EACF;EACA,WAAW;EACX,WAAW;EACX,WAAW;GACT;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAGA;GACA;GACA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EACA,MAAM;GACJ;GAGA;GACA;GACA;EACF;EACA,aAAa;EACb,cAAc;GACZ;GACA;GACA;GACA;EACF;EACA,OAAO;EAIP,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;EACF;EACA,OAAO,CACL,6DACA,4DACF;EACA,QAAQ;EACR,QAAQ;EACR,YAAY;EAEZ,qBAAqB;EAGrB,gBAAgB;GACd;GACA;GACA;GACA;GACA;EACF;EACA,SAAS;GACP;GACA;GACA;GACA;GACA;EACF;EAEA,WAAW;EACX,UAAU;CACZ;CACA,UAAU;EACR,SAAS;GACP,QAAQ;IACN,WAAW,CACT,0DACA,yDACF;IACA,SAAS;GACX;GACA,UAAU;IACR,WAAW;IACX,WAAW;IAEX,OAAO;GACT;EACF;EAGA,UAAU;GAAE,MAAM,EAAE,SAAS,aAAa;GAAG,OAAO,EAAE,SAAS,QAAQ;EAAE;EACzE,gBAAgB;GACd,MAAM;IAAE,OAAO;IAAY,OAAO;GAAe;GACjD,OAAO;IAAE,OAAO;IAAa,OAAO;GAAe;EACrD;EACA,iBAAiB;GACf,MAAM,EAAE,OAAO,WAAW;GAC1B,OAAO,EAAE,OAAO,YAAY;EAC9B;EACA,WAAW;GACT,MAAM;IAAE,OAAO;IAAe,OAAO;GAAQ;GAC7C,OAAO,CAAC;EACV;EAEA,aAAa;GACX,MAAM,EAAE,OAAO,kBAAkB;GACjC,OAAO,CAAC;EACV;EAGA,uBAAuB;GACrB,MAAM,EACJ,gBAAgB,CACd,qEACA,iBACF,EACF;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB;EAShB;GACE,SAAS;GACT,UAAU;GACV,OAAO;IACL,WAAW;IACX,OAAO,CACL,8EACA,4EACF;GACF;EACF;EACA;GACE,SAAS;GACT,UAAU;GACV,OAAO,EAAE,OAAO,8CAA8C;EAChE;EACA;GAAE,SAAS;GAAY,OAAO,EAAE,OAAO,8CAA8C;EAAE;EAEvF;GACE,SAAS;GACT,UAAU;GACV,OAAO,EACL,OAAO;IACL;IACA;IAEA;GACF,EACF;EACF;EACA;GACE,SAAS;GACT,UAAU;GACV,OAAO,EACL,OAAO,CACL,sHACA,6HACF,EACF;EACF;EAEA;GACE,UAAU;GACV,OAAO;IAEL,OAAO,CACL,0EACA,6MACF;IACA,OAAO,CACL,6GACA,iBACF;GACF;EACF;CACF;CACA,iBAAiB;EACf,SAAS;EACT,UAAU;EACV,gBAAgB;EAChB,iBAAiB;EACjB,WAAW;EACX,aAAa;EACb,uBAAuB;CACzB;AACF,CAAC"}
|