@windmorph/react 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/autocomplete/Autocomplete.d.ts +4 -1
- package/dist/components/autocomplete/Autocomplete.js +2 -4
- package/dist/components/autocomplete/Autocomplete.js.map +1 -1
- package/dist/components/badge/Badge.d.ts +5 -2
- package/dist/components/badge/Badge.js +7 -3
- package/dist/components/badge/Badge.js.map +1 -1
- package/dist/components/button/Button.d.ts +4 -2
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/button/ButtonContext.d.ts +1 -1
- package/dist/components/button/button-pending.js +2 -4
- package/dist/components/button/button-pending.js.map +1 -1
- package/dist/{composites → components}/button-group/ButtonGroup.d.ts +4 -4
- package/dist/{composites → components}/button-group/ButtonGroup.js +9 -7
- package/dist/components/button-group/ButtonGroup.js.map +1 -0
- package/dist/{composites → components}/button-group/button-group-styles.d.ts +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js +1 -1
- package/dist/{composites → components}/button-group/button-group-styles.js.map +1 -1
- package/dist/{composites → components}/button-group/lazy-overflow-menu.js +2 -2
- package/dist/components/button-group/lazy-overflow-menu.js.map +1 -0
- package/dist/{composites → components}/button-group/overflow-menu-list.js +2 -2
- package/dist/components/button-group/overflow-menu-list.js.map +1 -0
- package/dist/{composites → components}/button-group/use-overflow.js +1 -1
- package/dist/{composites → components}/button-group/use-overflow.js.map +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js +1 -1
- package/dist/{composites → components}/button-group/use-press-expansion.js.map +1 -1
- package/dist/components/carousel/Carousel.d.ts +4 -1
- package/dist/components/carousel/Carousel.js +2 -4
- package/dist/components/carousel/Carousel.js.map +1 -1
- package/dist/components/checkbox/checkbox-styles.d.ts +3 -3
- package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
- package/dist/components/chip/Chip.d.ts +8 -2
- package/dist/components/chip/Chip.js +3 -8
- package/dist/components/chip/Chip.js.map +1 -1
- package/dist/components/date-picker/DateField.d.ts +4 -1
- package/dist/components/date-picker/DateField.js +3 -1
- package/dist/components/date-picker/DateField.js.map +1 -1
- package/dist/components/date-picker/DatePicker.d.ts +4 -1
- package/dist/components/date-picker/DatePicker.js +5 -2
- package/dist/components/date-picker/DatePicker.js.map +1 -1
- package/dist/components/date-picker/PickerDialog.d.ts +4 -1
- package/dist/components/date-picker/PickerDialog.js +3 -1
- package/dist/components/date-picker/PickerDialog.js.map +1 -1
- package/dist/components/date-picker/date-picker-styles.d.ts +15 -15
- package/dist/components/dialog/Dialog.d.ts +5 -2
- package/dist/components/dialog/Dialog.js +4 -2
- package/dist/components/dialog/Dialog.js.map +1 -1
- package/dist/components/icon-button/IconButton.d.ts +4 -2
- package/dist/components/icon-button/IconButton.js.map +1 -1
- package/dist/components/labels/LabelsProvider.d.ts +35 -0
- package/dist/components/labels/LabelsProvider.js +47 -0
- package/dist/components/labels/LabelsProvider.js.map +1 -0
- package/dist/components/labels/use-labels.d.ts +16 -0
- package/dist/components/labels/use-labels.js +42 -0
- package/dist/components/labels/use-labels.js.map +1 -0
- package/dist/components/menu/Menu.d.ts +4 -1
- package/dist/components/menu/Menu.js +7 -10
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/menu/menu-styles.js +12 -12
- package/dist/components/menu/menu-styles.js.map +1 -1
- package/dist/components/menu/menu-tokens.js.map +1 -1
- package/dist/components/navigation/NavItem.d.ts +1 -1
- package/dist/components/navigation/NavItem.js +9 -6
- package/dist/components/navigation/NavItem.js.map +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.d.ts +1 -1
- package/dist/components/pull-to-refresh/PullToRefresh.js +2 -4
- package/dist/components/pull-to-refresh/PullToRefresh.js.map +1 -1
- package/dist/components/search/SearchField.d.ts +4 -1
- package/dist/components/search/SearchField.js +2 -4
- package/dist/components/search/SearchField.js.map +1 -1
- package/dist/components/select/Select.d.ts +4 -1
- package/dist/components/select/Select.js +2 -4
- package/dist/components/select/Select.js.map +1 -1
- package/dist/components/sheet/BottomSheet.d.ts +4 -1
- package/dist/components/sheet/BottomSheet.js +2 -4
- package/dist/components/sheet/BottomSheet.js.map +1 -1
- package/dist/components/sheet/SideSheet.d.ts +4 -1
- package/dist/components/sheet/SideSheet.js +2 -4
- package/dist/components/sheet/SideSheet.js.map +1 -1
- package/dist/components/slider/Slider.d.ts +4 -1
- package/dist/components/slider/Slider.js +2 -4
- package/dist/components/slider/Slider.js.map +1 -1
- package/dist/components/snackbar/Snackbar.d.ts +8 -2
- package/dist/components/snackbar/Snackbar.js +2 -4
- package/dist/components/snackbar/Snackbar.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
- package/dist/components/split-button/SplitButton.d.ts +4 -1
- package/dist/components/split-button/SplitButton.js +4 -2
- package/dist/components/split-button/SplitButton.js.map +1 -1
- package/dist/components/switch/switch-styles.d.ts +3 -3
- package/dist/components/text-field/TextField.d.ts +4 -1
- package/dist/components/text-field/TextField.js +2 -4
- package/dist/components/text-field/TextField.js.map +1 -1
- package/dist/components/time-picker/TimeField.d.ts +24 -2
- package/dist/components/time-picker/TimeField.js +40 -12
- package/dist/components/time-picker/TimeField.js.map +1 -1
- package/dist/components/time-picker/TimeFieldList.js +95 -0
- package/dist/components/time-picker/TimeFieldList.js.map +1 -0
- package/dist/components/time-picker/TimePicker.d.ts +9 -5
- package/dist/components/time-picker/TimePicker.js +5 -2
- package/dist/components/time-picker/TimePicker.js.map +1 -1
- package/dist/components/time-picker/time-list.js +33 -0
- package/dist/components/time-picker/time-list.js.map +1 -0
- package/dist/components/tooltip/Tooltip.d.ts +1 -1
- package/dist/index.d.ts +40 -38
- package/dist/index.js +4 -3
- package/dist/primitives.d.ts +7 -4
- package/dist/primitives.js +4 -1
- package/dist/styles.compiled.css +1 -1
- package/dist/utils/use-scroll-keys.d.ts +29 -0
- package/dist/utils/use-scroll-keys.js +66 -0
- package/dist/utils/use-scroll-keys.js.map +1 -0
- package/dist/utils/use-scroll-restoration.d.ts +43 -0
- package/dist/utils/use-scroll-restoration.js +173 -0
- package/dist/utils/use-scroll-restoration.js.map +1 -0
- package/dist/windmorph-labels.d.ts +101 -0
- package/package.json +1 -1
- package/dist/composites/button-group/ButtonGroup.js.map +0 -1
- package/dist/composites/button-group/lazy-overflow-menu.js.map +0 -1
- package/dist/composites/button-group/overflow-menu-list.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"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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"}
|
|
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 { useLabels } from '../labels/use-labels';\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 /**\n * Words the range slider announces (its thumbs' names), with English defaults.\n * A {@link LabelsProvider} sets them for the whole app.\n */\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 = useLabels('RangeSlider', 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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"}
|
|
@@ -31,7 +31,10 @@ interface SnackbarOwnProps {
|
|
|
31
31
|
* button. @default onDismiss
|
|
32
32
|
*/
|
|
33
33
|
onEscape?: () => void;
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Words the snackbar announces, with English defaults. A {@link LabelsProvider} sets them for the
|
|
36
|
+
* whole app.
|
|
37
|
+
*/
|
|
35
38
|
labels?: Partial<SnackbarLabels>;
|
|
36
39
|
/** Put the actions under the message, for long action labels. @default false */
|
|
37
40
|
actionOnNewLine?: boolean;
|
|
@@ -58,7 +61,10 @@ export interface SnackbarHostProps extends Omit<ComponentPropsWithRef<'div'>, 'c
|
|
|
58
61
|
state: SnackbarHostState;
|
|
59
62
|
/** Renders each snackbar; defaults to {@link Snackbar}. */
|
|
60
63
|
snackbar?: (data: SnackbarData) => React.ReactNode;
|
|
61
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Words each default snackbar announces, with English defaults. A {@link LabelsProvider} sets
|
|
66
|
+
* them for the whole app.
|
|
67
|
+
*/
|
|
62
68
|
labels?: Partial<SnackbarLabels>;
|
|
63
69
|
/**
|
|
64
70
|
* The key combination that moves focus to a snackbar with actions, as the spec asks for on
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
3
4
|
import { mergeDataTokens } from "../../utils/merge-data-tokens.js";
|
|
4
5
|
import { usePresence } from "../../primitives/use-presence.js";
|
|
5
6
|
import { announcePolite } from "../../utils/announce.js";
|
|
@@ -40,10 +41,7 @@ function Snackbar({ children, actionLabel, onAction, onDismiss, onEscape = onDis
|
|
|
40
41
|
newLine: actionOnNewLine && actionLabel !== void 0,
|
|
41
42
|
dismissible: onDismiss !== void 0
|
|
42
43
|
});
|
|
43
|
-
const labels =
|
|
44
|
-
...DEFAULT_LABELS,
|
|
45
|
-
...labelsProp
|
|
46
|
-
};
|
|
44
|
+
const labels = useLabels("Snackbar", DEFAULT_LABELS, labelsProp);
|
|
47
45
|
const hasActions = actionLabel !== void 0 || onDismiss !== void 0;
|
|
48
46
|
const inHost = useContext(InSnackbarHost);
|
|
49
47
|
const messageRef = useRef(null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Snackbar.js","names":[],"sources":["../../../src/components/snackbar/Snackbar.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n useSyncExternalStore,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { usePresence } from '../../primitives/use-presence';\nimport { announcePolite, LIVE_REGION_DELAY } from '../../utils/announce';\nimport { cn } from '../../utils/cn';\nimport { firstTabbable, tabbables } from '../../utils/focus';\nimport { matchesShortcut } from '../../utils/shortcut';\nimport { Button } from '../button/Button';\nimport { IconButton } from '../icon-button/IconButton';\nimport { durationMillis, SnackbarHostState, type SnackbarData } from './snackbar-state';\nimport { snackbarHostStyles, snackbarStyles } from './snackbar-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Whether a snackbar is inside a {@link SnackbarHost}, whose live region announces it. */\nconst InSnackbarHost = createContext(false);\n\n/** Material Symbols \"close\" (Apache-2.0). */\nfunction CloseIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z\" />\n </svg>\n );\n}\n\nexport interface SnackbarClassNames {\n /** The snackbar's container. */\n root?: string;\n /** The message text. */\n message?: string;\n /** The row holding the action and dismiss buttons. */\n actions?: string;\n /** The action button. */\n action?: string;\n /** The dismiss (×) button. */\n dismiss?: string;\n}\n\nexport interface SnackbarLabels {\n /** Name of the dismiss (×) button. @default \"Dismiss\" */\n dismiss: string;\n}\n\nconst DEFAULT_LABELS: SnackbarLabels = { dismiss: 'Dismiss' };\n\ninterface SnackbarOwnProps {\n /** The message. */\n children: React.ReactNode;\n /** Label of the action button. */\n actionLabel?: string;\n /** Called when the action button is pressed. */\n onAction?: () => void;\n /** Shows a dismiss (×) button that calls this. Escape calls it too. */\n onDismiss?: () => void;\n /**\n * Called on Escape while focus is inside, which dismisses a snackbar with or without the ×\n * button. @default onDismiss\n */\n onEscape?: () => void;\n /** Words the snackbar announces, with English defaults. */\n labels?: Partial<SnackbarLabels>;\n /** Put the actions under the message, for long action labels. @default false */\n actionOnNewLine?: boolean;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SnackbarClassNames;\n}\n\nexport type SnackbarProps = SnackbarOwnProps &\n Omit<ComponentPropsWithRef<'div'>, keyof SnackbarOwnProps>;\n\n/**\n * M3 snackbar: a brief message about an app process, with an optional action and dismiss\n * button. Usually shown through a {@link SnackbarHost}; render it directly for custom\n * placement. Escape dismisses it while focus is inside (`onEscape`, or `onDismiss`).\n *\n * Outside a host it announces its message politely itself, when it appears and when the\n * message changes, without moving focus (the host's live region does this inside one).\n *\n * @example\n * <Snackbar actionLabel=\"Undo\" onAction={undo} onDismiss={close}>Message archived</Snackbar>\n */\nexport function Snackbar({\n children,\n actionLabel,\n onAction,\n onDismiss,\n onEscape = onDismiss,\n labels: labelsProp,\n actionOnNewLine = false,\n classNames,\n className,\n onKeyDown,\n ...rest\n}: SnackbarProps) {\n const styles = snackbarStyles({\n newLine: actionOnNewLine && actionLabel !== undefined,\n dismissible: onDismiss !== undefined,\n });\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const hasActions = actionLabel !== undefined || onDismiss !== undefined;\n const inHost = useContext(InSnackbarHost);\n const messageRef = useRef<HTMLDivElement>(null);\n const announced = useRef('');\n // Runs after every render, but announces only a new message.\n useEffect(() => {\n if (inHost) return;\n const text = messageRef.current?.textContent?.trim() ?? '';\n if (!text || text === announced.current) return;\n announced.current = text;\n announcePolite(text);\n });\n return (\n <div\n {...rest}\n onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key === 'Escape' && onEscape && !event.defaultPrevented) {\n event.stopPropagation();\n onEscape();\n }\n }}\n // The component tokens' hook (snackbar-tokens.ts).\n data-tokens={mergeDataTokens('md-comp-snackbar', rest)}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div ref={messageRef} className={styles.message({ class: classNames?.message })}>\n {children}\n </div>\n {hasActions && (\n <div className={styles.actions({ class: classNames?.actions })}>\n {actionLabel !== undefined && (\n <Button\n variant=\"text\"\n onPress={onAction}\n data-slot=\"action\"\n className={styles.action({ class: classNames?.action })}\n >\n {actionLabel}\n </Button>\n )}\n {onDismiss && (\n <IconButton\n variant=\"standard\"\n // The snackbar keeps its 48px height at every density, as a dialog header does.\n density={0}\n icon={<CloseIcon />}\n aria-label={labels.dismiss}\n onPress={onDismiss}\n data-slot=\"dismiss\"\n className={styles.dismiss({ class: classNames?.dismiss })}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** A {@link SnackbarHostState} that lives as long as the component. */\nexport function useSnackbarHostState(): SnackbarHostState {\n const [state] = useState(() => new SnackbarHostState());\n return state;\n}\n\nexport interface SnackbarHostProps extends Omit<ComponentPropsWithRef<'div'>, 'children'> {\n /** The queue of snackbars to show, from `useSnackbarHostState()`. */\n state: SnackbarHostState;\n /** Renders each snackbar; defaults to {@link Snackbar}. */\n snackbar?: (data: SnackbarData) => React.ReactNode;\n /** Words each default snackbar announces, with English defaults. */\n labels?: Partial<SnackbarLabels>;\n /**\n * The key combination that moves focus to a snackbar with actions, as the spec asks for on\n * the web. F6 by default, the key React Aria's toasts take (browsers use it to move between\n * the window's panes): while a snackbar with a control shows, F6 goes to it from anywhere on\n * the page, and pressed again there it stays. Document it where your app lists its\n * shortcuts. `false` turns it off.\n * @default \"F6\"\n */\n focusShortcut?: string | false;\n}\n\n/**\n * Shows the snackbars of a {@link SnackbarHostState} one at a time, in a polite live\n * region. Position it with `className` (e.g. `fixed inset-x-0 bottom-0`); a snackbar\n * expands upward from the bottom edge as it enters.\n *\n * - Snackbars without an action leave after 4s (`short`) or 10s (`long`); the timer pauses\n * while the pointer or focus is on the snackbar. On the web, use an auto-dismissing\n * snackbar only alongside inline feedback for the same event: it may be gone before\n * someone reads it. Snackbars with an action stay until acted on or dismissed.\n * - The live region is rendered empty first: a snackbar shown before the host mounts (one\n * at launch) appears once the region has been registered, so it's announced.\n * - `focusShortcut` (F6) moves focus to a snackbar with actions; Escape dismisses it.\n * When it leaves, focus returns to where it was before it entered the snackbar; if that\n * element is gone (an undone row was removed, say), to the control that followed it in\n * the Tab order, or else the one before it.\n *\n * @example\n * const snackbar = useSnackbarHostState();\n * <SnackbarHost state={snackbar} className=\"fixed inset-x-0 bottom-0\" />\n * snackbar.showSnackbar({ message: 'Saved' });\n */\nexport function SnackbarHost({\n state,\n snackbar,\n labels,\n focusShortcut = 'F6',\n className,\n ref,\n ...rest\n}: SnackbarHostProps) {\n const current = useSyncExternalStore(state.subscribe, state.getSnapshot, () => null);\n // Keep a leaving snackbar rendered while the next one fades in (Compose cross-fades).\n const [items, setItems] = useState<SnackbarData[]>([]);\n const [shown, setShown] = useState<SnackbarData | null>(null);\n if (current !== shown) {\n setShown(current);\n if (current && !items.includes(current)) setItems([...items, current]);\n }\n const rootRef = useObjectRef(ref);\n // Screen readers often skip content that arrives with its live region, so snackbars go in\n // only once the empty region has been on the page for a moment.\n const [regionReady, setRegionReady] = useState(false);\n useEffect(() => {\n const timer = setTimeout(() => setRegionReady(true), LIVE_REGION_DELAY);\n return () => clearTimeout(timer);\n }, []);\n\n // The shortcut focuses the shown snackbar's first control, if it has one.\n useEffect(() => {\n if (!focusShortcut || !current) return;\n const onKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.defaultPrevented || !matchesShortcut(event, focusShortcut)) return;\n const item = rootRef.current?.querySelector(`[data-snackbar-id=\"${current.id}\"]`);\n const target = item ? firstTabbable(item) : null;\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [focusShortcut, current, rootRef]);\n\n const styles = snackbarHostStyles();\n return (\n <div\n {...rest}\n ref={rootRef}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n className={styles.root({ class: className })}\n >\n {(regionReady ? items : []).map((data) => (\n <HostItem\n key={data.id}\n data={data}\n visible={data === current}\n shortcut={\n focusShortcut &&\n (data.visuals.actionLabel !== undefined || data.visuals.withDismissAction)\n ? focusShortcut\n : undefined\n }\n onExited={() => setItems((all) => all.filter((item) => item !== data))}\n className={styles.item()}\n >\n {snackbar ? (\n snackbar(data)\n ) : (\n <Snackbar\n actionLabel={data.visuals.actionLabel}\n onAction={data.performAction}\n onDismiss={data.visuals.withDismissAction ? data.dismiss : undefined}\n onEscape={data.dismiss}\n labels={labels}\n actionOnNewLine={data.visuals.actionOnNewLine}\n >\n {data.visuals.message}\n </Snackbar>\n )}\n </HostItem>\n ))}\n </div>\n );\n}\n\nfunction HostItem({\n data,\n visible,\n shortcut,\n onExited,\n className,\n children,\n}: {\n data: SnackbarData;\n visible: boolean;\n shortcut: string | undefined;\n onExited: () => void;\n className: string;\n children: React.ReactNode;\n}) {\n const { isPresent, isExiting, exitProps } = usePresence(visible);\n const [paused, setPaused] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n // Where focus was before it entered, and whether it's still inside: when the snackbar\n // leaves with focus in it, focus goes back there rather than falling to the page.\n const returnTo = useRef<FocusReturn | null>(null);\n const focusInside = useRef(false);\n\n useLayoutEffect(() => {\n if (visible || !focusInside.current) return;\n focusInside.current = false;\n const record = returnTo.current;\n returnTo.current = null;\n if (record) focusFirstAvailable(record);\n }, [visible]);\n\n useEffect(() => {\n if (!isPresent) onExited();\n }, [isPresent, onExited]);\n\n // Auto-dismiss after the duration, paused while hovered or focused within.\n useEffect(() => {\n const ms = durationMillis(data.visuals);\n if (!visible || paused || !Number.isFinite(ms)) return;\n const timer = setTimeout(data.dismiss, ms);\n return () => clearTimeout(timer);\n }, [data, visible, paused]);\n\n return (\n <div\n {...exitProps}\n ref={ref}\n data-exiting={isExiting || undefined}\n data-snackbar-id={data.id}\n aria-keyshortcuts={visible ? shortcut : undefined}\n inert={!visible}\n onPointerEnter={() => setPaused(true)}\n onPointerLeave={() => setPaused(ref.current?.contains(document.activeElement) ?? false)}\n onFocus={(event) => {\n const from = event.relatedTarget;\n if (!focusInside.current && !event.currentTarget.contains(from as Node | null)) {\n returnTo.current =\n from instanceof HTMLElement ? focusReturn(from, event.currentTarget) : null;\n }\n focusInside.current = true;\n setPaused(true);\n }}\n onBlur={(event) => {\n if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n // A snackbar that is leaving keeps the record, to hand focus back.\n if (visible) focusInside.current = false;\n setPaused(false);\n }}\n className={className}\n >\n <InSnackbarHost value>{children}</InSnackbarHost>\n </div>\n );\n}\n\n/** Where focus goes back to when a snackbar leaves, with fallbacks in Tab order. */\ninterface FocusReturn {\n target: HTMLElement;\n next: HTMLElement | null;\n previous: HTMLElement | null;\n}\n\n/**\n * Records `target` with its neighbours in the page's Tab order (leaving out the snackbar),\n * taken as focus enters the snackbar, so focus has somewhere sensible to go if `target` is\n * removed while the snackbar is open (APG: when the element that had focus is gone, move\n * focus to a logical place rather than letting it fall to the page).\n */\nfunction focusReturn(target: HTMLElement, snackbar: Element): FocusReturn {\n const order = tabbables(target.ownerDocument.body, (el) => snackbar.contains(el));\n const isOutside = (el: HTMLElement) => !el.contains(target) && !target.contains(el);\n const next =\n order.find(\n (el) =>\n isOutside(el) && target.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING,\n ) ?? null;\n const previous =\n order.findLast(\n (el) =>\n isOutside(el) && target.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING,\n ) ?? null;\n return { target, next, previous };\n}\n\nfunction focusFirstAvailable({ target, next, previous }: FocusReturn) {\n const available = [target, next, previous].find(\n (el) => el?.isConnected && !el.closest('[inert]') && !el.matches(':disabled'),\n );\n // The element may have scrolled out of view while the snackbar had focus; returning\n // focus never scrolls the page back to it (2026-10-15).\n available?.focus({ preventScroll: true });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Snackbar.js","names":[],"sources":["../../../src/components/snackbar/Snackbar.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n useSyncExternalStore,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { useLabels } from '../labels/use-labels';\nimport { usePresence } from '../../primitives/use-presence';\nimport { announcePolite, LIVE_REGION_DELAY } from '../../utils/announce';\nimport { cn } from '../../utils/cn';\nimport { firstTabbable, tabbables } from '../../utils/focus';\nimport { matchesShortcut } from '../../utils/shortcut';\nimport { Button } from '../button/Button';\nimport { IconButton } from '../icon-button/IconButton';\nimport { durationMillis, SnackbarHostState, type SnackbarData } from './snackbar-state';\nimport { snackbarHostStyles, snackbarStyles } from './snackbar-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Whether a snackbar is inside a {@link SnackbarHost}, whose live region announces it. */\nconst InSnackbarHost = createContext(false);\n\n/** Material Symbols \"close\" (Apache-2.0). */\nfunction CloseIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z\" />\n </svg>\n );\n}\n\nexport interface SnackbarClassNames {\n /** The snackbar's container. */\n root?: string;\n /** The message text. */\n message?: string;\n /** The row holding the action and dismiss buttons. */\n actions?: string;\n /** The action button. */\n action?: string;\n /** The dismiss (×) button. */\n dismiss?: string;\n}\n\nexport interface SnackbarLabels {\n /** Name of the dismiss (×) button. @default \"Dismiss\" */\n dismiss: string;\n}\n\nconst DEFAULT_LABELS: SnackbarLabels = { dismiss: 'Dismiss' };\n\ninterface SnackbarOwnProps {\n /** The message. */\n children: React.ReactNode;\n /** Label of the action button. */\n actionLabel?: string;\n /** Called when the action button is pressed. */\n onAction?: () => void;\n /** Shows a dismiss (×) button that calls this. Escape calls it too. */\n onDismiss?: () => void;\n /**\n * Called on Escape while focus is inside, which dismisses a snackbar with or without the ×\n * button. @default onDismiss\n */\n onEscape?: () => void;\n /**\n * Words the snackbar announces, with English defaults. A {@link LabelsProvider} sets them for the\n * whole app.\n */\n labels?: Partial<SnackbarLabels>;\n /** Put the actions under the message, for long action labels. @default false */\n actionOnNewLine?: boolean;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SnackbarClassNames;\n}\n\nexport type SnackbarProps = SnackbarOwnProps &\n Omit<ComponentPropsWithRef<'div'>, keyof SnackbarOwnProps>;\n\n/**\n * M3 snackbar: a brief message about an app process, with an optional action and dismiss\n * button. Usually shown through a {@link SnackbarHost}; render it directly for custom\n * placement. Escape dismisses it while focus is inside (`onEscape`, or `onDismiss`).\n *\n * Outside a host it announces its message politely itself, when it appears and when the\n * message changes, without moving focus (the host's live region does this inside one).\n *\n * @example\n * <Snackbar actionLabel=\"Undo\" onAction={undo} onDismiss={close}>Message archived</Snackbar>\n */\nexport function Snackbar({\n children,\n actionLabel,\n onAction,\n onDismiss,\n onEscape = onDismiss,\n labels: labelsProp,\n actionOnNewLine = false,\n classNames,\n className,\n onKeyDown,\n ...rest\n}: SnackbarProps) {\n const styles = snackbarStyles({\n newLine: actionOnNewLine && actionLabel !== undefined,\n dismissible: onDismiss !== undefined,\n });\n const labels = useLabels('Snackbar', DEFAULT_LABELS, labelsProp);\n const hasActions = actionLabel !== undefined || onDismiss !== undefined;\n const inHost = useContext(InSnackbarHost);\n const messageRef = useRef<HTMLDivElement>(null);\n const announced = useRef('');\n // Runs after every render, but announces only a new message.\n useEffect(() => {\n if (inHost) return;\n const text = messageRef.current?.textContent?.trim() ?? '';\n if (!text || text === announced.current) return;\n announced.current = text;\n announcePolite(text);\n });\n return (\n <div\n {...rest}\n onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key === 'Escape' && onEscape && !event.defaultPrevented) {\n event.stopPropagation();\n onEscape();\n }\n }}\n // The component tokens' hook (snackbar-tokens.ts).\n data-tokens={mergeDataTokens('md-comp-snackbar', rest)}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div ref={messageRef} className={styles.message({ class: classNames?.message })}>\n {children}\n </div>\n {hasActions && (\n <div className={styles.actions({ class: classNames?.actions })}>\n {actionLabel !== undefined && (\n <Button\n variant=\"text\"\n onPress={onAction}\n data-slot=\"action\"\n className={styles.action({ class: classNames?.action })}\n >\n {actionLabel}\n </Button>\n )}\n {onDismiss && (\n <IconButton\n variant=\"standard\"\n // The snackbar keeps its 48px height at every density, as a dialog header does.\n density={0}\n icon={<CloseIcon />}\n aria-label={labels.dismiss}\n onPress={onDismiss}\n data-slot=\"dismiss\"\n className={styles.dismiss({ class: classNames?.dismiss })}\n />\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** A {@link SnackbarHostState} that lives as long as the component. */\nexport function useSnackbarHostState(): SnackbarHostState {\n const [state] = useState(() => new SnackbarHostState());\n return state;\n}\n\nexport interface SnackbarHostProps extends Omit<ComponentPropsWithRef<'div'>, 'children'> {\n /** The queue of snackbars to show, from `useSnackbarHostState()`. */\n state: SnackbarHostState;\n /** Renders each snackbar; defaults to {@link Snackbar}. */\n snackbar?: (data: SnackbarData) => React.ReactNode;\n /**\n * Words each default snackbar announces, with English defaults. A {@link LabelsProvider} sets\n * them for the whole app.\n */\n labels?: Partial<SnackbarLabels>;\n /**\n * The key combination that moves focus to a snackbar with actions, as the spec asks for on\n * the web. F6 by default, the key React Aria's toasts take (browsers use it to move between\n * the window's panes): while a snackbar with a control shows, F6 goes to it from anywhere on\n * the page, and pressed again there it stays. Document it where your app lists its\n * shortcuts. `false` turns it off.\n * @default \"F6\"\n */\n focusShortcut?: string | false;\n}\n\n/**\n * Shows the snackbars of a {@link SnackbarHostState} one at a time, in a polite live\n * region. Position it with `className` (e.g. `fixed inset-x-0 bottom-0`); a snackbar\n * expands upward from the bottom edge as it enters.\n *\n * - Snackbars without an action leave after 4s (`short`) or 10s (`long`); the timer pauses\n * while the pointer or focus is on the snackbar. On the web, use an auto-dismissing\n * snackbar only alongside inline feedback for the same event: it may be gone before\n * someone reads it. Snackbars with an action stay until acted on or dismissed.\n * - The live region is rendered empty first: a snackbar shown before the host mounts (one\n * at launch) appears once the region has been registered, so it's announced.\n * - `focusShortcut` (F6) moves focus to a snackbar with actions; Escape dismisses it.\n * When it leaves, focus returns to where it was before it entered the snackbar; if that\n * element is gone (an undone row was removed, say), to the control that followed it in\n * the Tab order, or else the one before it.\n *\n * @example\n * const snackbar = useSnackbarHostState();\n * <SnackbarHost state={snackbar} className=\"fixed inset-x-0 bottom-0\" />\n * snackbar.showSnackbar({ message: 'Saved' });\n */\nexport function SnackbarHost({\n state,\n snackbar,\n labels,\n focusShortcut = 'F6',\n className,\n ref,\n ...rest\n}: SnackbarHostProps) {\n const current = useSyncExternalStore(state.subscribe, state.getSnapshot, () => null);\n // Keep a leaving snackbar rendered while the next one fades in (Compose cross-fades).\n const [items, setItems] = useState<SnackbarData[]>([]);\n const [shown, setShown] = useState<SnackbarData | null>(null);\n if (current !== shown) {\n setShown(current);\n if (current && !items.includes(current)) setItems([...items, current]);\n }\n const rootRef = useObjectRef(ref);\n // Screen readers often skip content that arrives with its live region, so snackbars go in\n // only once the empty region has been on the page for a moment.\n const [regionReady, setRegionReady] = useState(false);\n useEffect(() => {\n const timer = setTimeout(() => setRegionReady(true), LIVE_REGION_DELAY);\n return () => clearTimeout(timer);\n }, []);\n\n // The shortcut focuses the shown snackbar's first control, if it has one.\n useEffect(() => {\n if (!focusShortcut || !current) return;\n const onKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.defaultPrevented || !matchesShortcut(event, focusShortcut)) return;\n const item = rootRef.current?.querySelector(`[data-snackbar-id=\"${current.id}\"]`);\n const target = item ? firstTabbable(item) : null;\n if (!target) return;\n event.preventDefault();\n target.focus();\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [focusShortcut, current, rootRef]);\n\n const styles = snackbarHostStyles();\n return (\n <div\n {...rest}\n ref={rootRef}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n className={styles.root({ class: className })}\n >\n {(regionReady ? items : []).map((data) => (\n <HostItem\n key={data.id}\n data={data}\n visible={data === current}\n shortcut={\n focusShortcut &&\n (data.visuals.actionLabel !== undefined || data.visuals.withDismissAction)\n ? focusShortcut\n : undefined\n }\n onExited={() => setItems((all) => all.filter((item) => item !== data))}\n className={styles.item()}\n >\n {snackbar ? (\n snackbar(data)\n ) : (\n <Snackbar\n actionLabel={data.visuals.actionLabel}\n onAction={data.performAction}\n onDismiss={data.visuals.withDismissAction ? data.dismiss : undefined}\n onEscape={data.dismiss}\n labels={labels}\n actionOnNewLine={data.visuals.actionOnNewLine}\n >\n {data.visuals.message}\n </Snackbar>\n )}\n </HostItem>\n ))}\n </div>\n );\n}\n\nfunction HostItem({\n data,\n visible,\n shortcut,\n onExited,\n className,\n children,\n}: {\n data: SnackbarData;\n visible: boolean;\n shortcut: string | undefined;\n onExited: () => void;\n className: string;\n children: React.ReactNode;\n}) {\n const { isPresent, isExiting, exitProps } = usePresence(visible);\n const [paused, setPaused] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n // Where focus was before it entered, and whether it's still inside: when the snackbar\n // leaves with focus in it, focus goes back there rather than falling to the page.\n const returnTo = useRef<FocusReturn | null>(null);\n const focusInside = useRef(false);\n\n useLayoutEffect(() => {\n if (visible || !focusInside.current) return;\n focusInside.current = false;\n const record = returnTo.current;\n returnTo.current = null;\n if (record) focusFirstAvailable(record);\n }, [visible]);\n\n useEffect(() => {\n if (!isPresent) onExited();\n }, [isPresent, onExited]);\n\n // Auto-dismiss after the duration, paused while hovered or focused within.\n useEffect(() => {\n const ms = durationMillis(data.visuals);\n if (!visible || paused || !Number.isFinite(ms)) return;\n const timer = setTimeout(data.dismiss, ms);\n return () => clearTimeout(timer);\n }, [data, visible, paused]);\n\n return (\n <div\n {...exitProps}\n ref={ref}\n data-exiting={isExiting || undefined}\n data-snackbar-id={data.id}\n aria-keyshortcuts={visible ? shortcut : undefined}\n inert={!visible}\n onPointerEnter={() => setPaused(true)}\n onPointerLeave={() => setPaused(ref.current?.contains(document.activeElement) ?? false)}\n onFocus={(event) => {\n const from = event.relatedTarget;\n if (!focusInside.current && !event.currentTarget.contains(from as Node | null)) {\n returnTo.current =\n from instanceof HTMLElement ? focusReturn(from, event.currentTarget) : null;\n }\n focusInside.current = true;\n setPaused(true);\n }}\n onBlur={(event) => {\n if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n // A snackbar that is leaving keeps the record, to hand focus back.\n if (visible) focusInside.current = false;\n setPaused(false);\n }}\n className={className}\n >\n <InSnackbarHost value>{children}</InSnackbarHost>\n </div>\n );\n}\n\n/** Where focus goes back to when a snackbar leaves, with fallbacks in Tab order. */\ninterface FocusReturn {\n target: HTMLElement;\n next: HTMLElement | null;\n previous: HTMLElement | null;\n}\n\n/**\n * Records `target` with its neighbours in the page's Tab order (leaving out the snackbar),\n * taken as focus enters the snackbar, so focus has somewhere sensible to go if `target` is\n * removed while the snackbar is open (APG: when the element that had focus is gone, move\n * focus to a logical place rather than letting it fall to the page).\n */\nfunction focusReturn(target: HTMLElement, snackbar: Element): FocusReturn {\n const order = tabbables(target.ownerDocument.body, (el) => snackbar.contains(el));\n const isOutside = (el: HTMLElement) => !el.contains(target) && !target.contains(el);\n const next =\n order.find(\n (el) =>\n isOutside(el) && target.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING,\n ) ?? null;\n const previous =\n order.findLast(\n (el) =>\n isOutside(el) && target.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING,\n ) ?? null;\n return { target, next, previous };\n}\n\nfunction focusFirstAvailable({ target, next, previous }: FocusReturn) {\n const available = [target, next, previous].find(\n (el) => el?.isConnected && !el.closest('[inert]') && !el.matches(':disabled'),\n );\n // The element may have scrolled out of view while the snackbar had focus; returning\n // focus never scrolls the page back to it (2026-10-15).\n available?.focus({ preventScroll: true });\n}\n"],"mappings":"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"}
|
|
@@ -2,6 +2,6 @@ import { ComponentTokenSet } from "../../tokens/component.js";
|
|
|
2
2
|
import "../../index.js";
|
|
3
3
|
//#region src/components/snackbar/snackbar-tokens.d.ts
|
|
4
4
|
/** The tokens of {@link Snackbar}: `--md-comp-snackbar-<part>`. */
|
|
5
|
-
export declare const snackbarTokenSet: ComponentTokenSet<"
|
|
5
|
+
export declare const snackbarTokenSet: ComponentTokenSet<"container-color" | "container-elevation" | "icon-color" | "container-shape" | "supporting-text-color" | "action-label-text-color">;
|
|
6
6
|
//#endregion
|
|
7
7
|
//# sourceMappingURL=snackbar-tokens.d.ts.map
|
|
@@ -37,7 +37,10 @@ interface SplitButtonOwnProps {
|
|
|
37
37
|
size?: SplitButtonSize;
|
|
38
38
|
/** Icon before the label, or on its own with `leadingLabel`. Hidden from assistive tech. */
|
|
39
39
|
leadingIcon?: ReactElement;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Words the split button announces, with English defaults. A {@link LabelsProvider} sets them for
|
|
42
|
+
* the whole app.
|
|
43
|
+
*/
|
|
41
44
|
labels?: Partial<SplitButtonLabels>;
|
|
42
45
|
/**
|
|
43
46
|
* The `<Menu>` the trailing button opens, 4px below it and aligned with its end edge
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
3
4
|
import { TriggerContext } from "../../primitives/TriggerContext.js";
|
|
4
5
|
import { ButtonBase } from "../../primitives/ButtonBase.js";
|
|
5
6
|
import { TouchTarget } from "../../primitives/TouchTarget.js";
|
|
@@ -37,8 +38,9 @@ const DEFAULT_LABELS = { menu: "More options" };
|
|
|
37
38
|
* </SplitButton>
|
|
38
39
|
*/
|
|
39
40
|
function SplitButton(props) {
|
|
40
|
-
const { variant = "filled", size = "sm", children, leadingIcon, leadingLabel, labels, menu, onTrailingPress, trailingRef, open, defaultOpen, onOpenChange, disabled, classNames, className, onPress, ...rest } = props;
|
|
41
|
+
const { variant = "filled", size = "sm", children, leadingIcon, leadingLabel, labels: labelsProp, menu, onTrailingPress, trailingRef, open, defaultOpen, onOpenChange, disabled, classNames, className, onPress, ...rest } = props;
|
|
41
42
|
const { href, target, rel, type, onPressStart, onPressEnd, ...root } = rest;
|
|
43
|
+
const labels = useLabels("SplitButton", DEFAULT_LABELS, labelsProp);
|
|
42
44
|
const outerTrigger = useContext(TriggerContext);
|
|
43
45
|
const [isOpen, setOpen] = useControlledState(open, defaultOpen ?? false, onOpenChange);
|
|
44
46
|
const styles = splitButtonStyles({
|
|
@@ -67,7 +69,7 @@ function SplitButton(props) {
|
|
|
67
69
|
};
|
|
68
70
|
const trailingProps = {
|
|
69
71
|
tokenAttributes: halfTokens,
|
|
70
|
-
label: labels
|
|
72
|
+
label: labels.menu,
|
|
71
73
|
disabled,
|
|
72
74
|
hasTouchTarget,
|
|
73
75
|
buttonRef: trailingRef,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SplitButton.js","names":[],"sources":["../../../src/components/split-button/SplitButton.tsx"],"sourcesContent":["'use client';\n\nimport {\n cloneElement,\n useContext,\n useMemo,\n type ComponentPropsWithRef,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport type { PressEvent } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { TriggerContext, type TriggerContextValue } from '../../primitives/TriggerContext';\nimport { cn } from '../../utils/cn';\nimport type { MenuProps } from '../menu/Menu';\nimport { MenuTrigger } from '../menu/menu-trigger';\nimport {\n splitButtonStyles,\n type SplitButtonSize,\n type SplitButtonVariant,\n} from './split-button-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Material Symbols \"keyboard_arrow_down\" (Apache-2.0). */\nfunction ArrowDownIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z\" />\n </svg>\n );\n}\n\nexport interface SplitButtonClassNames {\n /** The group holding both buttons; merged with `className`. */\n root?: string;\n /** The leading (main action) button. */\n leading?: string;\n /** Inner wrapper of the leading button, around its icon and label. */\n content?: string;\n /** The leading button's label text. */\n label?: string;\n /** The leading button's icon. */\n icon?: string;\n /** The trailing (menu) button. */\n trailing?: string;\n /** Inner wrapper of the trailing button, around its icon. */\n trailingContent?: string;\n /** The trailing button's chevron, which turns while the menu is open. */\n menuIcon?: string;\n}\n\n/** Words a {@link SplitButton} announces. */\nexport interface SplitButtonLabels {\n /**\n * The trailing button's name; name it for what the menu holds (\"More send options\").\n * @default \"More options\"\n */\n menu: string;\n}\n\nconst DEFAULT_LABELS: SplitButtonLabels = { menu: 'More options' };\n\ninterface SplitButtonOwnProps {\n /** The M3 style of both buttons: filled, tonal, outlined or elevated. @default \"filled\" */\n variant?: SplitButtonVariant;\n /** Height, padding and label size of both buttons, from extra small to extra large. @default \"sm\" */\n size?: SplitButtonSize;\n /** Icon before the label, or on its own with `leadingLabel`. Hidden from assistive tech. */\n leadingIcon?: ReactElement;\n /** Words the split button announces, with English defaults. */\n labels?: Partial<SplitButtonLabels>;\n /**\n * The `<Menu>` the trailing button opens, 4px below it and aligned with its end edge\n * unless the menu sets its own `placement` or `offset`. Leave it out to open something\n * else: the trailing button then takes an enclosing trigger (a `DialogTrigger`, a\n * `RichTooltipTrigger`, …), or reports presses through `onTrailingPress` and `onOpenChange`.\n */\n menu?: ReactElement<MenuProps>;\n /** Called when the trailing button is pressed. Without a menu or trigger it also toggles `open`. */\n onTrailingPress?: (event: PressEvent) => void;\n /** Ref to the trailing button, e.g. to anchor a custom overlay to it. */\n trailingRef?: Ref<HTMLButtonElement>;\n /** Whether the trailing button's menu or overlay is open (controlled). */\n open?: boolean;\n /** Whether the menu starts open (uncontrolled). @default false */\n defaultOpen?: boolean;\n /** Called with the new state when the menu or overlay opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** Disables both buttons. @default false */\n disabled?: boolean;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SplitButtonClassNames;\n}\n\n/** The leading button shows a label, an icon and a label, or an icon alone. */\ntype LeadingContent =\n | {\n /** The leading button's label. */\n children: ReactNode;\n leadingLabel?: undefined;\n }\n | {\n children?: undefined;\n leadingIcon: ReactElement;\n /** Accessible name of an icon-only leading button. */\n leadingLabel: string;\n };\n\n/** The leading button's own attributes; the rest of the DOM props go to the wrapper. */\ninterface LeadingActionProps extends Pick<\n ButtonBaseActionProps,\n 'onPress' | 'onPressStart' | 'onPressEnd'\n> {\n /** The leading button's `type`, e.g. `submit` to send the form it's in. @default \"button\" */\n type?: ButtonBaseActionProps['type'];\n}\n\ninterface LeadingLinkProps extends Pick<ButtonBaseLinkProps, 'onPress'> {\n /** Makes the leading button a link (`<a>`) to this URL. */\n href: string;\n /** The leading link's `target`, e.g. `_blank`. */\n target?: ButtonBaseLinkProps['target'];\n /** The leading link's `rel`, e.g. `noopener`. */\n rel?: ButtonBaseLinkProps['rel'];\n}\n\ntype LeadingProps = LeadingActionProps | LeadingLinkProps;\n\nexport type SplitButtonProps = SplitButtonOwnProps &\n LeadingContent &\n LeadingProps &\n Omit<\n ComponentPropsWithRef<'div'>,\n | keyof SplitButtonOwnProps\n | 'children'\n | 'leadingLabel'\n | 'onPress'\n | 'onPressStart'\n | 'onPressEnd'\n | 'type'\n >;\n\n/**\n * M3 Expressive split button: a leading action and a trailing button that opens a menu\n * of related actions (or another overlay). Five sizes (`xs`–`xl`) in the filled, tonal,\n * outlined and elevated styles. While the menu is open the trailing button becomes round\n * and its chevron turns over. The wrapper is a `group`; `className`, `style`, `data-*` and\n * `aria-*` go on it, and leading-button props (`onPress`, `href`, …) go to the leading button.\n *\n * @example\n * <SplitButton\n * leadingIcon={<SendIcon />}\n * onPress={send}\n * labels={{ menu: 'More send options' }}\n * menu={<Menu onAction={schedule}><MenuItem key=\"later\">Send later</MenuItem></Menu>}\n * >\n * Send\n * </SplitButton>\n */\nexport function SplitButton(props: SplitButtonProps) {\n const {\n variant = 'filled',\n size = 'sm',\n children,\n leadingIcon,\n leadingLabel,\n labels,\n menu,\n onTrailingPress,\n trailingRef,\n open,\n defaultOpen,\n onOpenChange,\n disabled,\n classNames,\n className,\n onPress,\n ...rest\n } = props;\n const { href, target, rel, type, onPressStart, onPressEnd, ...root } = rest as typeof rest & {\n href?: string;\n target?: string;\n rel?: string;\n type?: ButtonBaseActionProps['type'];\n onPressStart?: ButtonBaseActionProps['onPressStart'];\n onPressEnd?: ButtonBaseActionProps['onPressEnd'];\n };\n const outerTrigger = useContext(TriggerContext);\n const [isOpen, setOpen] = useControlledState(open, defaultOpen ?? false, onOpenChange);\n const styles = splitButtonStyles({ variant, size });\n const hasTouchTarget = size === 'xs' || size === 'sm';\n // The halves are buttons: they read Button's colour tokens (button-tokens.ts).\n const halfTokens = { 'data-tokens': 'md-comp-button', 'data-variant': variant };\n const leadingAttributes = { ...halfTokens, 'data-slot': 'leading' };\n const leadingProps = {\n ...leadingAttributes,\n disabled,\n onPress,\n onPressStart,\n onPressEnd,\n 'aria-label': leadingLabel,\n className: styles.leading({ class: classNames?.leading }),\n ...(href !== undefined ? { href, target, rel } : { type }),\n } as ButtonBaseActionProps | ButtonBaseLinkProps;\n\n const trailingProps = {\n tokenAttributes: halfTokens,\n label: labels?.menu ?? DEFAULT_LABELS.menu,\n disabled,\n hasTouchTarget,\n buttonRef: trailingRef,\n className: styles.trailing({ class: classNames?.trailing }),\n contentClassName: styles.trailingContent({ class: classNames?.trailingContent }),\n iconClassName: styles.menuIcon({ class: classNames?.menuIcon }),\n };\n\n let trailing: ReactNode;\n if (menu) {\n trailing = (\n <MenuTrigger open={isOpen} onOpenChange={setOpen}>\n <OpenOnMouseRelease>\n <TrailingButton {...trailingProps} open={isOpen} onPress={onTrailingPress} />\n </OpenOnMouseRelease>\n {cloneElement(menu, {\n placement: menu.props.placement ?? 'bottom-end',\n offset: menu.props.offset ?? 4,\n // Aligned with the trailing button even against the window's edge.\n containerPadding: menu.props.containerPadding ?? 0,\n })}\n </MenuTrigger>\n );\n } else if (outerTrigger) {\n // An enclosing trigger (dialog, sheet, rich tooltip) opens its overlay from here.\n trailing = (\n <TrailingButton\n {...trailingProps}\n open={outerTrigger['aria-expanded'] === true}\n onPress={onTrailingPress}\n />\n );\n } else {\n trailing = (\n <TrailingButton\n {...trailingProps}\n open={isOpen}\n aria-expanded={isOpen}\n onPress={(event) => {\n setOpen(!isOpen);\n onTrailingPress?.(event);\n }}\n />\n );\n }\n\n return (\n <div\n // A group of its two buttons, so assistive tech reads them together and an app may name\n // it (`aria-label`, which ARIA prohibits on a generic `div`).\n role=\"group\"\n {...root}\n // The component tokens' hooks (split-button-tokens.ts); `data-variant` and `data-size`\n // are also public styling hooks for apps.\n data-tokens={mergeDataTokens('md-comp-split-button', root)}\n data-variant={variant}\n data-size={size}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n {/* The leading button must not pick up the trailing button's trigger props. */}\n <TriggerContext value={null}>\n <ButtonBase {...leadingProps}>\n <span className={styles.content({ class: classNames?.content })}>\n {hasTouchTarget && <TouchTarget />}\n {leadingIcon && (\n <span aria-hidden=\"true\" className={styles.icon({ class: classNames?.icon })}>\n {leadingIcon}\n </span>\n )}\n {children != null && (\n <span className={styles.label({ class: classNames?.label })}>{children}</span>\n )}\n </span>\n </ButtonBase>\n </TriggerContext>\n {trailing}\n </div>\n );\n}\n\n/**\n * A menu trigger opens on pointer down for a mouse (React Aria, like native menus). The\n * split button opens it on release instead, so the trailing button's pressed shape shows\n * while it's held, as the spec draws it (\"Pressed\" before \"Selected\"; 2026-10-13). A\n * release outside the button doesn't open it. Keyboard and touch are unchanged.\n */\nfunction OpenOnMouseRelease({ children }: { children: ReactNode }) {\n const trigger = useContext(TriggerContext);\n const value = useMemo<TriggerContextValue | null>(() => {\n if (!trigger) return null;\n const { onPressStart, onPress } = trigger;\n return {\n ...trigger,\n onPressStart: (event) => {\n if (event.pointerType !== 'mouse') onPressStart?.(event);\n },\n onPress: (event) => {\n if (event.pointerType === 'mouse') {\n // Focus the trigger first, so closing the menu returns focus to it.\n (event.target as Element).closest<HTMLElement>('button')?.focus({ preventScroll: true });\n onPressStart?.(event);\n }\n onPress?.(event);\n },\n };\n }, [trigger]);\n return <TriggerContext value={value}>{children}</TriggerContext>;\n}\n\ninterface TrailingButtonProps {\n /** Button's token hooks: `data-tokens` and `data-variant`. */\n tokenAttributes: { 'data-tokens': string; 'data-variant': string };\n label: string;\n open: boolean;\n disabled?: boolean;\n hasTouchTarget: boolean;\n buttonRef?: Ref<HTMLButtonElement>;\n onPress?: (event: PressEvent) => void;\n 'aria-expanded'?: boolean;\n className: string;\n contentClassName: string;\n iconClassName: string;\n}\n\n/**\n * The trailing button. An enclosing trigger (the menu's or another overlay's) adds its\n * press handler and `aria-expanded` through `TriggerContext`.\n */\nfunction TrailingButton({\n tokenAttributes,\n label,\n open,\n disabled,\n hasTouchTarget,\n buttonRef,\n onPress,\n 'aria-expanded': ariaExpanded,\n className,\n contentClassName,\n iconClassName,\n}: TrailingButtonProps) {\n return (\n <ButtonBase\n {...tokenAttributes}\n data-slot=\"trailing\"\n ref={buttonRef}\n aria-label={label}\n aria-expanded={ariaExpanded}\n data-open={open || undefined}\n disabled={disabled}\n onPress={onPress}\n className={className}\n >\n <span className={contentClassName}>\n {hasTouchTarget && <TouchTarget />}\n {/* M3: the trailing button always shows this icon, since it turns over when open. */}\n <span aria-hidden=\"true\" data-motion=\"standard\" className={iconClassName}>\n <ArrowDownIcon />\n </span>\n </span>\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SplitButton.js","names":[],"sources":["../../../src/components/split-button/SplitButton.tsx"],"sourcesContent":["'use client';\n\nimport {\n cloneElement,\n useContext,\n useMemo,\n type ComponentPropsWithRef,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport type { PressEvent } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { useLabels } from '../labels/use-labels';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n} from '../../primitives/ButtonBase';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { TriggerContext, type TriggerContextValue } from '../../primitives/TriggerContext';\nimport { cn } from '../../utils/cn';\nimport type { MenuProps } from '../menu/Menu';\nimport { MenuTrigger } from '../menu/menu-trigger';\nimport {\n splitButtonStyles,\n type SplitButtonSize,\n type SplitButtonVariant,\n} from './split-button-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Material Symbols \"keyboard_arrow_down\" (Apache-2.0). */\nfunction ArrowDownIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z\" />\n </svg>\n );\n}\n\nexport interface SplitButtonClassNames {\n /** The group holding both buttons; merged with `className`. */\n root?: string;\n /** The leading (main action) button. */\n leading?: string;\n /** Inner wrapper of the leading button, around its icon and label. */\n content?: string;\n /** The leading button's label text. */\n label?: string;\n /** The leading button's icon. */\n icon?: string;\n /** The trailing (menu) button. */\n trailing?: string;\n /** Inner wrapper of the trailing button, around its icon. */\n trailingContent?: string;\n /** The trailing button's chevron, which turns while the menu is open. */\n menuIcon?: string;\n}\n\n/** Words a {@link SplitButton} announces. */\nexport interface SplitButtonLabels {\n /**\n * The trailing button's name; name it for what the menu holds (\"More send options\").\n * @default \"More options\"\n */\n menu: string;\n}\n\nconst DEFAULT_LABELS: SplitButtonLabels = { menu: 'More options' };\n\ninterface SplitButtonOwnProps {\n /** The M3 style of both buttons: filled, tonal, outlined or elevated. @default \"filled\" */\n variant?: SplitButtonVariant;\n /** Height, padding and label size of both buttons, from extra small to extra large. @default \"sm\" */\n size?: SplitButtonSize;\n /** Icon before the label, or on its own with `leadingLabel`. Hidden from assistive tech. */\n leadingIcon?: ReactElement;\n /**\n * Words the split button announces, with English defaults. A {@link LabelsProvider} sets them for\n * the whole app.\n */\n labels?: Partial<SplitButtonLabels>;\n /**\n * The `<Menu>` the trailing button opens, 4px below it and aligned with its end edge\n * unless the menu sets its own `placement` or `offset`. Leave it out to open something\n * else: the trailing button then takes an enclosing trigger (a `DialogTrigger`, a\n * `RichTooltipTrigger`, …), or reports presses through `onTrailingPress` and `onOpenChange`.\n */\n menu?: ReactElement<MenuProps>;\n /** Called when the trailing button is pressed. Without a menu or trigger it also toggles `open`. */\n onTrailingPress?: (event: PressEvent) => void;\n /** Ref to the trailing button, e.g. to anchor a custom overlay to it. */\n trailingRef?: Ref<HTMLButtonElement>;\n /** Whether the trailing button's menu or overlay is open (controlled). */\n open?: boolean;\n /** Whether the menu starts open (uncontrolled). @default false */\n defaultOpen?: boolean;\n /** Called with the new state when the menu or overlay opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** Disables both buttons. @default false */\n disabled?: boolean;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: SplitButtonClassNames;\n}\n\n/** The leading button shows a label, an icon and a label, or an icon alone. */\ntype LeadingContent =\n | {\n /** The leading button's label. */\n children: ReactNode;\n leadingLabel?: undefined;\n }\n | {\n children?: undefined;\n leadingIcon: ReactElement;\n /** Accessible name of an icon-only leading button. */\n leadingLabel: string;\n };\n\n/** The leading button's own attributes; the rest of the DOM props go to the wrapper. */\ninterface LeadingActionProps extends Pick<\n ButtonBaseActionProps,\n 'onPress' | 'onPressStart' | 'onPressEnd'\n> {\n /** The leading button's `type`, e.g. `submit` to send the form it's in. @default \"button\" */\n type?: ButtonBaseActionProps['type'];\n}\n\ninterface LeadingLinkProps extends Pick<ButtonBaseLinkProps, 'onPress'> {\n /** Makes the leading button a link (`<a>`) to this URL. */\n href: string;\n /** The leading link's `target`, e.g. `_blank`. */\n target?: ButtonBaseLinkProps['target'];\n /** The leading link's `rel`, e.g. `noopener`. */\n rel?: ButtonBaseLinkProps['rel'];\n}\n\ntype LeadingProps = LeadingActionProps | LeadingLinkProps;\n\nexport type SplitButtonProps = SplitButtonOwnProps &\n LeadingContent &\n LeadingProps &\n Omit<\n ComponentPropsWithRef<'div'>,\n | keyof SplitButtonOwnProps\n | 'children'\n | 'leadingLabel'\n | 'onPress'\n | 'onPressStart'\n | 'onPressEnd'\n | 'type'\n >;\n\n/**\n * M3 Expressive split button: a leading action and a trailing button that opens a menu\n * of related actions (or another overlay). Five sizes (`xs`–`xl`) in the filled, tonal,\n * outlined and elevated styles. While the menu is open the trailing button becomes round\n * and its chevron turns over. The wrapper is a `group`; `className`, `style`, `data-*` and\n * `aria-*` go on it, and leading-button props (`onPress`, `href`, …) go to the leading button.\n *\n * @example\n * <SplitButton\n * leadingIcon={<SendIcon />}\n * onPress={send}\n * labels={{ menu: 'More send options' }}\n * menu={<Menu onAction={schedule}><MenuItem key=\"later\">Send later</MenuItem></Menu>}\n * >\n * Send\n * </SplitButton>\n */\nexport function SplitButton(props: SplitButtonProps) {\n const {\n variant = 'filled',\n size = 'sm',\n children,\n leadingIcon,\n leadingLabel,\n labels: labelsProp,\n menu,\n onTrailingPress,\n trailingRef,\n open,\n defaultOpen,\n onOpenChange,\n disabled,\n classNames,\n className,\n onPress,\n ...rest\n } = props;\n const { href, target, rel, type, onPressStart, onPressEnd, ...root } = rest as typeof rest & {\n href?: string;\n target?: string;\n rel?: string;\n type?: ButtonBaseActionProps['type'];\n onPressStart?: ButtonBaseActionProps['onPressStart'];\n onPressEnd?: ButtonBaseActionProps['onPressEnd'];\n };\n const labels = useLabels('SplitButton', DEFAULT_LABELS, labelsProp);\n const outerTrigger = useContext(TriggerContext);\n const [isOpen, setOpen] = useControlledState(open, defaultOpen ?? false, onOpenChange);\n const styles = splitButtonStyles({ variant, size });\n const hasTouchTarget = size === 'xs' || size === 'sm';\n // The halves are buttons: they read Button's colour tokens (button-tokens.ts).\n const halfTokens = { 'data-tokens': 'md-comp-button', 'data-variant': variant };\n const leadingAttributes = { ...halfTokens, 'data-slot': 'leading' };\n const leadingProps = {\n ...leadingAttributes,\n disabled,\n onPress,\n onPressStart,\n onPressEnd,\n 'aria-label': leadingLabel,\n className: styles.leading({ class: classNames?.leading }),\n ...(href !== undefined ? { href, target, rel } : { type }),\n } as ButtonBaseActionProps | ButtonBaseLinkProps;\n\n const trailingProps = {\n tokenAttributes: halfTokens,\n label: labels.menu,\n disabled,\n hasTouchTarget,\n buttonRef: trailingRef,\n className: styles.trailing({ class: classNames?.trailing }),\n contentClassName: styles.trailingContent({ class: classNames?.trailingContent }),\n iconClassName: styles.menuIcon({ class: classNames?.menuIcon }),\n };\n\n let trailing: ReactNode;\n if (menu) {\n trailing = (\n <MenuTrigger open={isOpen} onOpenChange={setOpen}>\n <OpenOnMouseRelease>\n <TrailingButton {...trailingProps} open={isOpen} onPress={onTrailingPress} />\n </OpenOnMouseRelease>\n {cloneElement(menu, {\n placement: menu.props.placement ?? 'bottom-end',\n offset: menu.props.offset ?? 4,\n // Aligned with the trailing button even against the window's edge.\n containerPadding: menu.props.containerPadding ?? 0,\n })}\n </MenuTrigger>\n );\n } else if (outerTrigger) {\n // An enclosing trigger (dialog, sheet, rich tooltip) opens its overlay from here.\n trailing = (\n <TrailingButton\n {...trailingProps}\n open={outerTrigger['aria-expanded'] === true}\n onPress={onTrailingPress}\n />\n );\n } else {\n trailing = (\n <TrailingButton\n {...trailingProps}\n open={isOpen}\n aria-expanded={isOpen}\n onPress={(event) => {\n setOpen(!isOpen);\n onTrailingPress?.(event);\n }}\n />\n );\n }\n\n return (\n <div\n // A group of its two buttons, so assistive tech reads them together and an app may name\n // it (`aria-label`, which ARIA prohibits on a generic `div`).\n role=\"group\"\n {...root}\n // The component tokens' hooks (split-button-tokens.ts); `data-variant` and `data-size`\n // are also public styling hooks for apps.\n data-tokens={mergeDataTokens('md-comp-split-button', root)}\n data-variant={variant}\n data-size={size}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n {/* The leading button must not pick up the trailing button's trigger props. */}\n <TriggerContext value={null}>\n <ButtonBase {...leadingProps}>\n <span className={styles.content({ class: classNames?.content })}>\n {hasTouchTarget && <TouchTarget />}\n {leadingIcon && (\n <span aria-hidden=\"true\" className={styles.icon({ class: classNames?.icon })}>\n {leadingIcon}\n </span>\n )}\n {children != null && (\n <span className={styles.label({ class: classNames?.label })}>{children}</span>\n )}\n </span>\n </ButtonBase>\n </TriggerContext>\n {trailing}\n </div>\n );\n}\n\n/**\n * A menu trigger opens on pointer down for a mouse (React Aria, like native menus). The\n * split button opens it on release instead, so the trailing button's pressed shape shows\n * while it's held, as the spec draws it (\"Pressed\" before \"Selected\"; 2026-10-13). A\n * release outside the button doesn't open it. Keyboard and touch are unchanged.\n */\nfunction OpenOnMouseRelease({ children }: { children: ReactNode }) {\n const trigger = useContext(TriggerContext);\n const value = useMemo<TriggerContextValue | null>(() => {\n if (!trigger) return null;\n const { onPressStart, onPress } = trigger;\n return {\n ...trigger,\n onPressStart: (event) => {\n if (event.pointerType !== 'mouse') onPressStart?.(event);\n },\n onPress: (event) => {\n if (event.pointerType === 'mouse') {\n // Focus the trigger first, so closing the menu returns focus to it.\n (event.target as Element).closest<HTMLElement>('button')?.focus({ preventScroll: true });\n onPressStart?.(event);\n }\n onPress?.(event);\n },\n };\n }, [trigger]);\n return <TriggerContext value={value}>{children}</TriggerContext>;\n}\n\ninterface TrailingButtonProps {\n /** Button's token hooks: `data-tokens` and `data-variant`. */\n tokenAttributes: { 'data-tokens': string; 'data-variant': string };\n label: string;\n open: boolean;\n disabled?: boolean;\n hasTouchTarget: boolean;\n buttonRef?: Ref<HTMLButtonElement>;\n onPress?: (event: PressEvent) => void;\n 'aria-expanded'?: boolean;\n className: string;\n contentClassName: string;\n iconClassName: string;\n}\n\n/**\n * The trailing button. An enclosing trigger (the menu's or another overlay's) adds its\n * press handler and `aria-expanded` through `TriggerContext`.\n */\nfunction TrailingButton({\n tokenAttributes,\n label,\n open,\n disabled,\n hasTouchTarget,\n buttonRef,\n onPress,\n 'aria-expanded': ariaExpanded,\n className,\n contentClassName,\n iconClassName,\n}: TrailingButtonProps) {\n return (\n <ButtonBase\n {...tokenAttributes}\n data-slot=\"trailing\"\n ref={buttonRef}\n aria-label={label}\n aria-expanded={ariaExpanded}\n data-open={open || undefined}\n disabled={disabled}\n onPress={onPress}\n className={className}\n >\n <span className={contentClassName}>\n {hasTouchTarget && <TouchTarget />}\n {/* M3: the trailing button always shows this icon, since it turns over when open. */}\n <span aria-hidden=\"true\" data-motion=\"standard\" className={iconClassName}>\n <ArrowDownIcon />\n </span>\n </span>\n </ButtonBase>\n );\n}\n"],"mappings":"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"}
|
|
@@ -6,8 +6,8 @@ export declare const switchStyles: import("tailwind-variants").TVReturnType<{
|
|
|
6
6
|
[key: string]: import("tailwind-variants").ClassValue | {
|
|
7
7
|
control?: import("tailwind-variants").ClassValue;
|
|
8
8
|
icon?: import("tailwind-variants").ClassValue;
|
|
9
|
-
stateLayer?: import("tailwind-variants").ClassValue;
|
|
10
9
|
thumb?: import("tailwind-variants").ClassValue;
|
|
10
|
+
stateLayer?: import("tailwind-variants").ClassValue;
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
13
|
} | {
|
|
@@ -15,14 +15,14 @@ export declare const switchStyles: import("tailwind-variants").TVReturnType<{
|
|
|
15
15
|
circle: import("tailwind-variants").ClassValue | {
|
|
16
16
|
control?: import("tailwind-variants").ClassValue;
|
|
17
17
|
icon?: import("tailwind-variants").ClassValue;
|
|
18
|
-
stateLayer?: import("tailwind-variants").ClassValue;
|
|
19
18
|
thumb?: import("tailwind-variants").ClassValue;
|
|
19
|
+
stateLayer?: import("tailwind-variants").ClassValue;
|
|
20
20
|
};
|
|
21
21
|
track: import("tailwind-variants").ClassValue | {
|
|
22
22
|
control?: import("tailwind-variants").ClassValue;
|
|
23
23
|
icon?: import("tailwind-variants").ClassValue;
|
|
24
|
-
stateLayer?: import("tailwind-variants").ClassValue;
|
|
25
24
|
thumb?: import("tailwind-variants").ClassValue;
|
|
25
|
+
stateLayer?: import("tailwind-variants").ClassValue;
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
28
|
}, {
|
|
@@ -131,7 +131,10 @@ interface TextFieldOwnProps extends Omit<AriaTextFieldProps<HTMLInputElement>, R
|
|
|
131
131
|
maxRows?: number;
|
|
132
132
|
/** Shows `count/maxLength` below the field. Defaults to `true` when `maxLength` is set. */
|
|
133
133
|
counter?: boolean;
|
|
134
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Words the field announces, for other languages. A {@link LabelsProvider} sets them for the
|
|
136
|
+
* whole app.
|
|
137
|
+
*/
|
|
135
138
|
labels?: Partial<TextFieldLabels>;
|
|
136
139
|
/** Classes for the root element, merged with `classNames.root`; consumer classes win. */
|
|
137
140
|
className?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { densityAttribute } from "../../tokens/density.js";
|
|
3
3
|
import { cn } from "../../utils/cn.js";
|
|
4
|
+
import { useLabels } from "../labels/use-labels.js";
|
|
4
5
|
import { mergeDataTokens } from "../../utils/merge-data-tokens.js";
|
|
5
6
|
import { pickWidgetAria, splitRootProps } from "../../utils/root-props.js";
|
|
6
7
|
import { CharacterCounter, ErrorText, FieldErrorIcon, FieldLabel, SupportingText } from "../../primitives/Field.js";
|
|
@@ -39,10 +40,7 @@ const INTERACTIVE = "input, textarea, button, a, [role=\"button\"]";
|
|
|
39
40
|
function TextField(props) {
|
|
40
41
|
const { variant = "filled", density, label, value: valueProp, defaultValue, onChange, supportingText, supportingTextOnFocus = false, errorMessage, invalid, required, disabled = false, readOnly, leadingIcon, trailingIcon, errorIcon, prefix, prefixLabel, suffix, suffixLabel, multiline = false, autoGrow = false, rows = autoGrow ? 1 : 4, maxRows, counter, labels: labelsProp, className, classNames, style, ref, inputRef, ...otherProps } = props;
|
|
41
42
|
const { root: rootData, rest: ariaProps } = splitRootProps(otherProps, { wrapper: true });
|
|
42
|
-
const labels =
|
|
43
|
-
...DEFAULT_LABELS,
|
|
44
|
-
...labelsProp
|
|
45
|
-
};
|
|
43
|
+
const labels = useLabels("TextField", DEFAULT_LABELS, labelsProp);
|
|
46
44
|
const prefixId = useId();
|
|
47
45
|
const suffixId = useId();
|
|
48
46
|
const counterId = useId();
|