@windmorph/react 0.2.2 → 0.2.3
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.js +1 -1
- package/dist/components/autocomplete/Autocomplete.js.map +1 -1
- package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
- package/dist/components/chip/chip-tokens.d.ts +4 -4
- package/dist/components/date-picker/date-picker-styles.d.ts +3 -3
- package/dist/components/date-picker/date-picker-tokens.d.ts +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/menu/Menu.d.ts +13 -0
- package/dist/components/menu/Menu.js +14 -1
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/menu/menu-tokens.d.ts +1 -1
- package/dist/components/navigation/navigation-styles.js +1 -1
- package/dist/components/navigation/navigation-styles.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/slider/Slider.js +5 -1
- package/dist/components/slider/Slider.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +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/tooltip/tooltip-tokens.d.ts +1 -1
- package/dist/primitives/use-m3-interaction.js +10 -6
- package/dist/primitives/use-m3-interaction.js.map +1 -1
- package/dist/styles/utilities.css +22 -15
- package/dist/styles.compiled.css +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Slider.js","names":[],"sources":["../../../src/components/slider/Slider.tsx"],"sourcesContent":["'use client';\n\nimport {\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n} from 'react';\nimport {\n mergeProps,\n useFocusRing,\n useLocale,\n useNumberFormatter,\n useObjectRef,\n useSlider,\n useSliderThumb,\n VisuallyHidden,\n} from 'react-aria';\nimport { useSliderState, type SliderState } from 'react-stately';\nimport { DomDirectionLocale } from '../../primitives/DomDirectionLocale';\nimport { cn } from '../../utils/cn';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { splitRootProps, type RootDOMProps } from '../../utils/root-props';\nimport { useTouchTrack } from './use-touch-track';\nimport {\n iconSizeWhenFits,\n movedStartIcon,\n segmentLength,\n trackGeometry,\n type TrackSegment,\n} from './slider-geometry';\nimport { sliderStyles, type SliderOrientation, type SliderSize } from './slider-styles';\n\nexport interface SliderClassNames {\n /** The outer element. */\n root?: string;\n /** The track, holding the active and inactive segments and the stop indicators. */\n track?: string;\n /** Each active track segment (the filled part, from the start or the centre). */\n activeTrack?: string;\n /** Each inactive track segment. */\n inactiveTrack?: string;\n /** The stop indicators: at the track's ends, and at every step with `ticks`. */\n stop?: string;\n /** The inset icons (`leadingIcon`, `trailingIcon`). */\n icon?: string;\n /** Each thumb, the pressable area around its handle. */\n thumb?: string;\n /** The handle bar drawn inside each thumb. */\n handle?: string;\n /** The value indicator above a thumb (`valueLabel`). */\n label?: string;\n}\n\n/** A slider needs a name: `aria-label`, or `aria-labelledby` pointing at a visible label. */\ntype Naming =\n | {\n /** Accessible name when there's no visible label. */\n 'aria-label': string;\n }\n | {\n /** The id of the visible label that names the slider. */\n 'aria-labelledby': string;\n };\n\ninterface SliderCommonProps extends RootDOMProps {\n /** The track's thickness, from XS (a 16px track) to XL. @default \"xs\" */\n size?: SliderSize;\n /**\n * The direction the slider runs. Vertical sliders run bottom to top and default to 240px\n * tall. @default \"horizontal\"\n */\n orientation?: SliderOrientation;\n /** The lowest value. @default 0 */\n min?: number;\n /** The highest value. @default 100 */\n max?: number;\n /** The amount the value changes by in one step. @default 1 */\n step?: number;\n /** Discrete: show a stop indicator at every step (use a coarse `step`). @default false */\n ticks?: boolean;\n /** Turns the slider off: it can't be moved or focused. @default false */\n disabled?: boolean;\n /** Show the value above the thumb while it is dragged or keyboard-focused. @default false */\n valueLabel?: boolean;\n /** Formats the value for the value indicator and assistive tech. */\n formatOptions?: Intl.NumberFormatOptions;\n /** Form field name (one hidden input per thumb). */\n name?: string;\n /** The id of the slider's root, its `group` (an error summary can link to it). */\n id?: string;\n /** Ref to the root `<div>` element. */\n ref?: Ref<HTMLDivElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SliderClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SliderProps = SliderCommonProps &\n Naming & {\n /** The value (controlled). */\n value?: number;\n /** The initial value when uncontrolled. @default min */\n defaultValue?: number;\n /** Called as the value changes, while dragging too. */\n onChange?: (value: number) => void;\n /** Called when the user stops dragging or releases a key. */\n onChangeEnd?: (value: number) => void;\n /**\n * Fill the track from its centre (for values around a midpoint). M3 doesn't use inset\n * icons on centred sliders, so `leadingIcon` and `trailingIcon` are ignored. @default false\n */\n centered?: boolean;\n /**\n * An icon inside the active track at its start (sizes md and up; ignored, with a dev\n * warning, on xs and sm). When the active track is too short for it, it moves to the\n * start of the inactive track.\n */\n leadingIcon?: ReactNode;\n /** An icon inside the inactive track at its end (sizes md and up; ignored on xs and sm). */\n trailingIcon?: ReactNode;\n };\n\nexport interface RangeSliderLabels {\n /** Name of the start thumb. @default \"Minimum\" */\n start: string;\n /** Name of the end thumb. @default \"Maximum\" */\n end: string;\n}\n\nconst DEFAULT_LABELS: RangeSliderLabels = { start: 'Minimum', end: 'Maximum' };\n\nexport type RangeSliderProps = SliderCommonProps &\n Naming & {\n /** The range, as `[start, end]` (controlled). */\n value?: [number, number];\n /** The initial range when uncontrolled. @default [min, max] */\n defaultValue?: [number, number];\n /** Called as the range changes, while dragging too. */\n onChange?: (value: [number, number]) => void;\n /** Called when the user stops dragging or releases a key. */\n onChangeEnd?: (value: [number, number]) => void;\n /** Words the range slider announces (its thumbs' names), with English defaults. */\n labels?: Partial<RangeSliderLabels>;\n };\n\n/**\n * M3 Expressive slider: XS–XL sizes, horizontal or vertical, continuous or discrete with\n * stop indicators, optionally centred, with inset icons and a value indicator. Arrow keys,\n * Page Up / Down and Home / End adjust it (mirrored in RTL).\n *\n * @example\n * <Slider aria-label=\"Volume\" size=\"md\" leadingIcon={<VolumeIcon />} defaultValue={40} />\n */\nexport function Slider(props: SliderProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <SliderRoot {...props} range={false} directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\n/**\n * M3 Expressive range slider: two thumbs selecting a range, with the same sizes,\n * orientations and discrete stops as {@link Slider}.\n *\n * @example\n * <RangeSlider aria-label=\"Price\" defaultValue={[20, 80]} />\n */\nexport function RangeSlider(props: RangeSliderProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <SliderRoot {...props} range directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\ntype RootProps = (SliderProps | RangeSliderProps) & {\n range: boolean;\n directionRef: (element: HTMLElement | null) => void;\n};\n\nfunction SliderRoot(props: RootProps) {\n const {\n size = 'xs',\n orientation = 'horizontal',\n min = 0,\n max = 100,\n step = 1,\n ticks = false,\n disabled = false,\n valueLabel = false,\n formatOptions,\n name,\n id,\n ref,\n className,\n style,\n classNames,\n range,\n directionRef,\n value,\n defaultValue,\n onChange,\n onChangeEnd,\n ...rest\n } = props;\n const {\n centered = false,\n leadingIcon: startIconProp,\n trailingIcon: endIconProp,\n } = props as SliderProps;\n // M3: only M, L and XL standard sliders take inset icons (\"Avoid adding inset icons to XS\n // or S\"; none on centred sliders).\n const iconSize = size === 'md' || size === 'lg' || size === 'xl';\n const startIcon = iconSize && !centered ? startIconProp : undefined;\n const endIcon = iconSize && !centered ? endIconProp : undefined;\n const labels = { ...DEFAULT_LABELS, ...(props as RangeSliderProps).labels };\n // The root is the slider's group: its DOM attributes join the hook's.\n const { root: data, rest: labelling } = splitRootProps(rest);\n // The naming attributes: the name goes to the group and each thumb (React Aria), and the\n // description, details and error message to each thumb's input, which takes focus.\n const {\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n 'aria-errormessage': errorMessage,\n } = labelling as {\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-details'?: string;\n 'aria-errormessage'?: string;\n };\n\n const numberFormatter = useNumberFormatter(formatOptions);\n // React Stately works with arrays; a single slider wraps and unwraps its number.\n const wrap = (v: number | [number, number] | undefined) =>\n v === undefined ? undefined : Array.isArray(v) ? v : [v];\n const unwrap = (v: number[]) => (range ? (v as [number, number]) : v[0]!);\n const ariaProps = {\n id,\n minValue: min,\n maxValue: max,\n step,\n orientation,\n isDisabled: disabled,\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n value: wrap(value),\n defaultValue: wrap(defaultValue) ?? (range ? [min, max] : [min]),\n onChange: onChange && ((v: number[]) => (onChange as (x: unknown) => void)(unwrap(v))),\n onChangeEnd: onChangeEnd && ((v: number[]) => (onChangeEnd as (x: unknown) => void)(unwrap(v))),\n };\n const state = useSliderState({ ...ariaProps, numberFormatter });\n const rootRef = useObjectRef(ref);\n const trackRef = useRef<HTMLDivElement>(null);\n const { groupProps, trackProps } = useSlider(ariaProps, state, trackRef);\n const { direction } = useLocale();\n const count = Math.round((max - min) / step);\n const touchTrackProps = useTouchTrack(trackProps, {\n state,\n trackRef,\n orientation,\n direction,\n disabled,\n discrete: ticks && count > 0,\n });\n\n const tickFractions =\n ticks && count > 0 ? Array.from({ length: count + 1 }, (_, i) => i / count) : [];\n const fractions = state.values.map((_, index) => state.getThumbPercent(index));\n const geometry = trackGeometry({ fractions, ticks: tickFractions, centered: centered && !range });\n const styles = sliderStyles({ size, orientation });\n if (process.env.NODE_ENV !== 'production' && (startIconProp != null || endIconProp != null)) {\n if (centered) {\n console.warn(\n 'Slider: inset icons are not used on centred sliders (M3), so they are ignored.',\n );\n } else if (!iconSize) {\n console.warn('Slider: inset icons need size md, lg or xl (M3), so they are ignored.');\n }\n }\n if (process.env.NODE_ENV !== 'production' && range && orientation === 'vertical') {\n console.warn('RangeSlider: M3 advises against vertical range sliders.');\n }\n const activeSegment = geometry.segments.find((segment) => segment.icon === 'start');\n const moved =\n activeSegment && startIcon != null && movedStartIcon(activeSegment, geometry.thumbs[0]!);\n\n return (\n <div\n {...mergeProps(data, groupProps)}\n ref={(element) => {\n rootRef.current = element;\n directionRef(element);\n }}\n style={style}\n data-tokens={mergeDataTokens('md-comp-slider', data)}\n data-size={size}\n data-orientation={orientation}\n data-disabled={disabled || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n {...touchTrackProps}\n ref={trackRef}\n className={styles.track({ class: classNames?.track })}\n >\n {geometry.segments.map((segment, index) => (\n <Segment\n key={index}\n segment={segment}\n styles={styles}\n classNames={classNames}\n icon={\n segment.icon === 'start' ? startIcon : segment.icon === 'end' ? endIcon : undefined\n }\n />\n ))}\n {moved && (\n <span\n aria-hidden=\"true\"\n data-moved-icon=\"\"\n style={{ marginInlineStart: moved.start, inlineSize: moved.size }}\n className={styles.movedIcon({ class: classNames?.icon })}\n >\n {startIcon}\n </span>\n )}\n {geometry.ticks.map((tick, index) => (\n <span\n key={index}\n aria-hidden=\"true\"\n data-slot=\"stop-indicator\"\n data-active={tick.active || undefined}\n style={{ marginInlineStart: `calc(${tick.at} - 2px)` }}\n className={styles.tick({ class: classNames?.stop })}\n />\n ))}\n {geometry.thumbs.map((at, index) => (\n <Thumb\n key={index}\n index={index as 0 | 1}\n at={at}\n state={state}\n trackRef={trackRef}\n name={name}\n label={range ? (index === 0 ? labels.start : labels.end) : undefined}\n errorMessage={errorMessage}\n valueLabel={valueLabel}\n styles={styles}\n classNames={classNames}\n />\n ))}\n </div>\n </div>\n );\n}\n\nfunction Segment({\n segment,\n styles,\n classNames,\n icon,\n}: {\n segment: TrackSegment;\n styles: ReturnType<typeof sliderStyles>;\n classNames: SliderClassNames | undefined;\n icon: ReactNode;\n}) {\n const radius = {\n borderStartStartRadius: segment.startRadius,\n borderEndStartRadius: segment.startRadius,\n borderStartEndRadius: segment.endRadius,\n borderEndEndRadius: segment.endRadius,\n };\n // Inset icons sit 10px inside and show only when the segment fits them (MDC).\n const iconBox = (\n <span\n aria-hidden=\"true\"\n style={{\n inlineSize: iconSizeWhenFits('100%'),\n marginInlineStart: segment.icon === 'start' ? '10px' : 'auto',\n marginInlineEnd: segment.icon === 'end' ? '10px' : undefined,\n }}\n className={styles.icon({ class: classNames?.icon })}\n >\n {icon}\n </span>\n );\n // Stop indicators sit a corner's length in from the track's ends.\n const stop = (at: 'start' | 'end') => (\n <span\n aria-hidden=\"true\"\n data-slot=\"stop-indicator\"\n style={\n at === 'start'\n ? { marginInlineStart: 'calc(var(--m3-slider-corner) - 2px)' }\n : { marginInlineStart: 'auto', marginInlineEnd: 'calc(var(--m3-slider-corner) - 2px)' }\n }\n className={styles.stop({ class: classNames?.stop })}\n />\n );\n return (\n <span\n data-tone={segment.tone}\n data-slot={`${segment.tone}-track`}\n style={{ marginInlineStart: segment.start, inlineSize: segmentLength(segment), ...radius }}\n className={styles.segment({\n class: segment.tone === 'active' ? classNames?.activeTrack : classNames?.inactiveTrack,\n })}\n >\n {segment.stopAtStart && stop('start')}\n {icon != null && iconBox}\n {/* An end icon takes the stop indicator's place. */}\n {segment.stopAtEnd && icon == null && stop('end')}\n </span>\n );\n}\n\n/** Whether each arrow key increases the value (true) or decreases it (false), by direction. */\nconst INCREASE_KEYS: Record<'ltr' | 'rtl', Record<string, boolean>> = {\n ltr: { ArrowRight: true, ArrowUp: true, ArrowLeft: false, ArrowDown: false },\n rtl: { ArrowLeft: true, ArrowUp: true, ArrowRight: false, ArrowDown: false },\n};\n\nfunction Thumb({\n index,\n at,\n state,\n trackRef,\n name,\n label,\n errorMessage,\n valueLabel,\n styles,\n classNames,\n}: {\n index: 0 | 1;\n at: string;\n state: SliderState;\n trackRef: RefObject<HTMLDivElement | null>;\n name?: string;\n label?: string;\n errorMessage?: string;\n valueLabel: boolean;\n styles: ReturnType<typeof sliderStyles>;\n classNames?: SliderClassNames;\n}) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { thumbProps, inputProps, isDragging } = useSliderThumb(\n { index, trackRef, inputRef, name, 'aria-label': label, 'aria-errormessage': errorMessage },\n state,\n );\n // React Aria joins the description's ids into an empty string when there are none.\n const thumbInputProps = {\n ...inputProps,\n 'aria-describedby': inputProps['aria-describedby'] || undefined,\n 'aria-details': inputProps['aria-details'] || undefined,\n };\n const { focusProps, isFocusVisible } = useFocusRing();\n const { direction } = useLocale();\n // M3: \"Space & Arrows — Increase and decrease the value by one interval\", so arrows step\n // by React Aria's page size while Space is held (as Shift + arrows already do).\n const [spaceHeld, setSpaceHeld] = useState(false);\n const intervalKeys = {\n onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === ' ') {\n event.preventDefault();\n setSpaceHeld(true);\n return;\n }\n const increase = INCREASE_KEYS[direction][event.key];\n if (!spaceHeld || increase === undefined) return;\n event.preventDefault();\n event.stopPropagation();\n state.setThumbDragging(index, true);\n if (increase) state.incrementThumb(index, state.pageSize);\n else state.decrementThumb(index, state.pageSize);\n state.setThumbDragging(index, false);\n },\n onKeyUp: (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === ' ') setSpaceHeld(false);\n },\n onBlur: () => setSpaceHeld(false),\n };\n // The handle narrows to 2px while it is pressed or dragged (M3), or keyboard-focused.\n const active = isDragging || isFocusVisible;\n return (\n <>\n <div\n {...thumbProps}\n data-thumb={index}\n data-focus-visible={isFocusVisible || undefined}\n data-dragging={isDragging || undefined}\n style={{ marginInlineStart: `calc(${at} - 2px)` }}\n className={styles.thumb({ class: classNames?.thumb })}\n >\n <span\n data-slot=\"handle\"\n data-active={active || undefined}\n className={styles.handle({ class: classNames?.handle })}\n />\n <VisuallyHidden>\n <input ref={inputRef} {...mergeProps(thumbInputProps, focusProps, intervalKeys)} />\n </VisuallyHidden>\n </div>\n {valueLabel && (isDragging || isFocusVisible) && (\n <span aria-hidden=\"true\" style={{ marginInlineStart: at }} className={styles.labelAnchor()}>\n <span data-slot=\"value-indicator\" className={styles.label({ class: classNames?.label })}>\n {state.getThumbValueLabel(index)}\n </span>\n </span>\n )}\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Slider.js","names":[],"sources":["../../../src/components/slider/Slider.tsx"],"sourcesContent":["'use client';\n\nimport {\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n} from 'react';\nimport {\n mergeProps,\n useFocusRing,\n useLocale,\n useNumberFormatter,\n useObjectRef,\n useSlider,\n useSliderThumb,\n VisuallyHidden,\n} from 'react-aria';\nimport { useSliderState, type SliderState } from 'react-stately';\nimport { DomDirectionLocale } from '../../primitives/DomDirectionLocale';\nimport { cn } from '../../utils/cn';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { splitRootProps, type RootDOMProps } from '../../utils/root-props';\nimport { useTouchTrack } from './use-touch-track';\nimport {\n iconSizeWhenFits,\n movedStartIcon,\n segmentLength,\n trackGeometry,\n type TrackSegment,\n} from './slider-geometry';\nimport { sliderStyles, type SliderOrientation, type SliderSize } from './slider-styles';\n\nexport interface SliderClassNames {\n /** The outer element. */\n root?: string;\n /** The track, holding the active and inactive segments and the stop indicators. */\n track?: string;\n /** Each active track segment (the filled part, from the start or the centre). */\n activeTrack?: string;\n /** Each inactive track segment. */\n inactiveTrack?: string;\n /** The stop indicators: at the track's ends, and at every step with `ticks`. */\n stop?: string;\n /** The inset icons (`leadingIcon`, `trailingIcon`). */\n icon?: string;\n /** Each thumb, the pressable area around its handle. */\n thumb?: string;\n /** The handle bar drawn inside each thumb. */\n handle?: string;\n /** The value indicator above a thumb (`valueLabel`). */\n label?: string;\n}\n\n/** A slider needs a name: `aria-label`, or `aria-labelledby` pointing at a visible label. */\ntype Naming =\n | {\n /** Accessible name when there's no visible label. */\n 'aria-label': string;\n }\n | {\n /** The id of the visible label that names the slider. */\n 'aria-labelledby': string;\n };\n\ninterface SliderCommonProps extends RootDOMProps {\n /** The track's thickness, from XS (a 16px track) to XL. @default \"xs\" */\n size?: SliderSize;\n /**\n * The direction the slider runs. Vertical sliders run bottom to top and default to 240px\n * tall. @default \"horizontal\"\n */\n orientation?: SliderOrientation;\n /** The lowest value. @default 0 */\n min?: number;\n /** The highest value. @default 100 */\n max?: number;\n /** The amount the value changes by in one step. @default 1 */\n step?: number;\n /** Discrete: show a stop indicator at every step (use a coarse `step`). @default false */\n ticks?: boolean;\n /** Turns the slider off: it can't be moved or focused. @default false */\n disabled?: boolean;\n /** Show the value above the thumb while it is dragged or keyboard-focused. @default false */\n valueLabel?: boolean;\n /** Formats the value for the value indicator and assistive tech. */\n formatOptions?: Intl.NumberFormatOptions;\n /** Form field name (one hidden input per thumb). */\n name?: string;\n /** The id of the slider's root, its `group` (an error summary can link to it). */\n id?: string;\n /** Ref to the root `<div>` element. */\n ref?: Ref<HTMLDivElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SliderClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SliderProps = SliderCommonProps &\n Naming & {\n /** The value (controlled). */\n value?: number;\n /** The initial value when uncontrolled. @default min */\n defaultValue?: number;\n /** Called as the value changes, while dragging too. */\n onChange?: (value: number) => void;\n /** Called when the user stops dragging or releases a key. */\n onChangeEnd?: (value: number) => void;\n /**\n * Fill the track from its centre (for values around a midpoint). M3 doesn't use inset\n * icons on centred sliders, so `leadingIcon` and `trailingIcon` are ignored. @default false\n */\n centered?: boolean;\n /**\n * An icon inside the active track at its start (sizes md and up; ignored, with a dev\n * warning, on xs and sm). When the active track is too short for it, it moves to the\n * start of the inactive track.\n */\n leadingIcon?: ReactNode;\n /** An icon inside the inactive track at its end (sizes md and up; ignored on xs and sm). */\n trailingIcon?: ReactNode;\n };\n\nexport interface RangeSliderLabels {\n /** Name of the start thumb. @default \"Minimum\" */\n start: string;\n /** Name of the end thumb. @default \"Maximum\" */\n end: string;\n}\n\nconst DEFAULT_LABELS: RangeSliderLabels = { start: 'Minimum', end: 'Maximum' };\n\nexport type RangeSliderProps = SliderCommonProps &\n Naming & {\n /** The range, as `[start, end]` (controlled). */\n value?: [number, number];\n /** The initial range when uncontrolled. @default [min, max] */\n defaultValue?: [number, number];\n /** Called as the range changes, while dragging too. */\n onChange?: (value: [number, number]) => void;\n /** Called when the user stops dragging or releases a key. */\n onChangeEnd?: (value: [number, number]) => void;\n /** Words the range slider announces (its thumbs' names), with English defaults. */\n labels?: Partial<RangeSliderLabels>;\n };\n\n/**\n * M3 Expressive slider: XS–XL sizes, horizontal or vertical, continuous or discrete with\n * stop indicators, optionally centred, with inset icons and a value indicator. Arrow keys,\n * Page Up / Down and Home / End adjust it (mirrored in RTL).\n *\n * @example\n * <Slider aria-label=\"Volume\" size=\"md\" leadingIcon={<VolumeIcon />} defaultValue={40} />\n */\nexport function Slider(props: SliderProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <SliderRoot {...props} range={false} directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\n/**\n * M3 Expressive range slider: two thumbs selecting a range, with the same sizes,\n * orientations and discrete stops as {@link Slider}.\n *\n * @example\n * <RangeSlider aria-label=\"Price\" defaultValue={[20, 80]} />\n */\nexport function RangeSlider(props: RangeSliderProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <SliderRoot {...props} range directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\ntype RootProps = (SliderProps | RangeSliderProps) & {\n range: boolean;\n directionRef: (element: HTMLElement | null) => void;\n};\n\nfunction SliderRoot(props: RootProps) {\n const {\n size = 'xs',\n orientation = 'horizontal',\n min = 0,\n max = 100,\n step = 1,\n ticks = false,\n disabled = false,\n valueLabel = false,\n formatOptions,\n name,\n id,\n ref,\n className,\n style,\n classNames,\n range,\n directionRef,\n value,\n defaultValue,\n onChange,\n onChangeEnd,\n ...rest\n } = props;\n const {\n centered = false,\n leadingIcon: startIconProp,\n trailingIcon: endIconProp,\n } = props as SliderProps;\n // M3: only M, L and XL standard sliders take inset icons (\"Avoid adding inset icons to XS\n // or S\"; none on centred sliders).\n const iconSize = size === 'md' || size === 'lg' || size === 'xl';\n const startIcon = iconSize && !centered ? startIconProp : undefined;\n const endIcon = iconSize && !centered ? endIconProp : undefined;\n const labels = { ...DEFAULT_LABELS, ...(props as RangeSliderProps).labels };\n // The root is the slider's group: its DOM attributes join the hook's.\n const { root: data, rest: labelling } = splitRootProps(rest);\n // The naming attributes: the name goes to the group and each thumb (React Aria), and the\n // description, details and error message to each thumb's input, which takes focus.\n const {\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n 'aria-errormessage': errorMessage,\n } = labelling as {\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-details'?: string;\n 'aria-errormessage'?: string;\n };\n\n const numberFormatter = useNumberFormatter(formatOptions);\n // React Stately works with arrays; a single slider wraps and unwraps its number.\n const wrap = (v: number | [number, number] | undefined) =>\n v === undefined ? undefined : Array.isArray(v) ? v : [v];\n const unwrap = (v: number[]) => (range ? (v as [number, number]) : v[0]!);\n const ariaProps = {\n id,\n minValue: min,\n maxValue: max,\n step,\n orientation,\n isDisabled: disabled,\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n value: wrap(value),\n defaultValue: wrap(defaultValue) ?? (range ? [min, max] : [min]),\n onChange: onChange && ((v: number[]) => (onChange as (x: unknown) => void)(unwrap(v))),\n onChangeEnd: onChangeEnd && ((v: number[]) => (onChangeEnd as (x: unknown) => void)(unwrap(v))),\n };\n const state = useSliderState({ ...ariaProps, numberFormatter });\n const rootRef = useObjectRef(ref);\n const trackRef = useRef<HTMLDivElement>(null);\n const { groupProps, trackProps } = useSlider(ariaProps, state, trackRef);\n const { direction } = useLocale();\n const count = Math.round((max - min) / step);\n const touchTrackProps = useTouchTrack(trackProps, {\n state,\n trackRef,\n orientation,\n direction,\n disabled,\n discrete: ticks && count > 0,\n });\n\n const tickFractions =\n ticks && count > 0 ? Array.from({ length: count + 1 }, (_, i) => i / count) : [];\n const fractions = state.values.map((_, index) => state.getThumbPercent(index));\n const geometry = trackGeometry({ fractions, ticks: tickFractions, centered: centered && !range });\n const styles = sliderStyles({ size, orientation });\n if (process.env.NODE_ENV !== 'production' && (startIconProp != null || endIconProp != null)) {\n if (centered) {\n console.warn(\n 'Slider: inset icons are not used on centred sliders (M3), so they are ignored.',\n );\n } else if (!iconSize) {\n console.warn('Slider: inset icons need size md, lg or xl (M3), so they are ignored.');\n }\n }\n if (process.env.NODE_ENV !== 'production' && range && orientation === 'vertical') {\n console.warn('RangeSlider: M3 advises against vertical range sliders.');\n }\n const activeSegment = geometry.segments.find((segment) => segment.icon === 'start');\n const moved =\n activeSegment && startIcon != null && movedStartIcon(activeSegment, geometry.thumbs[0]!);\n\n return (\n <div\n {...mergeProps(data, groupProps)}\n ref={(element) => {\n rootRef.current = element;\n directionRef(element);\n }}\n style={style}\n data-tokens={mergeDataTokens('md-comp-slider', data)}\n data-size={size}\n data-orientation={orientation}\n data-disabled={disabled || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div\n {...touchTrackProps}\n ref={trackRef}\n className={styles.track({ class: classNames?.track })}\n >\n {geometry.segments.map((segment, index) => (\n <Segment\n key={index}\n segment={segment}\n styles={styles}\n classNames={classNames}\n icon={\n segment.icon === 'start' ? startIcon : segment.icon === 'end' ? endIcon : undefined\n }\n />\n ))}\n {moved && (\n <span\n aria-hidden=\"true\"\n data-moved-icon=\"\"\n style={{ marginInlineStart: moved.start, inlineSize: moved.size }}\n className={styles.movedIcon({ class: classNames?.icon })}\n >\n {startIcon}\n </span>\n )}\n {geometry.ticks.map((tick, index) => (\n <span\n key={index}\n aria-hidden=\"true\"\n data-slot=\"stop-indicator\"\n data-active={tick.active || undefined}\n style={{ marginInlineStart: `calc(${tick.at} - 2px)` }}\n className={styles.tick({ class: classNames?.stop })}\n />\n ))}\n {geometry.thumbs.map((at, index) => (\n <Thumb\n key={index}\n index={index as 0 | 1}\n at={at}\n state={state}\n trackRef={trackRef}\n name={name}\n groupId={groupProps.id}\n labelledBy={labelledBy}\n label={range ? (index === 0 ? labels.start : labels.end) : undefined}\n errorMessage={errorMessage}\n valueLabel={valueLabel}\n styles={styles}\n classNames={classNames}\n />\n ))}\n </div>\n </div>\n );\n}\n\nfunction Segment({\n segment,\n styles,\n classNames,\n icon,\n}: {\n segment: TrackSegment;\n styles: ReturnType<typeof sliderStyles>;\n classNames: SliderClassNames | undefined;\n icon: ReactNode;\n}) {\n const radius = {\n borderStartStartRadius: segment.startRadius,\n borderEndStartRadius: segment.startRadius,\n borderStartEndRadius: segment.endRadius,\n borderEndEndRadius: segment.endRadius,\n };\n // Inset icons sit 10px inside and show only when the segment fits them (MDC).\n const iconBox = (\n <span\n aria-hidden=\"true\"\n style={{\n inlineSize: iconSizeWhenFits('100%'),\n marginInlineStart: segment.icon === 'start' ? '10px' : 'auto',\n marginInlineEnd: segment.icon === 'end' ? '10px' : undefined,\n }}\n className={styles.icon({ class: classNames?.icon })}\n >\n {icon}\n </span>\n );\n // Stop indicators sit a corner's length in from the track's ends.\n const stop = (at: 'start' | 'end') => (\n <span\n aria-hidden=\"true\"\n data-slot=\"stop-indicator\"\n style={\n at === 'start'\n ? { marginInlineStart: 'calc(var(--m3-slider-corner) - 2px)' }\n : { marginInlineStart: 'auto', marginInlineEnd: 'calc(var(--m3-slider-corner) - 2px)' }\n }\n className={styles.stop({ class: classNames?.stop })}\n />\n );\n return (\n <span\n data-tone={segment.tone}\n data-slot={`${segment.tone}-track`}\n style={{ marginInlineStart: segment.start, inlineSize: segmentLength(segment), ...radius }}\n className={styles.segment({\n class: segment.tone === 'active' ? classNames?.activeTrack : classNames?.inactiveTrack,\n })}\n >\n {segment.stopAtStart && stop('start')}\n {icon != null && iconBox}\n {/* An end icon takes the stop indicator's place. */}\n {segment.stopAtEnd && icon == null && stop('end')}\n </span>\n );\n}\n\n/** Whether each arrow key increases the value (true) or decreases it (false), by direction. */\nconst INCREASE_KEYS: Record<'ltr' | 'rtl', Record<string, boolean>> = {\n ltr: { ArrowRight: true, ArrowUp: true, ArrowLeft: false, ArrowDown: false },\n rtl: { ArrowLeft: true, ArrowUp: true, ArrowRight: false, ArrowDown: false },\n};\n\nfunction Thumb({\n index,\n at,\n state,\n trackRef,\n name,\n groupId,\n labelledBy,\n label,\n errorMessage,\n valueLabel,\n styles,\n classNames,\n}: {\n index: 0 | 1;\n at: string;\n state: SliderState;\n trackRef: RefObject<HTMLDivElement | null>;\n name?: string;\n /** The id of the slider's group, which React Aria names every thumb by. */\n groupId?: string;\n /** The ids that name the group (`aria-labelledby`), when it's named by a visible label. */\n labelledBy?: string;\n label?: string;\n errorMessage?: string;\n valueLabel: boolean;\n styles: ReturnType<typeof sliderStyles>;\n classNames?: SliderClassNames;\n}) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { thumbProps, inputProps, isDragging } = useSliderThumb(\n { index, trackRef, inputRef, name, 'aria-label': label, 'aria-errormessage': errorMessage },\n state,\n );\n // React Aria names each thumb by the group (`aria-labelledby` → the group's id). A name is\n // never followed through a second `aria-labelledby`, so a group named by a visible label would\n // name the thumb by its own content, the value text (\"0%\"): the thumb points at the label\n // itself instead, as the group does. An `aria-label` on the group is read through it as is.\n const thumbLabelledBy = labelledBy?.trim()\n ? inputProps['aria-labelledby']\n ?.split(' ')\n .flatMap((id) => (id === groupId ? labelledBy.trim().split(/\\s+/) : [id]))\n .join(' ')\n : inputProps['aria-labelledby'];\n // React Aria joins the description's ids into an empty string when there are none.\n const thumbInputProps = {\n ...inputProps,\n 'aria-labelledby': thumbLabelledBy,\n 'aria-describedby': inputProps['aria-describedby'] || undefined,\n 'aria-details': inputProps['aria-details'] || undefined,\n };\n const { focusProps, isFocusVisible } = useFocusRing();\n const { direction } = useLocale();\n // M3: \"Space & Arrows — Increase and decrease the value by one interval\", so arrows step\n // by React Aria's page size while Space is held (as Shift + arrows already do).\n const [spaceHeld, setSpaceHeld] = useState(false);\n const intervalKeys = {\n onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === ' ') {\n event.preventDefault();\n setSpaceHeld(true);\n return;\n }\n const increase = INCREASE_KEYS[direction][event.key];\n if (!spaceHeld || increase === undefined) return;\n event.preventDefault();\n event.stopPropagation();\n state.setThumbDragging(index, true);\n if (increase) state.incrementThumb(index, state.pageSize);\n else state.decrementThumb(index, state.pageSize);\n state.setThumbDragging(index, false);\n },\n onKeyUp: (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === ' ') setSpaceHeld(false);\n },\n onBlur: () => setSpaceHeld(false),\n };\n // The handle narrows to 2px while it is pressed or dragged (M3), or keyboard-focused.\n const active = isDragging || isFocusVisible;\n return (\n <>\n <div\n {...thumbProps}\n data-thumb={index}\n data-focus-visible={isFocusVisible || undefined}\n data-dragging={isDragging || undefined}\n style={{ marginInlineStart: `calc(${at} - 2px)` }}\n className={styles.thumb({ class: classNames?.thumb })}\n >\n <span\n data-slot=\"handle\"\n data-active={active || undefined}\n className={styles.handle({ class: classNames?.handle })}\n />\n <VisuallyHidden>\n <input ref={inputRef} {...mergeProps(thumbInputProps, focusProps, intervalKeys)} />\n </VisuallyHidden>\n </div>\n {valueLabel && (isDragging || isFocusVisible) && (\n <span aria-hidden=\"true\" style={{ marginInlineStart: at }} className={styles.labelAnchor()}>\n <span data-slot=\"value-indicator\" className={styles.label({ class: classNames?.label })}>\n {state.getThumbValueLabel(index)}\n </span>\n </span>\n )}\n </>\n );\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<"
|
|
5
|
+
export declare const snackbarTokenSet: ComponentTokenSet<"icon-color" | "container-color" | "container-elevation" | "container-shape" | "supporting-text-color" | "action-label-text-color">;
|
|
6
6
|
//#endregion
|
|
7
7
|
//# sourceMappingURL=snackbar-tokens.d.ts.map
|
|
@@ -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"}
|
|
@@ -4,6 +4,6 @@ import "../../index.js";
|
|
|
4
4
|
/** The tokens of {@link Tooltip}: `--md-comp-plain-tooltip-<part>`. */
|
|
5
5
|
export declare const plainTooltipTokenSet: ComponentTokenSet<"container-color" | "container-shape" | "supporting-text-color">;
|
|
6
6
|
/** The tokens of {@link RichTooltip}: `--md-comp-rich-tooltip-<part>`. */
|
|
7
|
-
export declare const richTooltipTokenSet: ComponentTokenSet<"container-color" | "container-
|
|
7
|
+
export declare const richTooltipTokenSet: ComponentTokenSet<"container-color" | "container-elevation" | "container-shape" | "supporting-text-color" | "subhead-color">;
|
|
8
8
|
//#endregion
|
|
9
9
|
//# sourceMappingURL=tooltip-tokens.d.ts.map
|
|
@@ -15,18 +15,22 @@ const RIPPLE_START = {
|
|
|
15
15
|
* one is still growing or fading. The ripple's animated properties are pinned to their start
|
|
16
16
|
* values with their transitions off and flushed to style, then released: they transition
|
|
17
17
|
* from this press's start to whatever the state layer's rules say (growing if
|
|
18
|
-
* `data-rippling` is on, or once it turns on).
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* `data-rippling` is on, or once it turns on). They're pinned with no duration or delay, not by
|
|
19
|
+
* leaving them out of `transition-property`: a running transition whose property drops out of
|
|
20
|
+
* the list is cancelled and jumps to its end, as did a corner morph on a quick second press
|
|
21
|
+
* and a width an app animates through `className` (2026-11-10). A running transition keeps its
|
|
22
|
+
* own timing when the durations change, so nothing else is touched.
|
|
21
23
|
*/
|
|
22
24
|
function restartRipple(element) {
|
|
23
25
|
const { style } = element;
|
|
24
|
-
const
|
|
25
|
-
style.
|
|
26
|
+
const { transitionDuration, transitionDelay } = style;
|
|
27
|
+
style.transitionDuration = "0s";
|
|
28
|
+
style.transitionDelay = "0s";
|
|
26
29
|
for (const [name, value] of Object.entries(RIPPLE_START)) style.setProperty(name, value);
|
|
27
30
|
getComputedStyle(element).getPropertyValue("--m3-ripple-x");
|
|
28
31
|
for (const name of Object.keys(RIPPLE_START)) style.removeProperty(name);
|
|
29
|
-
style.
|
|
32
|
+
style.transitionDuration = transitionDuration;
|
|
33
|
+
style.transitionDelay = transitionDelay;
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
32
36
|
* Positions a new ripple at the press point (Compose `RippleAnimation`): it grows from 30%
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-m3-interaction.js","names":[],"sources":["../../src/primitives/use-m3-interaction.ts"],"sourcesContent":["'use client';\n\nimport { mergeProps, useFocusRing, useHover, usePress, type PressEvents } from 'react-aria';\nimport {\n useEffect,\n useState,\n type DOMAttributes,\n type KeyboardEvent,\n type PointerEvent,\n type RefObject,\n} from 'react';\n\nexport interface M3InteractionOptions extends PressEvents {\n isDisabled?: boolean;\n isSelected?: boolean;\n isDragged?: boolean;\n /**\n * Pressed state from another React Aria hook (e.g. `useButton`). When provided,\n * this hook does not attach its own press handling.\n */\n isPressed?: boolean;\n /** Treat focus inside the element as focus on the element (e.g. text fields). */\n within?: boolean;\n /** Whether the element is a text input; focus is then always visible. */\n isTextInput?: boolean;\n autoFocus?: boolean;\n}\n\nexport interface M3InteractionState {\n isHovered: boolean;\n isPressed: boolean;\n isFocused: boolean;\n isFocusVisible: boolean;\n}\n\n/** `data-*` attributes read by the `state-layer` and `focus-ring` utilities. */\nexport interface M3InteractionDataAttributes {\n 'data-hovered'?: true;\n 'data-pressed'?: true;\n /** The ripple is showing: from press start until release, but at least 225ms. */\n 'data-rippling'?: true;\n 'data-focused'?: true;\n 'data-focus-visible'?: true;\n 'data-disabled'?: true;\n 'data-selected'?: true;\n 'data-dragged'?: true;\n}\n\nexport interface M3InteractionResult {\n /** Event handlers to spread on the interactive element. */\n interactionProps: DOMAttributes<HTMLElement>;\n dataAttributes: M3InteractionDataAttributes;\n state: M3InteractionState;\n}\n\nconst flag = (value: boolean | undefined): true | undefined => (value ? true : undefined);\n\n/** Compose `RippleAnimation`: the radius grows (and the centre travels) over 225ms. */\nexport const RIPPLE_GROW_MS = 225;\n\n/** The ripple's animated properties, pinned at the start of a new ripple. */\nconst RIPPLE_START = {\n '--m3-ripple-opacity': '0',\n '--m3-ripple-radius': 'var(--m3-ripple-start, 0px)',\n '--m3-ripple-x': 'var(--m3-ripple-origin-x, 50%)',\n '--m3-ripple-y': 'var(--m3-ripple-origin-y, 50%)',\n} as const;\n\n/**\n * Starts a fresh ripple on `element`, as Compose does for every press, even while the last\n * one is still growing or fading. The ripple's animated properties are pinned to their start\n * values with their transitions off and flushed to style, then released: they transition\n * from this press's start to whatever the state layer's rules say (growing if\n * `data-rippling` is on, or once it turns on). Only the ripple's transitions are switched\n * off: the component's own (`--m3-transition-property`, such as a running corner morph) keep\n * running, so a quick second press doesn't make the corners jump.\n */\nexport function restartRipple(element: HTMLElement) {\n const { style } = element;\n const transitionProperty = style.transitionProperty;\n style.transitionProperty = 'var(--m3-transition-property, --m3-none)';\n for (const [name, value] of Object.entries(RIPPLE_START)) style.setProperty(name, value);\n getComputedStyle(element).getPropertyValue('--m3-ripple-x');\n for (const name of Object.keys(RIPPLE_START)) style.removeProperty(name);\n style.transitionProperty = transitionProperty;\n}\n\n/**\n * Positions a new ripple at the press point (Compose `RippleAnimation`): it grows from 30%\n * of the element's longer side to half its diagonal plus 10px while its centre moves to the\n * middle. Without a point (a key press) it starts in the middle. For a state layer on a\n * child of the pressed element (a navigation item's pill), pass that child.\n */\nexport function setRippleOrigin(element: HTMLElement, clientX?: number, clientY?: number) {\n const rect = element.getBoundingClientRect();\n const x = clientX === undefined ? rect.width / 2 : clientX - rect.left;\n const y = clientY === undefined ? rect.height / 2 : clientY - rect.top;\n const { style } = element;\n style.setProperty('--m3-ripple-origin-x', `${x}px`);\n style.setProperty('--m3-ripple-origin-y', `${y}px`);\n style.setProperty('--m3-ripple-center-x', `${rect.width / 2}px`);\n style.setProperty('--m3-ripple-center-y', `${rect.height / 2}px`);\n style.setProperty('--m3-ripple-start', `${Math.max(rect.width, rect.height) * 0.3}px`);\n style.setProperty(\n '--m3-ripple-size',\n `${Math.ceil(Math.hypot(rect.width, rect.height) / 2 + 10)}px`,\n );\n restartRipple(element);\n}\n\n/**\n * Flags a press that starts while the element's corner morph is still running\n * (`data-rapid-press`). M3's buttons note that the web can use \"a modified motion curve to\n * avoid resonant effects from overlapping animations\": the `container-motion` utility then\n * morphs on the fast effects spring, which doesn't overshoot, instead of the fast spatial\n * one. The attribute is set on the element directly, before React renders the press, so\n * the morph that press starts already uses the modified curve. Call it before anything\n * that resets the element's transitions (such as `restartRipple`).\n */\nexport function markRapidPress(element: HTMLElement) {\n const morphing =\n typeof element.getAnimations === 'function' &&\n element\n .getAnimations()\n .some(\n (animation) =>\n animation instanceof CSSTransition &&\n animation.transitionProperty.endsWith('radius') &&\n animation.playState === 'running',\n );\n element.toggleAttribute('data-rapid-press', morphing);\n}\n\n/**\n * Whether a ripple should be showing. Compose always finishes a ripple's 225ms growth\n * before fading it out, so a quick tap still reaches the edges: this stays true from press\n * start until release, but for at least 225ms.\n */\nexport function useRippleHold(isPressed: boolean): boolean {\n const [wasPressed, setWasPressed] = useState(isPressed);\n const [held, setHeld] = useState(false);\n const [presses, setPresses] = useState(0);\n // Adjust state while rendering when a press starts (no effect, no extra paint).\n if (isPressed !== wasPressed) {\n setWasPressed(isPressed);\n if (isPressed) {\n setHeld(true);\n setPresses((n) => n + 1);\n }\n }\n useEffect(() => {\n if (!held) return;\n const timer = setTimeout(() => setHeld(false), RIPPLE_GROW_MS);\n return () => clearTimeout(timer);\n }, [held, presses]);\n return isPressed || held;\n}\n\n/**\n * M3 interaction states for any element: hover, press, focus-visible, selected,\n * dragged and disabled, exposed as `data-*` attributes, plus the ripple origin.\n * Pair with the `state-layer` and `focus-ring` utilities, which paint the state\n * layer and ripple as background layers on the element itself (no child nodes).\n */\nexport function useM3Interaction(\n options: M3InteractionOptions,\n ref: RefObject<HTMLElement | null>,\n): M3InteractionResult {\n const {\n isDisabled,\n isSelected,\n isDragged,\n within,\n isTextInput,\n autoFocus,\n onPress,\n onPressStart,\n onPressEnd,\n onPressChange,\n onPressUp,\n onClick,\n } = options;\n const hasExternalPress = options.isPressed !== undefined;\n\n // Only `usePress`'s own options: it spreads any others onto the props it returns, so this\n // hook's options (`isDragged`, `within` …) would reach the DOM as unknown attributes.\n const { pressProps, isPressed: isOwnPressed } = usePress({\n onPress,\n onPressStart,\n onPressEnd,\n onPressChange,\n onPressUp,\n onClick,\n ref,\n isDisabled: isDisabled || hasExternalPress,\n });\n const { hoverProps, isHovered } = useHover({ isDisabled });\n const { focusProps, isFocused, isFocusVisible } = useFocusRing({\n within,\n isTextInput,\n autoFocus,\n });\n\n const isPressed = hasExternalPress ? Boolean(options.isPressed) : isOwnPressed;\n const isRippling = useRippleHold(isPressed && !isDisabled);\n\n const rippleProps: DOMAttributes<HTMLElement> = {\n onPointerDown: (event: PointerEvent<HTMLElement>) => {\n if (!isDisabled && event.button === 0) {\n markRapidPress(event.currentTarget);\n setRippleOrigin(event.currentTarget, event.clientX, event.clientY);\n }\n },\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => {\n if (!isDisabled && (event.key === 'Enter' || event.key === ' ')) {\n markRapidPress(event.currentTarget);\n setRippleOrigin(event.currentTarget);\n }\n },\n };\n\n return {\n interactionProps: mergeProps(\n hasExternalPress ? {} : pressProps,\n hoverProps,\n focusProps,\n rippleProps,\n ),\n dataAttributes: {\n 'data-hovered': flag(isHovered && !isDisabled),\n 'data-pressed': flag(isPressed && !isDisabled),\n 'data-rippling': flag(isRippling),\n 'data-focused': flag(isFocused),\n 'data-focus-visible': flag(isFocusVisible),\n 'data-disabled': flag(isDisabled),\n 'data-selected': flag(isSelected),\n 'data-dragged': flag(isDragged),\n },\n state: { isHovered, isPressed, isFocused, isFocusVisible },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-m3-interaction.js","names":[],"sources":["../../src/primitives/use-m3-interaction.ts"],"sourcesContent":["'use client';\n\nimport { mergeProps, useFocusRing, useHover, usePress, type PressEvents } from 'react-aria';\nimport {\n useEffect,\n useState,\n type DOMAttributes,\n type KeyboardEvent,\n type PointerEvent,\n type RefObject,\n} from 'react';\n\nexport interface M3InteractionOptions extends PressEvents {\n isDisabled?: boolean;\n isSelected?: boolean;\n isDragged?: boolean;\n /**\n * Pressed state from another React Aria hook (e.g. `useButton`). When provided,\n * this hook does not attach its own press handling.\n */\n isPressed?: boolean;\n /** Treat focus inside the element as focus on the element (e.g. text fields). */\n within?: boolean;\n /** Whether the element is a text input; focus is then always visible. */\n isTextInput?: boolean;\n autoFocus?: boolean;\n}\n\nexport interface M3InteractionState {\n isHovered: boolean;\n isPressed: boolean;\n isFocused: boolean;\n isFocusVisible: boolean;\n}\n\n/** `data-*` attributes read by the `state-layer` and `focus-ring` utilities. */\nexport interface M3InteractionDataAttributes {\n 'data-hovered'?: true;\n 'data-pressed'?: true;\n /** The ripple is showing: from press start until release, but at least 225ms. */\n 'data-rippling'?: true;\n 'data-focused'?: true;\n 'data-focus-visible'?: true;\n 'data-disabled'?: true;\n 'data-selected'?: true;\n 'data-dragged'?: true;\n}\n\nexport interface M3InteractionResult {\n /** Event handlers to spread on the interactive element. */\n interactionProps: DOMAttributes<HTMLElement>;\n dataAttributes: M3InteractionDataAttributes;\n state: M3InteractionState;\n}\n\nconst flag = (value: boolean | undefined): true | undefined => (value ? true : undefined);\n\n/** Compose `RippleAnimation`: the radius grows (and the centre travels) over 225ms. */\nexport const RIPPLE_GROW_MS = 225;\n\n/** The ripple's animated properties, pinned at the start of a new ripple. */\nconst RIPPLE_START = {\n '--m3-ripple-opacity': '0',\n '--m3-ripple-radius': 'var(--m3-ripple-start, 0px)',\n '--m3-ripple-x': 'var(--m3-ripple-origin-x, 50%)',\n '--m3-ripple-y': 'var(--m3-ripple-origin-y, 50%)',\n} as const;\n\n/**\n * Starts a fresh ripple on `element`, as Compose does for every press, even while the last\n * one is still growing or fading. The ripple's animated properties are pinned to their start\n * values with their transitions off and flushed to style, then released: they transition\n * from this press's start to whatever the state layer's rules say (growing if\n * `data-rippling` is on, or once it turns on). They're pinned with no duration or delay, not by\n * leaving them out of `transition-property`: a running transition whose property drops out of\n * the list is cancelled and jumps to its end, as did a corner morph on a quick second press\n * and a width an app animates through `className` (2026-11-10). A running transition keeps its\n * own timing when the durations change, so nothing else is touched.\n */\nexport function restartRipple(element: HTMLElement) {\n const { style } = element;\n const { transitionDuration, transitionDelay } = style;\n style.transitionDuration = '0s';\n style.transitionDelay = '0s';\n for (const [name, value] of Object.entries(RIPPLE_START)) style.setProperty(name, value);\n getComputedStyle(element).getPropertyValue('--m3-ripple-x');\n for (const name of Object.keys(RIPPLE_START)) style.removeProperty(name);\n style.transitionDuration = transitionDuration;\n style.transitionDelay = transitionDelay;\n}\n\n/**\n * Positions a new ripple at the press point (Compose `RippleAnimation`): it grows from 30%\n * of the element's longer side to half its diagonal plus 10px while its centre moves to the\n * middle. Without a point (a key press) it starts in the middle. For a state layer on a\n * child of the pressed element (a navigation item's pill), pass that child.\n */\nexport function setRippleOrigin(element: HTMLElement, clientX?: number, clientY?: number) {\n const rect = element.getBoundingClientRect();\n const x = clientX === undefined ? rect.width / 2 : clientX - rect.left;\n const y = clientY === undefined ? rect.height / 2 : clientY - rect.top;\n const { style } = element;\n style.setProperty('--m3-ripple-origin-x', `${x}px`);\n style.setProperty('--m3-ripple-origin-y', `${y}px`);\n style.setProperty('--m3-ripple-center-x', `${rect.width / 2}px`);\n style.setProperty('--m3-ripple-center-y', `${rect.height / 2}px`);\n style.setProperty('--m3-ripple-start', `${Math.max(rect.width, rect.height) * 0.3}px`);\n style.setProperty(\n '--m3-ripple-size',\n `${Math.ceil(Math.hypot(rect.width, rect.height) / 2 + 10)}px`,\n );\n restartRipple(element);\n}\n\n/**\n * Flags a press that starts while the element's corner morph is still running\n * (`data-rapid-press`). M3's buttons note that the web can use \"a modified motion curve to\n * avoid resonant effects from overlapping animations\": the `container-motion` utility then\n * morphs on the fast effects spring, which doesn't overshoot, instead of the fast spatial\n * one. The attribute is set on the element directly, before React renders the press, so\n * the morph that press starts already uses the modified curve. Call it before anything\n * that resets the element's transitions (such as `restartRipple`).\n */\nexport function markRapidPress(element: HTMLElement) {\n const morphing =\n typeof element.getAnimations === 'function' &&\n element\n .getAnimations()\n .some(\n (animation) =>\n animation instanceof CSSTransition &&\n animation.transitionProperty.endsWith('radius') &&\n animation.playState === 'running',\n );\n element.toggleAttribute('data-rapid-press', morphing);\n}\n\n/**\n * Whether a ripple should be showing. Compose always finishes a ripple's 225ms growth\n * before fading it out, so a quick tap still reaches the edges: this stays true from press\n * start until release, but for at least 225ms.\n */\nexport function useRippleHold(isPressed: boolean): boolean {\n const [wasPressed, setWasPressed] = useState(isPressed);\n const [held, setHeld] = useState(false);\n const [presses, setPresses] = useState(0);\n // Adjust state while rendering when a press starts (no effect, no extra paint).\n if (isPressed !== wasPressed) {\n setWasPressed(isPressed);\n if (isPressed) {\n setHeld(true);\n setPresses((n) => n + 1);\n }\n }\n useEffect(() => {\n if (!held) return;\n const timer = setTimeout(() => setHeld(false), RIPPLE_GROW_MS);\n return () => clearTimeout(timer);\n }, [held, presses]);\n return isPressed || held;\n}\n\n/**\n * M3 interaction states for any element: hover, press, focus-visible, selected,\n * dragged and disabled, exposed as `data-*` attributes, plus the ripple origin.\n * Pair with the `state-layer` and `focus-ring` utilities, which paint the state\n * layer and ripple as background layers on the element itself (no child nodes).\n */\nexport function useM3Interaction(\n options: M3InteractionOptions,\n ref: RefObject<HTMLElement | null>,\n): M3InteractionResult {\n const {\n isDisabled,\n isSelected,\n isDragged,\n within,\n isTextInput,\n autoFocus,\n onPress,\n onPressStart,\n onPressEnd,\n onPressChange,\n onPressUp,\n onClick,\n } = options;\n const hasExternalPress = options.isPressed !== undefined;\n\n // Only `usePress`'s own options: it spreads any others onto the props it returns, so this\n // hook's options (`isDragged`, `within` …) would reach the DOM as unknown attributes.\n const { pressProps, isPressed: isOwnPressed } = usePress({\n onPress,\n onPressStart,\n onPressEnd,\n onPressChange,\n onPressUp,\n onClick,\n ref,\n isDisabled: isDisabled || hasExternalPress,\n });\n const { hoverProps, isHovered } = useHover({ isDisabled });\n const { focusProps, isFocused, isFocusVisible } = useFocusRing({\n within,\n isTextInput,\n autoFocus,\n });\n\n const isPressed = hasExternalPress ? Boolean(options.isPressed) : isOwnPressed;\n const isRippling = useRippleHold(isPressed && !isDisabled);\n\n const rippleProps: DOMAttributes<HTMLElement> = {\n onPointerDown: (event: PointerEvent<HTMLElement>) => {\n if (!isDisabled && event.button === 0) {\n markRapidPress(event.currentTarget);\n setRippleOrigin(event.currentTarget, event.clientX, event.clientY);\n }\n },\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => {\n if (!isDisabled && (event.key === 'Enter' || event.key === ' ')) {\n markRapidPress(event.currentTarget);\n setRippleOrigin(event.currentTarget);\n }\n },\n };\n\n return {\n interactionProps: mergeProps(\n hasExternalPress ? {} : pressProps,\n hoverProps,\n focusProps,\n rippleProps,\n ),\n dataAttributes: {\n 'data-hovered': flag(isHovered && !isDisabled),\n 'data-pressed': flag(isPressed && !isDisabled),\n 'data-rippling': flag(isRippling),\n 'data-focused': flag(isFocused),\n 'data-focus-visible': flag(isFocusVisible),\n 'data-disabled': flag(isDisabled),\n 'data-selected': flag(isSelected),\n 'data-dragged': flag(isDragged),\n },\n state: { isHovered, isPressed, isFocused, isFocusVisible },\n };\n}\n"],"mappings":"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"}
|
|
@@ -147,18 +147,30 @@
|
|
|
147
147
|
transparent
|
|
148
148
|
)
|
|
149
149
|
);
|
|
150
|
-
/*
|
|
150
|
+
/*
|
|
151
|
+
* After the ripple: fade it out, then reset its radius and centre once it is invisible. The
|
|
152
|
+
* states below change only these variables, never the transition itself, so an app's own
|
|
153
|
+
* transition through `className` (one class, which wins over this one at rest) keeps its
|
|
154
|
+
* timing while the element is pressed too (2026-11-10).
|
|
155
|
+
*/
|
|
156
|
+
--m3-ripple-fade-duration: 150ms;
|
|
157
|
+
--m3-ripple-grow-duration: 0s;
|
|
158
|
+
--m3-ripple-grow-delay: 150ms;
|
|
159
|
+
--m3-ripple-grow-easing: linear;
|
|
151
160
|
/* Components append their own transitions through the --m3-transition-* lists. */
|
|
152
161
|
transition-property:
|
|
153
162
|
--m3-state-opacity, --m3-ripple-opacity, --m3-ripple-radius, --m3-ripple-x, --m3-ripple-y,
|
|
154
163
|
var(--m3-transition-property, --m3-none);
|
|
155
164
|
transition-duration:
|
|
156
|
-
var(--md-sys-motion-spring-effects-fast-duration),
|
|
165
|
+
var(--md-sys-motion-spring-effects-fast-duration), var(--m3-ripple-fade-duration),
|
|
166
|
+
var(--m3-ripple-grow-duration), var(--m3-ripple-grow-duration), var(--m3-ripple-grow-duration),
|
|
157
167
|
var(--m3-transition-duration, 0s);
|
|
158
|
-
transition-delay:
|
|
168
|
+
transition-delay:
|
|
169
|
+
0s, 0s, var(--m3-ripple-grow-delay), var(--m3-ripple-grow-delay), var(--m3-ripple-grow-delay),
|
|
170
|
+
var(--m3-transition-delay, 0s);
|
|
159
171
|
transition-timing-function:
|
|
160
|
-
var(--md-sys-motion-spring-effects-fast-easing), linear,
|
|
161
|
-
var(--m3-transition-easing, linear);
|
|
172
|
+
var(--md-sys-motion-spring-effects-fast-easing), linear, var(--m3-ripple-grow-easing), linear,
|
|
173
|
+
linear, var(--m3-transition-easing, linear);
|
|
162
174
|
|
|
163
175
|
&[data-hovered] {
|
|
164
176
|
--m3-state-opacity: var(--md-sys-state-hover-state-layer-opacity);
|
|
@@ -181,13 +193,10 @@
|
|
|
181
193
|
--m3-ripple-radius: var(--m3-ripple-size, 0px);
|
|
182
194
|
--m3-ripple-x: var(--m3-ripple-center-x, 50%);
|
|
183
195
|
--m3-ripple-y: var(--m3-ripple-center-y, 50%);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
transition-timing-function:
|
|
189
|
-
var(--md-sys-motion-spring-effects-fast-easing), linear, cubic-bezier(0.4, 0, 0.2, 1), linear,
|
|
190
|
-
linear, var(--m3-transition-easing, linear);
|
|
196
|
+
--m3-ripple-fade-duration: 75ms;
|
|
197
|
+
--m3-ripple-grow-duration: 225ms;
|
|
198
|
+
--m3-ripple-grow-delay: 0s;
|
|
199
|
+
--m3-ripple-grow-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
191
200
|
}
|
|
192
201
|
&[data-disabled] {
|
|
193
202
|
--m3-state-opacity: 0;
|
|
@@ -197,9 +206,7 @@
|
|
|
197
206
|
/* Reduced motion: the ripple appears at full size where it ends, with no growth or travel. */
|
|
198
207
|
@media (prefers-reduced-motion: reduce) {
|
|
199
208
|
&[data-rippling] {
|
|
200
|
-
|
|
201
|
-
var(--md-sys-motion-spring-effects-fast-duration), 75ms, 0s, 0s, 0s,
|
|
202
|
-
var(--m3-transition-duration, 0s);
|
|
209
|
+
--m3-ripple-grow-duration: 0s;
|
|
203
210
|
}
|
|
204
211
|
}
|
|
205
212
|
}
|