@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.
Files changed (119) hide show
  1. package/dist/components/autocomplete/Autocomplete.d.ts +4 -1
  2. package/dist/components/autocomplete/Autocomplete.js +2 -4
  3. package/dist/components/autocomplete/Autocomplete.js.map +1 -1
  4. package/dist/components/badge/Badge.d.ts +5 -2
  5. package/dist/components/badge/Badge.js +7 -3
  6. package/dist/components/badge/Badge.js.map +1 -1
  7. package/dist/components/button/Button.d.ts +4 -2
  8. package/dist/components/button/Button.js.map +1 -1
  9. package/dist/components/button/ButtonContext.d.ts +1 -1
  10. package/dist/components/button/button-pending.js +2 -4
  11. package/dist/components/button/button-pending.js.map +1 -1
  12. package/dist/{composites → components}/button-group/ButtonGroup.d.ts +4 -4
  13. package/dist/{composites → components}/button-group/ButtonGroup.js +9 -7
  14. package/dist/components/button-group/ButtonGroup.js.map +1 -0
  15. package/dist/{composites → components}/button-group/button-group-styles.d.ts +1 -1
  16. package/dist/{composites → components}/button-group/button-group-styles.js +1 -1
  17. package/dist/{composites → components}/button-group/button-group-styles.js.map +1 -1
  18. package/dist/{composites → components}/button-group/lazy-overflow-menu.js +2 -2
  19. package/dist/components/button-group/lazy-overflow-menu.js.map +1 -0
  20. package/dist/{composites → components}/button-group/overflow-menu-list.js +2 -2
  21. package/dist/components/button-group/overflow-menu-list.js.map +1 -0
  22. package/dist/{composites → components}/button-group/use-overflow.js +1 -1
  23. package/dist/{composites → components}/button-group/use-overflow.js.map +1 -1
  24. package/dist/{composites → components}/button-group/use-press-expansion.js +1 -1
  25. package/dist/{composites → components}/button-group/use-press-expansion.js.map +1 -1
  26. package/dist/components/carousel/Carousel.d.ts +4 -1
  27. package/dist/components/carousel/Carousel.js +2 -4
  28. package/dist/components/carousel/Carousel.js.map +1 -1
  29. package/dist/components/checkbox/checkbox-styles.d.ts +3 -3
  30. package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
  31. package/dist/components/chip/Chip.d.ts +8 -2
  32. package/dist/components/chip/Chip.js +3 -8
  33. package/dist/components/chip/Chip.js.map +1 -1
  34. package/dist/components/date-picker/DateField.d.ts +4 -1
  35. package/dist/components/date-picker/DateField.js +3 -1
  36. package/dist/components/date-picker/DateField.js.map +1 -1
  37. package/dist/components/date-picker/DatePicker.d.ts +4 -1
  38. package/dist/components/date-picker/DatePicker.js +5 -2
  39. package/dist/components/date-picker/DatePicker.js.map +1 -1
  40. package/dist/components/date-picker/PickerDialog.d.ts +4 -1
  41. package/dist/components/date-picker/PickerDialog.js +3 -1
  42. package/dist/components/date-picker/PickerDialog.js.map +1 -1
  43. package/dist/components/date-picker/date-picker-styles.d.ts +15 -15
  44. package/dist/components/dialog/Dialog.d.ts +5 -2
  45. package/dist/components/dialog/Dialog.js +4 -2
  46. package/dist/components/dialog/Dialog.js.map +1 -1
  47. package/dist/components/icon-button/IconButton.d.ts +4 -2
  48. package/dist/components/icon-button/IconButton.js.map +1 -1
  49. package/dist/components/labels/LabelsProvider.d.ts +35 -0
  50. package/dist/components/labels/LabelsProvider.js +47 -0
  51. package/dist/components/labels/LabelsProvider.js.map +1 -0
  52. package/dist/components/labels/use-labels.d.ts +16 -0
  53. package/dist/components/labels/use-labels.js +42 -0
  54. package/dist/components/labels/use-labels.js.map +1 -0
  55. package/dist/components/menu/Menu.d.ts +4 -1
  56. package/dist/components/menu/Menu.js +7 -10
  57. package/dist/components/menu/Menu.js.map +1 -1
  58. package/dist/components/menu/menu-styles.js +12 -12
  59. package/dist/components/menu/menu-styles.js.map +1 -1
  60. package/dist/components/menu/menu-tokens.js.map +1 -1
  61. package/dist/components/navigation/NavItem.d.ts +1 -1
  62. package/dist/components/navigation/NavItem.js +9 -6
  63. package/dist/components/navigation/NavItem.js.map +1 -1
  64. package/dist/components/pull-to-refresh/PullToRefresh.d.ts +1 -1
  65. package/dist/components/pull-to-refresh/PullToRefresh.js +2 -4
  66. package/dist/components/pull-to-refresh/PullToRefresh.js.map +1 -1
  67. package/dist/components/search/SearchField.d.ts +4 -1
  68. package/dist/components/search/SearchField.js +2 -4
  69. package/dist/components/search/SearchField.js.map +1 -1
  70. package/dist/components/select/Select.d.ts +4 -1
  71. package/dist/components/select/Select.js +2 -4
  72. package/dist/components/select/Select.js.map +1 -1
  73. package/dist/components/sheet/BottomSheet.d.ts +4 -1
  74. package/dist/components/sheet/BottomSheet.js +2 -4
  75. package/dist/components/sheet/BottomSheet.js.map +1 -1
  76. package/dist/components/sheet/SideSheet.d.ts +4 -1
  77. package/dist/components/sheet/SideSheet.js +2 -4
  78. package/dist/components/sheet/SideSheet.js.map +1 -1
  79. package/dist/components/slider/Slider.d.ts +4 -1
  80. package/dist/components/slider/Slider.js +2 -4
  81. package/dist/components/slider/Slider.js.map +1 -1
  82. package/dist/components/snackbar/Snackbar.d.ts +8 -2
  83. package/dist/components/snackbar/Snackbar.js +2 -4
  84. package/dist/components/snackbar/Snackbar.js.map +1 -1
  85. package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
  86. package/dist/components/split-button/SplitButton.d.ts +4 -1
  87. package/dist/components/split-button/SplitButton.js +4 -2
  88. package/dist/components/split-button/SplitButton.js.map +1 -1
  89. package/dist/components/switch/switch-styles.d.ts +3 -3
  90. package/dist/components/text-field/TextField.d.ts +4 -1
  91. package/dist/components/text-field/TextField.js +2 -4
  92. package/dist/components/text-field/TextField.js.map +1 -1
  93. package/dist/components/time-picker/TimeField.d.ts +24 -2
  94. package/dist/components/time-picker/TimeField.js +40 -12
  95. package/dist/components/time-picker/TimeField.js.map +1 -1
  96. package/dist/components/time-picker/TimeFieldList.js +95 -0
  97. package/dist/components/time-picker/TimeFieldList.js.map +1 -0
  98. package/dist/components/time-picker/TimePicker.d.ts +9 -5
  99. package/dist/components/time-picker/TimePicker.js +5 -2
  100. package/dist/components/time-picker/TimePicker.js.map +1 -1
  101. package/dist/components/time-picker/time-list.js +33 -0
  102. package/dist/components/time-picker/time-list.js.map +1 -0
  103. package/dist/components/tooltip/Tooltip.d.ts +1 -1
  104. package/dist/index.d.ts +40 -38
  105. package/dist/index.js +4 -3
  106. package/dist/primitives.d.ts +7 -4
  107. package/dist/primitives.js +4 -1
  108. package/dist/styles.compiled.css +1 -1
  109. package/dist/utils/use-scroll-keys.d.ts +29 -0
  110. package/dist/utils/use-scroll-keys.js +66 -0
  111. package/dist/utils/use-scroll-keys.js.map +1 -0
  112. package/dist/utils/use-scroll-restoration.d.ts +43 -0
  113. package/dist/utils/use-scroll-restoration.js +173 -0
  114. package/dist/utils/use-scroll-restoration.js.map +1 -0
  115. package/dist/windmorph-labels.d.ts +101 -0
  116. package/package.json +1 -1
  117. package/dist/composites/button-group/ButtonGroup.js.map +0 -1
  118. package/dist/composites/button-group/lazy-overflow-menu.js.map +0 -1
  119. package/dist/composites/button-group/overflow-menu-list.js.map +0 -1
@@ -110,7 +110,10 @@ export type AutocompleteProps<T extends object, M extends SelectionMode = 'singl
110
110
  * ignoring case and accents ("zurich" finds "Zürich"). Not used with controlled `items`.
111
111
  */
112
112
  filter?: (textValue: string, inputValue: string) => boolean;
113
- /** Words the Autocomplete shows or announces, for other languages. */
113
+ /**
114
+ * Words the Autocomplete shows or announces, for other languages. A {@link LabelsProvider} sets
115
+ * them for the whole app.
116
+ */
114
117
  labels?: Partial<AutocompleteLabels>;
115
118
  /** Classes for the root element, merged with `classNames.root`; consumer classes win. */
116
119
  className?: string;
@@ -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 { TouchTarget } from "../../primitives/TouchTarget.js";
4
5
  import { usePreloadLoadingIndicator } from "../loading-indicator/lazy-loading-indicator.js";
5
6
  import { FOCUSABLE_DISABLED_OPTIONS } from "../../utils/focusable-disabled.js";
@@ -70,10 +71,7 @@ function Autocomplete(props) {
70
71
  const invalid = invalidProp ?? ariaFlag(ariaInvalid);
71
72
  const required = requiredProp ?? ariaFlag(ariaRequired);
72
73
  const readOnly = readOnlyProp ?? ariaFlag(ariaReadonly) ?? false;
73
- const labels = {
74
- ...DEFAULT_LABELS,
75
- ...labelsProp
76
- };
74
+ const labels = useLabels("Autocomplete", DEFAULT_LABELS, labelsProp);
77
75
  const stately = rest;
78
76
  const multiple = stately.selectionMode === "multiple";
79
77
  const cap = useSelectionCap({
@@ -1 +1 @@
1
- {"version":3,"file":"Autocomplete.js","names":[],"sources":["../../../src/components/autocomplete/Autocomplete.tsx"],"sourcesContent":["'use client';\n\nimport {\n useMemo,\n useRef,\n type AriaAttributes,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n mergeProps,\n useButton,\n useComboBox,\n useFocusRing,\n useHover,\n useObjectRef,\n ListKeyboardDelegate,\n type AriaComboBoxOptions,\n} from 'react-aria';\nimport {\n useComboBoxState,\n type ComboBoxProps,\n type ComboBoxStateOptions,\n type SelectionMode,\n} from 'react-stately/useComboBoxState';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { assertCollectionChildren } from '../../utils/assert-collection-children';\nimport type { Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport { matchesSearch } from '../../utils/search';\nimport {\n ariaFlag,\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { usePreloadLoadingIndicator } from '../loading-indicator/lazy-loading-indicator';\nimport { InputChip } from '../chip/Chip';\nimport {\n DropdownArrow,\n FieldShell,\n fieldState,\n type FieldShellClassNames,\n} from '../select/field-shell';\nimport {\n FOCUSABLE_DISABLED_OPTIONS,\n OptionList,\n OptionPopover,\n type OptionListClassNames,\n} from '../select/option-list';\nimport { useSelectionCap } from '../select/selection-cap';\nimport type { TextFieldVariant } from '../text-field/text-field-styles';\n\n/** Words the Autocomplete shows or announces; override them for other languages. */\nexport interface AutocompleteLabels {\n /** Shown when no option matches what was typed. @default \"No results\" */\n noResults: string;\n /** The loading indicator's name. @default \"Loading options\" */\n loading: string;\n /** The arrow button's name. @default \"Show options\" */\n showOptions: string;\n /** A chip's remove button; the chip's text follows it (\"Remove London\"). @default \"Remove\" */\n remove: string;\n /** With `clearable`: the clear button's name. @default \"Clear\" */\n clear: string;\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n}\n\n/** Elements that keep their own presses; a press anywhere else on the field opens it. */\nconst INTERACTIVE = 'input, textarea, button, a, [role=\"button\"]';\n\nconst DEFAULT_LABELS: AutocompleteLabels = {\n noResults: 'No results',\n loading: 'Loading options',\n showOptions: 'Show options',\n remove: 'Remove',\n clear: 'Clear',\n error: 'Error',\n};\n\nexport interface AutocompleteClassNames extends FieldShellClassNames, OptionListClassNames {\n /** The text `<input>`. */\n input?: string;\n /** With `selectionMode=\"multiple\"`: each chosen option's input chip. */\n chip?: string;\n /** With `clearable`: the clear button. */\n clear?: string;\n}\n\n/** Material Symbols `close`. */\nconst ClearIcon = () => (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\" aria-hidden=\"true\">\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/** React Aria props this API names the library's way (`disabled`, `readOnly`…), or leaves out. */\ntype RenamedProps =\n | 'isDisabled'\n | 'isRequired'\n | 'isInvalid'\n | 'isReadOnly'\n | 'label'\n | 'description'\n | 'validationState'\n | 'allowsCustomValue'\n // React Stately's deprecated single-selection API; `value` / `defaultValue` / `onChange` replace it.\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange';\n\nexport type AutocompleteProps<T extends object, M extends SelectionMode = 'single'> = Omit<\n ComboBoxProps<T, M>,\n RenamedProps\n> &\n Pick<AriaComboBoxOptions<T, M>, 'id' | 'name' | 'form'> &\n Omit<WrapperRootDOMProps, keyof ComboBoxProps<T, M>> & {\n /** The `<input>`'s id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** The container style, as `TextField`'s: a filled surface or an outline. @default \"filled\" */\n variant?: TextFieldVariant;\n /**\n * Free text: whatever is typed stays as the field's text when no option matches, instead\n * of returning to the chosen option's. @default false\n */\n allowCustomValue?: boolean;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the field (56px at 0, 44px at -3) and\n * off its menu's options (44px at 0, 32px at -3). Left out, it takes the density of the\n * nearest `ThemeProvider` or `ThemeScope`. M3 asks for density to be the reader's choice\n * rather than a default, so prefer setting it there, from a setting.\n * @default 0\n */\n density?: Density;\n /** The field's label. Give `aria-label` instead when there's no visible label. */\n label?: ReactNode;\n /** Accessible name when there's no visible label. */\n 'aria-label'?: string;\n /** Shown in the empty input. With a `label`, it appears once the label floats (on focus). */\n placeholder?: string;\n /**\n * Helper text below the field. Replaced visually by the error when invalid, and shown in\n * the error colour while the field is invalid.\n */\n supportingText?: ReactNode;\n /** Marks the field invalid (in addition to `validate` and `required`). */\n invalid?: boolean;\n /** A value is required: adds the `*` to the label and validates on submit. */\n required?: boolean;\n /** Disables the field: it can't be focused or edited. @default false */\n disabled?: boolean;\n /** The value can be read and copied but not changed; the menu doesn't open. @default false */\n readOnly?: boolean;\n /**\n * Read as `invalid` when that's left out, for a form library that sets the attribute, so\n * the field shows its error state as well as announcing it.\n */\n 'aria-invalid'?: AriaAttributes['aria-invalid'];\n /** Read as `required` when that's left out, for a form library that sets the attribute. */\n 'aria-required'?: AriaAttributes['aria-required'];\n /** Read as `readOnly` when that's left out, for a form library that sets the attribute. */\n 'aria-readonly'?: AriaAttributes['aria-readonly'];\n /** Decorative icon at the start, in a 48px box. */\n leadingIcon?: ReactNode;\n /**\n * The icon shown before the arrow while the field is invalid (M3 strongly recommends one).\n * Defaults to Material Symbols `error`, named by `labels.error`; `false` turns it off.\n */\n errorIcon?: ReactNode | false;\n /**\n * A clear button (M3's clear icon) that appears while there's typed text and empties it,\n * and a single choice with it. It's skipped by Tab, since the text can be deleted from\n * the keyboard.\n * @default false\n */\n clearable?: boolean;\n /**\n * With `selectionMode=\"multiple\"`, the most options that can be chosen; the rest show as\n * disabled until one is removed.\n */\n maxSelections?: number;\n /**\n * Shows a loading indicator in the list, e.g. while options load for what was typed.\n * The indicator's code loads when it first shows, and ahead of that when the field is\n * hovered or focused while this prop is set at all, so pass `loading={false}` (rather than\n * leaving it out) on a field that loads options.\n */\n loading?: boolean;\n /**\n * Whether an option matches what was typed. Defaults to a match anywhere in its text,\n * ignoring case and accents (\"zurich\" finds \"Zürich\"). Not used with controlled `items`.\n */\n filter?: (textValue: string, inputValue: string) => boolean;\n /** Words the Autocomplete shows or announces, for other languages. */\n labels?: Partial<AutocompleteLabels>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Classes for each part, by slot. */\n classNames?: AutocompleteClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Ref to the root `<div>` element. Use `inputRef` for the input itself. */\n ref?: Ref<HTMLDivElement>;\n /** Ref to the text `<input>`, e.g. for form libraries. */\n inputRef?: Ref<HTMLInputElement>;\n };\n\n/**\n * The arrow's press moves focus to the input (React Aria does it on a touch press) without\n * scrolling the page: a press on a field partly off-screen scrolled it into view\n * (2026-10-15). The input's `focus` asks for `preventScroll` only while the press runs.\n */\nfunction withFocusInPlace(input: HTMLInputElement | null, run: () => void) {\n if (!input) return run();\n input.focus = (options) =>\n HTMLElement.prototype.focus.call(input, { ...options, preventScroll: true });\n try {\n run();\n } finally {\n Reflect.deleteProperty(input, 'focus');\n }\n}\n\n/**\n * M3 exposed dropdown menu, editable (Compose's `ExposedDropdownMenuBox` with a\n * `PrimaryEditable` anchor): type in the field and the menu below shows the matching\n * options. Single or multiple selection (chosen options as M3 input chips in the field),\n * accent-insensitive filtering by default, options loaded from a server through `items`,\n * `onInputChange` and `loading`, and free text with `allowCustomValue` (React Aria\n * `useComboBox`).\n *\n * @example\n * <Autocomplete label=\"City\" defaultItems={cities}>\n * {(city) => <AutocompleteItem key={city.id}>{city.name}</AutocompleteItem>}\n * </Autocomplete>\n */\nexport function Autocomplete<T extends object, M extends SelectionMode = 'single'>(\n props: AutocompleteProps<T, M>,\n) {\n const {\n variant = 'filled',\n density,\n label,\n placeholder,\n supportingText,\n invalid: invalidProp,\n required: requiredProp,\n disabled = false,\n readOnly: readOnlyProp,\n 'aria-invalid': ariaInvalid,\n 'aria-required': ariaRequired,\n 'aria-readonly': ariaReadonly,\n leadingIcon,\n errorIcon,\n clearable = false,\n maxSelections,\n loading,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n inputRef: inputRefProp,\n errorMessage,\n filter,\n allowCustomValue,\n ...otherProps\n } = props;\n // Items given as elements (not `items` with a render function) must come from a client.\n if (typeof props.children !== 'function') {\n assertCollectionChildren(\n props.children,\n 'Autocomplete',\n 'AutocompleteItem and AutocompleteSection elements',\n );\n }\n const { root: rootData, rest } = splitRootProps(otherProps, { wrapper: true });\n const invalid = invalidProp ?? ariaFlag(ariaInvalid);\n const required = requiredProp ?? ariaFlag(ariaRequired);\n const readOnly = readOnlyProp ?? ariaFlag(ariaReadonly) ?? false;\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const stately = rest as Omit<ComboBoxProps<T, M>, RenamedProps>;\n const multiple = stately.selectionMode === 'multiple';\n const cap = useSelectionCap({\n multiple,\n maxSelections,\n value: stately.value,\n defaultValue: stately.defaultValue,\n onChange: stately.onChange,\n });\n\n const inputRef = useObjectRef(inputRefProp);\n const buttonRef = useRef<HTMLButtonElement>(null);\n const listBoxRef = useRef<HTMLUListElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n // A field that takes `loading` (it loads options) preloads the indicator on hover or focus.\n usePreloadLoadingIndicator(containerRef, loading !== undefined);\n\n const ariaProps = {\n ...stately,\n ...cap.valueProps,\n label,\n description: supportingText,\n errorMessage,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n isReadOnly: readOnly,\n allowsCustomValue: allowCustomValue,\n // The arrow keys wrap at the ends of the list, as in menus and lists.\n shouldFocusWrap: true,\n placeholder,\n inputRef,\n buttonRef,\n listBoxRef,\n popoverRef,\n } as AriaComboBoxOptions<T, M>;\n\n const state = useComboBoxState<T, M>({\n ...(ariaProps as unknown as ComboBoxStateOptions<T, M>),\n ...FOCUSABLE_DISABLED_OPTIONS,\n defaultFilter: filter ?? matchesSearch,\n allowsEmptyCollection: true,\n });\n const { collection } = state;\n const { disabledKeys } = state.selectionManager;\n const keyboardDelegate = useMemo(\n () =>\n new ListKeyboardDelegate({\n collection,\n disabledKeys,\n disabledBehavior: 'selection',\n ref: listBoxRef,\n }),\n [collection, disabledKeys],\n );\n const {\n buttonProps,\n inputProps,\n listBoxProps,\n labelProps,\n valueProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useComboBox<T, M>(\n {\n ...ariaProps,\n // The input's arrow keys reach disabled options too (M3: they \"can receive focus\").\n keyboardDelegate,\n },\n state,\n );\n const arrowPress = buttonProps.onPress;\n const { buttonProps: arrowProps } = useButton(\n {\n ...buttonProps,\n onPress:\n arrowPress && ((event) => withFocusInPlace(inputRef.current, () => arrowPress(event))),\n },\n buttonRef,\n );\n // A touch on the arrow, opening the list on its click (see the arrow's handlers).\n const fingerOnArrowRef = useRef(false);\n const { focusProps, isFocused } = useFocusRing({ within: true, isTextInput: true });\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n const chips = multiple ? state.selectedItems : [];\n const removeChip = (key: string | number) => state.selectionManager.toggleSelection(key);\n // Backspace in an empty input removes the last chip, as in other chip inputs.\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const last = chips.at(-1);\n if (event.key === 'Backspace' && state.inputValue === '' && last && !readOnly) {\n event.preventDefault();\n removeChip(last.key);\n }\n };\n\n // M3: \"A menu should open when a person … Selects an element, such as … a text field\", as\n // Compose's editable anchor opens on press. A press on the input or the field opens the\n // whole list, as the arrow does; keyboard focus alone doesn't (unless `menuTrigger=\"focus\"`),\n // and `menuTrigger=\"manual\"` leaves opening to the arrow and the arrow keys.\n const openOnPress = () => {\n if (!state.isOpen && !disabled && !readOnly && ariaProps.menuTrigger !== 'manual') {\n state.open(null, 'manual');\n }\n };\n\n const focused = isFocused || state.isOpen;\n const floated = focused || state.inputValue !== '' || chips.length > 0;\n const showClear = clearable && state.inputValue !== '' && !disabled && !readOnly;\n const clear = () => {\n if (!multiple) state.setValue(null);\n state.setInputValue('');\n // Back to the input in the same field, without scrolling the page (2026-10-15).\n inputRef.current?.focus({ preventScroll: true });\n };\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n\n return (\n <>\n <FieldShell\n variant={variant}\n density={density}\n label={label}\n labelProps={labelProps}\n required={required}\n state={fieldState({ disabled, invalid: isInvalid, focused, hovered: isHovered })}\n floated={floated}\n open={state.isOpen}\n leadingIcon={leadingIcon}\n errorIcon={errorIcon}\n errorIconLabel={labels.error}\n readOnly={readOnly}\n trailing={\n <button\n {...arrowProps}\n // A finger opens the list on the click, with focus in the input. React Aria's\n // touch press never ended here in Chrome (the list stayed closed), and the mouse\n // events a tap emulates focused the arrow, out of the Tab order (2026-10-17). So a\n // touch keeps both away from the arrow; the mouse and the keyboard keep React\n // Aria's press, which opens on the press start.\n onPointerDown={(event) => {\n fingerOnArrowRef.current = event.pointerType !== 'mouse';\n if (!fingerOnArrowRef.current) return arrowProps.onPointerDown?.(event);\n event.preventDefault();\n }}\n onPointerCancel={() => {\n fingerOnArrowRef.current = false;\n }}\n onClick={(event) => {\n if (!fingerOnArrowRef.current) return arrowProps.onClick?.(event);\n fingerOnArrowRef.current = false;\n inputRef.current?.focus({ preventScroll: true });\n if (!disabled && !readOnly) state.toggle(null, 'manual');\n }}\n ref={buttonRef}\n aria-label={labels.showOptions}\n className=\"relative flex size-[40px] items-center justify-center rounded-corner-full outline-none state-layer focus-ring [&>svg]:size-[24px]\"\n >\n <TouchTarget />\n <DropdownArrow open={state.isOpen} />\n </button>\n }\n trailingAction={\n showClear ? (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n // Keeps focus (and the open menu) in the input.\n onPointerDown={(event) => event.preventDefault()}\n onClick={clear}\n className={cn(\n 'relative flex size-[40px] items-center justify-center rounded-corner-full text-on-surface-variant outline-none state-layer [&>svg]:size-[24px]',\n classNames?.clear,\n )}\n >\n <TouchTarget />\n <ClearIcon />\n </button>\n ) : null\n }\n supportingText={supportingText}\n descriptionProps={descriptionProps}\n errorText={resolvedError}\n errorMessageProps={errorMessageProps}\n showError={isInvalid && Boolean(resolvedError)}\n containerRef={containerRef}\n containerProps={mergeProps(hoverProps, focusProps)}\n onContainerPointerDown={(event) => {\n // A press anywhere on the field (its padding, a chip's label, the space between\n // chips) puts the caret in the input and opens the menu, as TextField and Select\n // react to the whole field. The input opens on its own click, and the buttons (the\n // arrow, clear, a chip's remove) keep their own presses. Presses in the menu reach\n // here through React's tree from its portal, and are left alone.\n const target = event.target as Element;\n if (event.currentTarget.contains(target) && !target.closest(INTERACTIVE) && !disabled) {\n event.preventDefault();\n // The field is where the press landed: the page never scrolls to it (2026-10-15).\n inputRef.current?.focus({ preventScroll: true });\n openOnPress();\n }\n }}\n rootProps={rootData}\n rootRef={ref}\n className={className}\n classNames={classNames}\n style={style}\n multiline={multiple}\n >\n <div\n className={cn(\n 'flex min-w-0 flex-1 flex-wrap items-center gap-[8px]',\n // A row of 32px input chips fits the 56px container (12px above and below), so a\n // first chip doesn't make the field taller and each further row adds 40px. A filled\n // field's floated label takes the top 24px, so there the row stays below it.\n multiple &&\n chips.length > 0 &&\n !(variant === 'filled' && label !== undefined && label !== null) &&\n '-my-[4px]',\n )}\n >\n {chips.map((item) => (\n <InputChip\n key={item.key}\n // The field's own density sets its padding around one 32px row of chips, so the\n // chips keep their size (the field's height is what density changes here).\n density={0}\n onRemove={() => removeChip(item.key)}\n labels={{ remove: labels.remove }}\n disabled={disabled || readOnly}\n className={classNames?.chip}\n >\n {item.textValue}\n </InputChip>\n ))}\n <input\n {...pickDescriptionAria(rest)}\n {...inputProps}\n aria-describedby={renderedDescription(inputProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !(isInvalid && resolvedError) && errorMessageProps.id,\n // React Aria's element for the chosen values, which the chips show instead.\n valueProps.id,\n ])}\n onKeyDown={(event) => {\n onKeyDown(event);\n inputProps.onKeyDown?.(event);\n }}\n onClick={(event) => {\n inputProps.onClick?.(event);\n openOnPress();\n }}\n ref={inputRef}\n className={cn(\n // A zero flex basis with a 48px minimum: the input shares the chips' last row\n // while 48px are left there, and wraps to a row of its own only below that.\n 'min-w-[48px] flex-1 bg-transparent p-0 text-body-large outline-none',\n 'text-[color:var(--_md-comp-text-field-input-text-color)]',\n 'caret-[color:var(--_md-comp-text-field-caret-color)]',\n // A long hint or value ends in an ellipsis, as a text field's (2026-11-11).\n 'text-ellipsis placeholder:text-on-surface-variant placeholder:opacity-100 placeholder:text-ellipsis',\n label\n ? 'placeholder:opacity-0 placeholder:transition-opacity group-data-floated/field:placeholder:opacity-100 placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]'\n : '',\n 'group-data-invalid/field:caret-error group-data-[field-state=disabled]/field:text-on-surface/38',\n classNames?.input,\n )}\n />\n </div>\n </FieldShell>\n <OptionPopover\n state={state}\n triggerRef={containerRef}\n popoverRef={popoverRef}\n nonModal\n density={density}\n >\n <OptionList\n animateFiltering\n listBoxProps={listBoxProps}\n state={state}\n listBoxRef={listBoxRef}\n emptyLabel={labels.noResults}\n loading={loading}\n loadingLabel={labels.loading}\n isBlocked={cap.isBlocked}\n classNames={classNames}\n />\n </OptionPopover>\n </>\n );\n}\n\nexport type { Key as AutocompleteKey } from 'react-stately';\n"],"mappings":"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"}
1
+ {"version":3,"file":"Autocomplete.js","names":[],"sources":["../../../src/components/autocomplete/Autocomplete.tsx"],"sourcesContent":["'use client';\n\nimport {\n useMemo,\n useRef,\n type AriaAttributes,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n mergeProps,\n useButton,\n useComboBox,\n useFocusRing,\n useHover,\n useObjectRef,\n ListKeyboardDelegate,\n type AriaComboBoxOptions,\n} from 'react-aria';\nimport {\n useComboBoxState,\n type ComboBoxProps,\n type ComboBoxStateOptions,\n type SelectionMode,\n} from 'react-stately/useComboBoxState';\nimport { useLabels } from '../labels/use-labels';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { assertCollectionChildren } from '../../utils/assert-collection-children';\nimport type { Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport { matchesSearch } from '../../utils/search';\nimport {\n ariaFlag,\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { usePreloadLoadingIndicator } from '../loading-indicator/lazy-loading-indicator';\nimport { InputChip } from '../chip/Chip';\nimport {\n DropdownArrow,\n FieldShell,\n fieldState,\n type FieldShellClassNames,\n} from '../select/field-shell';\nimport {\n FOCUSABLE_DISABLED_OPTIONS,\n OptionList,\n OptionPopover,\n type OptionListClassNames,\n} from '../select/option-list';\nimport { useSelectionCap } from '../select/selection-cap';\nimport type { TextFieldVariant } from '../text-field/text-field-styles';\n\n/** Words the Autocomplete shows or announces; override them for other languages. */\nexport interface AutocompleteLabels {\n /** Shown when no option matches what was typed. @default \"No results\" */\n noResults: string;\n /** The loading indicator's name. @default \"Loading options\" */\n loading: string;\n /** The arrow button's name. @default \"Show options\" */\n showOptions: string;\n /** A chip's remove button; the chip's text follows it (\"Remove London\"). @default \"Remove\" */\n remove: string;\n /** With `clearable`: the clear button's name. @default \"Clear\" */\n clear: string;\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n}\n\n/** Elements that keep their own presses; a press anywhere else on the field opens it. */\nconst INTERACTIVE = 'input, textarea, button, a, [role=\"button\"]';\n\nconst DEFAULT_LABELS: AutocompleteLabels = {\n noResults: 'No results',\n loading: 'Loading options',\n showOptions: 'Show options',\n remove: 'Remove',\n clear: 'Clear',\n error: 'Error',\n};\n\nexport interface AutocompleteClassNames extends FieldShellClassNames, OptionListClassNames {\n /** The text `<input>`. */\n input?: string;\n /** With `selectionMode=\"multiple\"`: each chosen option's input chip. */\n chip?: string;\n /** With `clearable`: the clear button. */\n clear?: string;\n}\n\n/** Material Symbols `close`. */\nconst ClearIcon = () => (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\" aria-hidden=\"true\">\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/** React Aria props this API names the library's way (`disabled`, `readOnly`…), or leaves out. */\ntype RenamedProps =\n | 'isDisabled'\n | 'isRequired'\n | 'isInvalid'\n | 'isReadOnly'\n | 'label'\n | 'description'\n | 'validationState'\n | 'allowsCustomValue'\n // React Stately's deprecated single-selection API; `value` / `defaultValue` / `onChange` replace it.\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange';\n\nexport type AutocompleteProps<T extends object, M extends SelectionMode = 'single'> = Omit<\n ComboBoxProps<T, M>,\n RenamedProps\n> &\n Pick<AriaComboBoxOptions<T, M>, 'id' | 'name' | 'form'> &\n Omit<WrapperRootDOMProps, keyof ComboBoxProps<T, M>> & {\n /** The `<input>`'s id, for a `<label for>` or an error summary's link. */\n id?: string;\n /** The container style, as `TextField`'s: a filled surface or an outline. @default \"filled\" */\n variant?: TextFieldVariant;\n /**\n * Free text: whatever is typed stays as the field's text when no option matches, instead\n * of returning to the chosen option's. @default false\n */\n allowCustomValue?: boolean;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the field (56px at 0, 44px at -3) and\n * off its menu's options (44px at 0, 32px at -3). Left out, it takes the density of the\n * nearest `ThemeProvider` or `ThemeScope`. M3 asks for density to be the reader's choice\n * rather than a default, so prefer setting it there, from a setting.\n * @default 0\n */\n density?: Density;\n /** The field's label. Give `aria-label` instead when there's no visible label. */\n label?: ReactNode;\n /** Accessible name when there's no visible label. */\n 'aria-label'?: string;\n /** Shown in the empty input. With a `label`, it appears once the label floats (on focus). */\n placeholder?: string;\n /**\n * Helper text below the field. Replaced visually by the error when invalid, and shown in\n * the error colour while the field is invalid.\n */\n supportingText?: ReactNode;\n /** Marks the field invalid (in addition to `validate` and `required`). */\n invalid?: boolean;\n /** A value is required: adds the `*` to the label and validates on submit. */\n required?: boolean;\n /** Disables the field: it can't be focused or edited. @default false */\n disabled?: boolean;\n /** The value can be read and copied but not changed; the menu doesn't open. @default false */\n readOnly?: boolean;\n /**\n * Read as `invalid` when that's left out, for a form library that sets the attribute, so\n * the field shows its error state as well as announcing it.\n */\n 'aria-invalid'?: AriaAttributes['aria-invalid'];\n /** Read as `required` when that's left out, for a form library that sets the attribute. */\n 'aria-required'?: AriaAttributes['aria-required'];\n /** Read as `readOnly` when that's left out, for a form library that sets the attribute. */\n 'aria-readonly'?: AriaAttributes['aria-readonly'];\n /** Decorative icon at the start, in a 48px box. */\n leadingIcon?: ReactNode;\n /**\n * The icon shown before the arrow while the field is invalid (M3 strongly recommends one).\n * Defaults to Material Symbols `error`, named by `labels.error`; `false` turns it off.\n */\n errorIcon?: ReactNode | false;\n /**\n * A clear button (M3's clear icon) that appears while there's typed text and empties it,\n * and a single choice with it. It's skipped by Tab, since the text can be deleted from\n * the keyboard.\n * @default false\n */\n clearable?: boolean;\n /**\n * With `selectionMode=\"multiple\"`, the most options that can be chosen; the rest show as\n * disabled until one is removed.\n */\n maxSelections?: number;\n /**\n * Shows a loading indicator in the list, e.g. while options load for what was typed.\n * The indicator's code loads when it first shows, and ahead of that when the field is\n * hovered or focused while this prop is set at all, so pass `loading={false}` (rather than\n * leaving it out) on a field that loads options.\n */\n loading?: boolean;\n /**\n * Whether an option matches what was typed. Defaults to a match anywhere in its text,\n * ignoring case and accents (\"zurich\" finds \"Zürich\"). Not used with controlled `items`.\n */\n filter?: (textValue: string, inputValue: string) => boolean;\n /**\n * Words the Autocomplete shows or announces, for other languages. A {@link LabelsProvider} sets\n * them for the whole app.\n */\n labels?: Partial<AutocompleteLabels>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Classes for each part, by slot. */\n classNames?: AutocompleteClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Ref to the root `<div>` element. Use `inputRef` for the input itself. */\n ref?: Ref<HTMLDivElement>;\n /** Ref to the text `<input>`, e.g. for form libraries. */\n inputRef?: Ref<HTMLInputElement>;\n };\n\n/**\n * The arrow's press moves focus to the input (React Aria does it on a touch press) without\n * scrolling the page: a press on a field partly off-screen scrolled it into view\n * (2026-10-15). The input's `focus` asks for `preventScroll` only while the press runs.\n */\nfunction withFocusInPlace(input: HTMLInputElement | null, run: () => void) {\n if (!input) return run();\n input.focus = (options) =>\n HTMLElement.prototype.focus.call(input, { ...options, preventScroll: true });\n try {\n run();\n } finally {\n Reflect.deleteProperty(input, 'focus');\n }\n}\n\n/**\n * M3 exposed dropdown menu, editable (Compose's `ExposedDropdownMenuBox` with a\n * `PrimaryEditable` anchor): type in the field and the menu below shows the matching\n * options. Single or multiple selection (chosen options as M3 input chips in the field),\n * accent-insensitive filtering by default, options loaded from a server through `items`,\n * `onInputChange` and `loading`, and free text with `allowCustomValue` (React Aria\n * `useComboBox`).\n *\n * @example\n * <Autocomplete label=\"City\" defaultItems={cities}>\n * {(city) => <AutocompleteItem key={city.id}>{city.name}</AutocompleteItem>}\n * </Autocomplete>\n */\nexport function Autocomplete<T extends object, M extends SelectionMode = 'single'>(\n props: AutocompleteProps<T, M>,\n) {\n const {\n variant = 'filled',\n density,\n label,\n placeholder,\n supportingText,\n invalid: invalidProp,\n required: requiredProp,\n disabled = false,\n readOnly: readOnlyProp,\n 'aria-invalid': ariaInvalid,\n 'aria-required': ariaRequired,\n 'aria-readonly': ariaReadonly,\n leadingIcon,\n errorIcon,\n clearable = false,\n maxSelections,\n loading,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n inputRef: inputRefProp,\n errorMessage,\n filter,\n allowCustomValue,\n ...otherProps\n } = props;\n // Items given as elements (not `items` with a render function) must come from a client.\n if (typeof props.children !== 'function') {\n assertCollectionChildren(\n props.children,\n 'Autocomplete',\n 'AutocompleteItem and AutocompleteSection elements',\n );\n }\n const { root: rootData, rest } = splitRootProps(otherProps, { wrapper: true });\n const invalid = invalidProp ?? ariaFlag(ariaInvalid);\n const required = requiredProp ?? ariaFlag(ariaRequired);\n const readOnly = readOnlyProp ?? ariaFlag(ariaReadonly) ?? false;\n const labels = useLabels('Autocomplete', DEFAULT_LABELS, labelsProp);\n const stately = rest as Omit<ComboBoxProps<T, M>, RenamedProps>;\n const multiple = stately.selectionMode === 'multiple';\n const cap = useSelectionCap({\n multiple,\n maxSelections,\n value: stately.value,\n defaultValue: stately.defaultValue,\n onChange: stately.onChange,\n });\n\n const inputRef = useObjectRef(inputRefProp);\n const buttonRef = useRef<HTMLButtonElement>(null);\n const listBoxRef = useRef<HTMLUListElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n // A field that takes `loading` (it loads options) preloads the indicator on hover or focus.\n usePreloadLoadingIndicator(containerRef, loading !== undefined);\n\n const ariaProps = {\n ...stately,\n ...cap.valueProps,\n label,\n description: supportingText,\n errorMessage,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n isReadOnly: readOnly,\n allowsCustomValue: allowCustomValue,\n // The arrow keys wrap at the ends of the list, as in menus and lists.\n shouldFocusWrap: true,\n placeholder,\n inputRef,\n buttonRef,\n listBoxRef,\n popoverRef,\n } as AriaComboBoxOptions<T, M>;\n\n const state = useComboBoxState<T, M>({\n ...(ariaProps as unknown as ComboBoxStateOptions<T, M>),\n ...FOCUSABLE_DISABLED_OPTIONS,\n defaultFilter: filter ?? matchesSearch,\n allowsEmptyCollection: true,\n });\n const { collection } = state;\n const { disabledKeys } = state.selectionManager;\n const keyboardDelegate = useMemo(\n () =>\n new ListKeyboardDelegate({\n collection,\n disabledKeys,\n disabledBehavior: 'selection',\n ref: listBoxRef,\n }),\n [collection, disabledKeys],\n );\n const {\n buttonProps,\n inputProps,\n listBoxProps,\n labelProps,\n valueProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useComboBox<T, M>(\n {\n ...ariaProps,\n // The input's arrow keys reach disabled options too (M3: they \"can receive focus\").\n keyboardDelegate,\n },\n state,\n );\n const arrowPress = buttonProps.onPress;\n const { buttonProps: arrowProps } = useButton(\n {\n ...buttonProps,\n onPress:\n arrowPress && ((event) => withFocusInPlace(inputRef.current, () => arrowPress(event))),\n },\n buttonRef,\n );\n // A touch on the arrow, opening the list on its click (see the arrow's handlers).\n const fingerOnArrowRef = useRef(false);\n const { focusProps, isFocused } = useFocusRing({ within: true, isTextInput: true });\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n const chips = multiple ? state.selectedItems : [];\n const removeChip = (key: string | number) => state.selectionManager.toggleSelection(key);\n // Backspace in an empty input removes the last chip, as in other chip inputs.\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const last = chips.at(-1);\n if (event.key === 'Backspace' && state.inputValue === '' && last && !readOnly) {\n event.preventDefault();\n removeChip(last.key);\n }\n };\n\n // M3: \"A menu should open when a person … Selects an element, such as … a text field\", as\n // Compose's editable anchor opens on press. A press on the input or the field opens the\n // whole list, as the arrow does; keyboard focus alone doesn't (unless `menuTrigger=\"focus\"`),\n // and `menuTrigger=\"manual\"` leaves opening to the arrow and the arrow keys.\n const openOnPress = () => {\n if (!state.isOpen && !disabled && !readOnly && ariaProps.menuTrigger !== 'manual') {\n state.open(null, 'manual');\n }\n };\n\n const focused = isFocused || state.isOpen;\n const floated = focused || state.inputValue !== '' || chips.length > 0;\n const showClear = clearable && state.inputValue !== '' && !disabled && !readOnly;\n const clear = () => {\n if (!multiple) state.setValue(null);\n state.setInputValue('');\n // Back to the input in the same field, without scrolling the page (2026-10-15).\n inputRef.current?.focus({ preventScroll: true });\n };\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n\n return (\n <>\n <FieldShell\n variant={variant}\n density={density}\n label={label}\n labelProps={labelProps}\n required={required}\n state={fieldState({ disabled, invalid: isInvalid, focused, hovered: isHovered })}\n floated={floated}\n open={state.isOpen}\n leadingIcon={leadingIcon}\n errorIcon={errorIcon}\n errorIconLabel={labels.error}\n readOnly={readOnly}\n trailing={\n <button\n {...arrowProps}\n // A finger opens the list on the click, with focus in the input. React Aria's\n // touch press never ended here in Chrome (the list stayed closed), and the mouse\n // events a tap emulates focused the arrow, out of the Tab order (2026-10-17). So a\n // touch keeps both away from the arrow; the mouse and the keyboard keep React\n // Aria's press, which opens on the press start.\n onPointerDown={(event) => {\n fingerOnArrowRef.current = event.pointerType !== 'mouse';\n if (!fingerOnArrowRef.current) return arrowProps.onPointerDown?.(event);\n event.preventDefault();\n }}\n onPointerCancel={() => {\n fingerOnArrowRef.current = false;\n }}\n onClick={(event) => {\n if (!fingerOnArrowRef.current) return arrowProps.onClick?.(event);\n fingerOnArrowRef.current = false;\n inputRef.current?.focus({ preventScroll: true });\n if (!disabled && !readOnly) state.toggle(null, 'manual');\n }}\n ref={buttonRef}\n aria-label={labels.showOptions}\n className=\"relative flex size-[40px] items-center justify-center rounded-corner-full outline-none state-layer focus-ring [&>svg]:size-[24px]\"\n >\n <TouchTarget />\n <DropdownArrow open={state.isOpen} />\n </button>\n }\n trailingAction={\n showClear ? (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n // Keeps focus (and the open menu) in the input.\n onPointerDown={(event) => event.preventDefault()}\n onClick={clear}\n className={cn(\n 'relative flex size-[40px] items-center justify-center rounded-corner-full text-on-surface-variant outline-none state-layer [&>svg]:size-[24px]',\n classNames?.clear,\n )}\n >\n <TouchTarget />\n <ClearIcon />\n </button>\n ) : null\n }\n supportingText={supportingText}\n descriptionProps={descriptionProps}\n errorText={resolvedError}\n errorMessageProps={errorMessageProps}\n showError={isInvalid && Boolean(resolvedError)}\n containerRef={containerRef}\n containerProps={mergeProps(hoverProps, focusProps)}\n onContainerPointerDown={(event) => {\n // A press anywhere on the field (its padding, a chip's label, the space between\n // chips) puts the caret in the input and opens the menu, as TextField and Select\n // react to the whole field. The input opens on its own click, and the buttons (the\n // arrow, clear, a chip's remove) keep their own presses. Presses in the menu reach\n // here through React's tree from its portal, and are left alone.\n const target = event.target as Element;\n if (event.currentTarget.contains(target) && !target.closest(INTERACTIVE) && !disabled) {\n event.preventDefault();\n // The field is where the press landed: the page never scrolls to it (2026-10-15).\n inputRef.current?.focus({ preventScroll: true });\n openOnPress();\n }\n }}\n rootProps={rootData}\n rootRef={ref}\n className={className}\n classNames={classNames}\n style={style}\n multiline={multiple}\n >\n <div\n className={cn(\n 'flex min-w-0 flex-1 flex-wrap items-center gap-[8px]',\n // A row of 32px input chips fits the 56px container (12px above and below), so a\n // first chip doesn't make the field taller and each further row adds 40px. A filled\n // field's floated label takes the top 24px, so there the row stays below it.\n multiple &&\n chips.length > 0 &&\n !(variant === 'filled' && label !== undefined && label !== null) &&\n '-my-[4px]',\n )}\n >\n {chips.map((item) => (\n <InputChip\n key={item.key}\n // The field's own density sets its padding around one 32px row of chips, so the\n // chips keep their size (the field's height is what density changes here).\n density={0}\n onRemove={() => removeChip(item.key)}\n labels={{ remove: labels.remove }}\n disabled={disabled || readOnly}\n className={classNames?.chip}\n >\n {item.textValue}\n </InputChip>\n ))}\n <input\n {...pickDescriptionAria(rest)}\n {...inputProps}\n aria-describedby={renderedDescription(inputProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !(isInvalid && resolvedError) && errorMessageProps.id,\n // React Aria's element for the chosen values, which the chips show instead.\n valueProps.id,\n ])}\n onKeyDown={(event) => {\n onKeyDown(event);\n inputProps.onKeyDown?.(event);\n }}\n onClick={(event) => {\n inputProps.onClick?.(event);\n openOnPress();\n }}\n ref={inputRef}\n className={cn(\n // A zero flex basis with a 48px minimum: the input shares the chips' last row\n // while 48px are left there, and wraps to a row of its own only below that.\n 'min-w-[48px] flex-1 bg-transparent p-0 text-body-large outline-none',\n 'text-[color:var(--_md-comp-text-field-input-text-color)]',\n 'caret-[color:var(--_md-comp-text-field-caret-color)]',\n // A long hint or value ends in an ellipsis, as a text field's (2026-11-11).\n 'text-ellipsis placeholder:text-on-surface-variant placeholder:opacity-100 placeholder:text-ellipsis',\n label\n ? 'placeholder:opacity-0 placeholder:transition-opacity group-data-floated/field:placeholder:opacity-100 placeholder:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)] placeholder:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]'\n : '',\n 'group-data-invalid/field:caret-error group-data-[field-state=disabled]/field:text-on-surface/38',\n classNames?.input,\n )}\n />\n </div>\n </FieldShell>\n <OptionPopover\n state={state}\n triggerRef={containerRef}\n popoverRef={popoverRef}\n nonModal\n density={density}\n >\n <OptionList\n animateFiltering\n listBoxProps={listBoxProps}\n state={state}\n listBoxRef={listBoxRef}\n emptyLabel={labels.noResults}\n loading={loading}\n loadingLabel={labels.loading}\n isBlocked={cap.isBlocked}\n classNames={classNames}\n />\n </OptionPopover>\n </>\n );\n}\n\nexport type { Key as AutocompleteKey } from 'react-stately';\n"],"mappings":"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"}
@@ -25,7 +25,10 @@ export interface BadgeProps extends Omit<ComponentPropsWithRef<'span'>, 'childre
25
25
  * Without it, a large badge reads its content and a small one `labels.newNotification`.
26
26
  */
27
27
  label?: string;
28
- /** Words the badge announces, with English defaults. */
28
+ /**
29
+ * Words the badge announces, with English defaults. A {@link LabelsProvider} sets them for the
30
+ * whole app.
31
+ */
29
32
  labels?: Partial<BadgeLabels>;
30
33
  /** Classes for each part, by slot. `classNames.root` is merged with `className`. */
31
34
  classNames?: BadgeClassNames;
@@ -40,7 +43,7 @@ export interface BadgeProps extends Omit<ComponentPropsWithRef<'span'>, 'childre
40
43
  * <Badge>{3}</Badge>
41
44
  * <Badge max={99}>{120}</Badge> // "99+"
42
45
  */
43
- export declare function Badge({ children, max, label, labels, classNames, className, ...rest }: BadgeProps): import("react").JSX.Element;
46
+ export declare function Badge({ children, max, label, labels: labelsProp, classNames, className, ...rest }: BadgeProps): import("react").JSX.Element;
44
47
  export interface BadgedBoxClassNames {
45
48
  /** The wrapper that positions the badge over its anchor. */
46
49
  root?: string;
@@ -1,10 +1,13 @@
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 { badgeStyles, badgedBoxStyles } from "./badge-styles.js";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  import { VisuallyHidden, useNumberFormatter } from "react-aria";
7
8
  //#region src/components/badge/Badge.tsx
9
+ /** Shared with the navigation items, which read a badge's words for their own description. */
10
+ const DEFAULT_BADGE_LABELS = { newNotification: "New notification" };
8
11
  /**
9
12
  * M3 badge: a small 6px dot, or a large 16px badge holding a count or a short label, in
10
13
  * `error` colours. Place it on an icon with {@link BadgedBox}. It is announced: a count is
@@ -15,7 +18,8 @@ import { VisuallyHidden, useNumberFormatter } from "react-aria";
15
18
  * <Badge>{3}</Badge>
16
19
  * <Badge max={99}>{120}</Badge> // "99+"
17
20
  */
18
- function Badge({ children, max = 999, label, labels, classNames, className, ...rest }) {
21
+ function Badge({ children, max = 999, label, labels: labelsProp, classNames, className, ...rest }) {
22
+ const labels = useLabels("Badge", DEFAULT_BADGE_LABELS, labelsProp);
19
23
  const format = useNumberFormatter();
20
24
  const size = children == null || children === false ? "small" : "large";
21
25
  const overflow = typeof children === "string" && [...children].length > 4;
@@ -24,7 +28,7 @@ function Badge({ children, max = 999, label, labels, classNames, className, ...r
24
28
  overflow
25
29
  });
26
30
  const content = typeof children === "number" ? children > max ? `${format.format(max)}+` : format.format(children) : children;
27
- const announcement = label ?? (size === "small" ? labels?.newNotification ?? "New notification" : null);
31
+ const announcement = label ?? (size === "small" ? labels.newNotification : null);
28
32
  return /* @__PURE__ */ jsxs("span", {
29
33
  ...rest,
30
34
  "data-tokens": mergeDataTokens("md-comp-badge", rest),
@@ -67,6 +71,6 @@ function BadgedBox({ badge, children, classNames, className, ...rest }) {
67
71
  });
68
72
  }
69
73
  //#endregion
70
- export { Badge, BadgedBox };
74
+ export { Badge, BadgedBox, DEFAULT_BADGE_LABELS };
71
75
 
72
76
  //# sourceMappingURL=Badge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Badge.js","names":[],"sources":["../../../src/components/badge/Badge.tsx"],"sourcesContent":["'use client';\n\nimport type { ComponentPropsWithRef, ReactElement, ReactNode } from 'react';\nimport { useNumberFormatter, VisuallyHidden } from 'react-aria';\nimport { cn } from '../../utils/cn';\nimport { badgedBoxStyles, badgeStyles } from './badge-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Words a {@link Badge} announces. */\nexport interface BadgeLabels {\n /** What a small badge (a dot) announces. @default \"New notification\" */\n newNotification: string;\n}\n\nexport interface BadgeClassNames {\n /** The badge itself: the dot, or the large badge's container. */\n root?: string;\n /** The large badge's text, which truncates at the badge's maximum width. */\n label?: string;\n}\n\nexport interface BadgeProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {\n /**\n * A count or a short label (large badge); leave it out for the small dot. Numbers are\n * formatted for the locale and capped at `max` (\"999+\"). M3 allows four characters,\n * including the \"+\"; text badges stop at 34px wide and truncate longer text.\n */\n children?: ReactNode;\n /** The highest count shown; larger counts show as `max` + \"+\". @default 999 */\n max?: number;\n /**\n * What assistive tech reads instead of the visible content, e.g. \"3 unread messages\".\n * Without it, a large badge reads its content and a small one `labels.newNotification`.\n */\n label?: string;\n /** Words the badge announces, with English defaults. */\n labels?: Partial<BadgeLabels>;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: BadgeClassNames;\n}\n\n/**\n * M3 badge: a small 6px dot, or a large 16px badge holding a count or a short label, in\n * `error` colours. Place it on an icon with {@link BadgedBox}. It is announced: a count is\n * read, a dot reads \"New notification\" (`labels`), and `label` replaces either.\n *\n * @example\n * <Badge />\n * <Badge>{3}</Badge>\n * <Badge max={99}>{120}</Badge> // \"99+\"\n */\nexport function Badge({\n children,\n max = 999,\n label,\n labels,\n classNames,\n className,\n ...rest\n}: BadgeProps) {\n const format = useNumberFormatter();\n const size = children == null || children === false ? 'small' : 'large';\n // M3's limit is four characters, \"+\" included; only longer text is cut.\n const overflow = typeof children === 'string' && [...children].length > 4;\n const styles = badgeStyles({ size, overflow });\n const content =\n typeof children === 'number'\n ? children > max\n ? `${format.format(max)}+`\n : format.format(children)\n : children;\n const announcement =\n label ?? (size === 'small' ? (labels?.newNotification ?? 'New notification') : null);\n return (\n <span\n {...rest}\n // The component tokens' hooks (badge-tokens.ts); `data-size` is also a public hook.\n data-tokens={mergeDataTokens('md-comp-badge', rest)}\n data-size={size}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n {size === 'large' && (\n <span\n aria-hidden={announcement != null || undefined}\n // A count reads left to right in every layout, so \"999+\" never shows as \"+999\".\n dir={typeof children === 'number' ? 'ltr' : undefined}\n className={styles.label({ class: classNames?.label })}\n >\n {content}\n </span>\n )}\n {announcement != null && <VisuallyHidden elementType=\"span\">{announcement}</VisuallyHidden>}\n </span>\n );\n}\n\nexport interface BadgedBoxClassNames {\n /** The wrapper that positions the badge over its anchor. */\n root?: string;\n /** The wrapper around the anchor (`children`). */\n anchor?: string;\n}\n\nexport interface BadgedBoxProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {\n /** The badge, a {@link Badge}. */\n badge: ReactElement<BadgeProps>;\n /** The anchor, usually an icon. */\n children: ReactNode;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: BadgedBoxClassNames;\n}\n\n/**\n * Places a badge at the top end of its anchor (Compose's `BadgedBox`), e.g. on an icon.\n * The badge's announcement is part of the text around it; an anchor named with its own\n * `aria-label` should say what the badge means.\n *\n * @example\n * <IconButton\n * aria-label=\"Messages, 3 unread\"\n * icon={<BadgedBox badge={<Badge>{3}</Badge>}><MailIcon /></BadgedBox>}\n * />\n */\nexport function BadgedBox({ badge, children, classNames, className, ...rest }: BadgedBoxProps) {\n // The placement follows the badge's own `data-size` in CSS, so a badge that arrives as a\n // lazy server reference (no props yet) still hangs in the right place.\n const styles = badgedBoxStyles();\n return (\n <span {...rest} className={styles.root({ class: cn(classNames?.root, className) })}>\n <span className={styles.anchor({ class: classNames?.anchor })}>{children}</span>\n <span className={styles.hang()}>{badge}</span>\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmDA,SAAgB,MAAM,EACpB,UACA,MAAM,KACN,OACA,QACA,YACA,WACA,GAAG,QACU;CACb,MAAM,SAAS,mBAAmB;CAClC,MAAM,OAAO,YAAY,QAAQ,aAAa,QAAQ,UAAU;CAEhE,MAAM,WAAW,OAAO,aAAa,YAAY,CAAC,GAAG,QAAQ,CAAC,CAAC,SAAS;CACxE,MAAM,SAAS,YAAY;EAAE;EAAM;CAAS,CAAC;CAC7C,MAAM,UACJ,OAAO,aAAa,WAChB,WAAW,MACT,GAAG,OAAO,OAAO,GAAG,EAAE,KACtB,OAAO,OAAO,QAAQ,IACxB;CACN,MAAM,eACJ,UAAU,SAAS,UAAW,QAAQ,mBAAmB,qBAAsB;CACjF,OACE,qBAAC,QAAD;EACE,GAAI;EAEJ,eAAa,gBAAgB,iBAAiB,IAAI;EAClD,aAAW;EACX,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;EALnE,UAAA,CAOG,SAAS,WACR,oBAAC,QAAD;GACE,eAAa,gBAAgB,QAAQ,KAAA;GAErC,KAAK,OAAO,aAAa,WAAW,QAAQ,KAAA;GAC5C,WAAW,OAAO,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;GAEnD,UAAA;EACG,CAAA,GAEP,gBAAgB,QAAQ,oBAAC,gBAAD;GAAgB,aAAY;GAAQ,UAAA;EAA6B,CAAA,CACtF;;AAEV;;;;;;;;;;;;AA6BA,SAAgB,UAAU,EAAE,OAAO,UAAU,YAAY,WAAW,GAAG,QAAwB;CAG7F,MAAM,SAAS,gBAAgB;CAC/B,OACE,qBAAC,QAAD;EAAM,GAAI;EAAM,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;EAAjF,UAAA,CACE,oBAAC,QAAD;GAAM,WAAW,OAAO,OAAO,EAAE,OAAO,YAAY,OAAO,CAAC;GAAI;EAAe,CAAA,GAC/E,oBAAC,QAAD;GAAM,WAAW,OAAO,KAAK;GAAI,UAAA;EAAY,CAAA,CACzC;;AAEV"}
1
+ {"version":3,"file":"Badge.js","names":[],"sources":["../../../src/components/badge/Badge.tsx"],"sourcesContent":["'use client';\n\nimport type { ComponentPropsWithRef, ReactElement, ReactNode } from 'react';\nimport { useNumberFormatter, VisuallyHidden } from 'react-aria';\nimport { useLabels } from '../labels/use-labels';\nimport { cn } from '../../utils/cn';\nimport { badgedBoxStyles, badgeStyles } from './badge-styles';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Words a {@link Badge} announces. */\nexport interface BadgeLabels {\n /** What a small badge (a dot) announces. @default \"New notification\" */\n newNotification: string;\n}\n\n/** Shared with the navigation items, which read a badge's words for their own description. */\nexport const DEFAULT_BADGE_LABELS: BadgeLabels = { newNotification: 'New notification' };\n\nexport interface BadgeClassNames {\n /** The badge itself: the dot, or the large badge's container. */\n root?: string;\n /** The large badge's text, which truncates at the badge's maximum width. */\n label?: string;\n}\n\nexport interface BadgeProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {\n /**\n * A count or a short label (large badge); leave it out for the small dot. Numbers are\n * formatted for the locale and capped at `max` (\"999+\"). M3 allows four characters,\n * including the \"+\"; text badges stop at 34px wide and truncate longer text.\n */\n children?: ReactNode;\n /** The highest count shown; larger counts show as `max` + \"+\". @default 999 */\n max?: number;\n /**\n * What assistive tech reads instead of the visible content, e.g. \"3 unread messages\".\n * Without it, a large badge reads its content and a small one `labels.newNotification`.\n */\n label?: string;\n /**\n * Words the badge announces, with English defaults. A {@link LabelsProvider} sets them for the\n * whole app.\n */\n labels?: Partial<BadgeLabels>;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: BadgeClassNames;\n}\n\n/**\n * M3 badge: a small 6px dot, or a large 16px badge holding a count or a short label, in\n * `error` colours. Place it on an icon with {@link BadgedBox}. It is announced: a count is\n * read, a dot reads \"New notification\" (`labels`), and `label` replaces either.\n *\n * @example\n * <Badge />\n * <Badge>{3}</Badge>\n * <Badge max={99}>{120}</Badge> // \"99+\"\n */\nexport function Badge({\n children,\n max = 999,\n label,\n labels: labelsProp,\n classNames,\n className,\n ...rest\n}: BadgeProps) {\n const labels = useLabels('Badge', DEFAULT_BADGE_LABELS, labelsProp);\n const format = useNumberFormatter();\n const size = children == null || children === false ? 'small' : 'large';\n // M3's limit is four characters, \"+\" included; only longer text is cut.\n const overflow = typeof children === 'string' && [...children].length > 4;\n const styles = badgeStyles({ size, overflow });\n const content =\n typeof children === 'number'\n ? children > max\n ? `${format.format(max)}+`\n : format.format(children)\n : children;\n const announcement = label ?? (size === 'small' ? labels.newNotification : null);\n return (\n <span\n {...rest}\n // The component tokens' hooks (badge-tokens.ts); `data-size` is also a public hook.\n data-tokens={mergeDataTokens('md-comp-badge', rest)}\n data-size={size}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n {size === 'large' && (\n <span\n aria-hidden={announcement != null || undefined}\n // A count reads left to right in every layout, so \"999+\" never shows as \"+999\".\n dir={typeof children === 'number' ? 'ltr' : undefined}\n className={styles.label({ class: classNames?.label })}\n >\n {content}\n </span>\n )}\n {announcement != null && <VisuallyHidden elementType=\"span\">{announcement}</VisuallyHidden>}\n </span>\n );\n}\n\nexport interface BadgedBoxClassNames {\n /** The wrapper that positions the badge over its anchor. */\n root?: string;\n /** The wrapper around the anchor (`children`). */\n anchor?: string;\n}\n\nexport interface BadgedBoxProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {\n /** The badge, a {@link Badge}. */\n badge: ReactElement<BadgeProps>;\n /** The anchor, usually an icon. */\n children: ReactNode;\n /** Classes for each part, by slot. `classNames.root` is merged with `className`. */\n classNames?: BadgedBoxClassNames;\n}\n\n/**\n * Places a badge at the top end of its anchor (Compose's `BadgedBox`), e.g. on an icon.\n * The badge's announcement is part of the text around it; an anchor named with its own\n * `aria-label` should say what the badge means.\n *\n * @example\n * <IconButton\n * aria-label=\"Messages, 3 unread\"\n * icon={<BadgedBox badge={<Badge>{3}</Badge>}><MailIcon /></BadgedBox>}\n * />\n */\nexport function BadgedBox({ badge, children, classNames, className, ...rest }: BadgedBoxProps) {\n // The placement follows the badge's own `data-size` in CSS, so a badge that arrives as a\n // lazy server reference (no props yet) still hangs in the right place.\n const styles = badgedBoxStyles();\n return (\n <span {...rest} className={styles.root({ class: cn(classNames?.root, className) })}>\n <span className={styles.anchor({ class: classNames?.anchor })}>{children}</span>\n <span className={styles.hang()}>{badge}</span>\n </span>\n );\n}\n"],"mappings":";;;;;;;;;AAgBA,MAAa,uBAAoC,EAAE,iBAAiB,mBAAmB;;;;;;;;;;;AA0CvF,SAAgB,MAAM,EACpB,UACA,MAAM,KACN,OACA,QAAQ,YACR,YACA,WACA,GAAG,QACU;CACb,MAAM,SAAS,UAAU,SAAS,sBAAsB,UAAU;CAClE,MAAM,SAAS,mBAAmB;CAClC,MAAM,OAAO,YAAY,QAAQ,aAAa,QAAQ,UAAU;CAEhE,MAAM,WAAW,OAAO,aAAa,YAAY,CAAC,GAAG,QAAQ,CAAC,CAAC,SAAS;CACxE,MAAM,SAAS,YAAY;EAAE;EAAM;CAAS,CAAC;CAC7C,MAAM,UACJ,OAAO,aAAa,WAChB,WAAW,MACT,GAAG,OAAO,OAAO,GAAG,EAAE,KACtB,OAAO,OAAO,QAAQ,IACxB;CACN,MAAM,eAAe,UAAU,SAAS,UAAU,OAAO,kBAAkB;CAC3E,OACE,qBAAC,QAAD;EACE,GAAI;EAEJ,eAAa,gBAAgB,iBAAiB,IAAI;EAClD,aAAW;EACX,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;EALnE,UAAA,CAOG,SAAS,WACR,oBAAC,QAAD;GACE,eAAa,gBAAgB,QAAQ,KAAA;GAErC,KAAK,OAAO,aAAa,WAAW,QAAQ,KAAA;GAC5C,WAAW,OAAO,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;GAEnD,UAAA;EACG,CAAA,GAEP,gBAAgB,QAAQ,oBAAC,gBAAD;GAAgB,aAAY;GAAQ,UAAA;EAA6B,CAAA,CACtF;;AAEV;;;;;;;;;;;;AA6BA,SAAgB,UAAU,EAAE,OAAO,UAAU,YAAY,WAAW,GAAG,QAAwB;CAG7F,MAAM,SAAS,gBAAgB;CAC/B,OACE,qBAAC,QAAD;EAAM,GAAI;EAAM,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;EAAjF,UAAA,CACE,oBAAC,QAAD;GAAM,WAAW,OAAO,OAAO,EAAE,OAAO,YAAY,OAAO,CAAC;GAAI;EAAe,CAAA,GAC/E,oBAAC,QAAD;GAAM,WAAW,OAAO,KAAK;GAAI,UAAA;EAAY,CAAA,CACzC;;AAEV"}
@@ -1,6 +1,6 @@
1
- import { ButtonBaseActionProps, ButtonBaseLinkProps, ButtonBaseToggleProps } from "../../primitives/ButtonBase.js";
2
1
  import { ButtonShape, ButtonSize, ButtonVariant } from "./button-styles.js";
3
2
  import { ButtonLabels } from "./button-pending.js";
3
+ import { ButtonBaseActionProps, ButtonBaseLinkProps, ButtonBaseToggleProps } from "../../primitives/ButtonBase.js";
4
4
  import { ReactElement, ReactNode } from "react";
5
5
  //#region src/components/button/Button.d.ts
6
6
  export interface ButtonClassNames {
@@ -56,7 +56,9 @@ interface ButtonOwnProps {
56
56
  children: Exclude<ReactNode, null | undefined | boolean>;
57
57
  /** Classes for each part. `classNames.root` is merged with `className`. */
58
58
  classNames?: ButtonClassNames;
59
- /** Words the button announces. */
59
+ /**
60
+ * Words the button announces. A {@link LabelsProvider} sets them for the whole app.
61
+ */
60
62
  labels?: Partial<ButtonLabels>;
61
63
  }
62
64
  export interface ButtonActionProps extends ButtonOwnProps, PendingProps, Omit<ButtonBaseActionProps, keyof ButtonOwnProps | keyof PendingProps> {}
@@ -1 +1 @@
1
- {"version":3,"file":"Button.js","names":[],"sources":["../../../src/components/button/Button.tsx"],"sourcesContent":["'use client';\n\nimport { useContext, type ReactElement, type ReactNode, type Ref } from 'react';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n type ButtonBaseProps,\n type ButtonBaseToggleProps,\n} from '../../primitives/ButtonBase';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { cn } from '../../utils/cn';\nimport {\n buttonStyles,\n type ButtonShape,\n type ButtonSize,\n type ButtonVariant,\n} from './button-styles';\nimport { ButtonContext } from './ButtonContext';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { pendingClasses, useButtonPending, type ButtonLabels } from './button-pending';\n\nexport interface ButtonClassNames {\n /** The button element itself; merged with `className`. */\n root?: string;\n /** Inner wrapper around the icons and label. */\n content?: string;\n /** The label text. */\n label?: string;\n /** The leading icon. */\n icon?: string;\n /** The slot holding the loading indicator while `pending`, centred over the hidden content. */\n pendingIndicator?: string;\n}\n\ninterface PendingProps {\n /**\n * Shows a loading indicator in place of the content while an action runs, and blocks\n * presses. The button keeps its width, its colours and keyboard focus; its accessible name\n * gains `labels.pending`, and a focused button announces it. The indicator appears after\n * 200ms, so a quick action doesn't flash. Use it for actions that take a moment, not for\n * every button in a list. A submit button doesn't submit again while pending.\n * @default false\n */\n pending?: boolean;\n /**\n * Replaces the loading indicator shown while `pending`, e.g. a `CircularProgressIndicator`\n * for a wait that may pass five seconds. It's centred over the content and takes the content\n * colour; a progress indicator also loses its track, as M3 asks inside a button.\n */\n pendingIndicator?: ReactElement;\n}\n\n/** A toggle's state is its selection: it has no pending state. */\ninterface NoPendingProps {\n /** Toggle buttons have no pending state. */\n pending?: never;\n /** Toggle buttons have no pending state. */\n pendingIndicator?: never;\n}\n\ninterface ButtonOwnProps {\n /** The M3 style: filled, tonal, outlined, elevated or text. @default \"filled\" */\n variant?: ButtonVariant;\n /** Height, padding and label size, from extra small to extra large. @default \"sm\" */\n size?: ButtonSize;\n /** Fully round corners, or square ones (still rounded, but less). @default \"round\" */\n shape?: ButtonShape;\n /**\n * Icon before the label, e.g. a Material Symbols SVG. Decorative; the label names the\n * button. M3 allows one icon, on the leading side (\"Don't use two icons in the same\n * button\"), so there's no trailing icon.\n */\n leadingIcon?: ReactElement;\n /** The visible label. Required: buttons without text are `IconButton`s. */\n children: Exclude<ReactNode, null | undefined | boolean>;\n /** Classes for each part. `classNames.root` is merged with `className`. */\n classNames?: ButtonClassNames;\n /** Words the button announces. */\n labels?: Partial<ButtonLabels>;\n}\n\nexport interface ButtonActionProps\n extends\n ButtonOwnProps,\n PendingProps,\n Omit<ButtonBaseActionProps, keyof ButtonOwnProps | keyof PendingProps> {}\n\nexport interface ButtonLinkProps\n extends\n ButtonOwnProps,\n PendingProps,\n Omit<ButtonBaseLinkProps, keyof ButtonOwnProps | keyof PendingProps> {}\n\nexport interface ButtonToggleProps\n extends\n Omit<ButtonOwnProps, 'variant'>,\n NoPendingProps,\n Omit<ButtonBaseToggleProps, keyof ButtonOwnProps | keyof NoPendingProps> {\n /** Text buttons have no toggle form. @default \"filled\" */\n variant?: Exclude<ButtonVariant, 'text'>;\n /**\n * Leading icon shown while selected, e.g. the filled form of `leadingIcon` (M3: \"use an\n * outlined icon when unselected, and a filled version of the icon when selected\").\n * @default leadingIcon\n */\n selectedLeadingIcon?: ReactElement;\n}\n\nexport type ButtonProps = ButtonActionProps | ButtonLinkProps | ButtonToggleProps;\n\n/**\n * M3 Expressive button: five variants, five sizes (XS–XL), round or square shape,\n * an optional toggle form, and a press morph that squares the corners while pressed.\n *\n * Renders a `<button>`, or an `<a>` when `href` is set. `className` and `style` go on\n * that element, and consumer classes always win.\n *\n * @example\n * <Button variant=\"tonal\" size=\"md\" leadingIcon={<EditIcon />} onPress={edit}>Edit</Button>\n * <Button toggle leadingIcon={<StarIcon />} selectedLeadingIcon={<StarFilledIcon />}\n * selected={starred} onSelectedChange={setStarred}>Starred</Button>\n * <Button href=\"/docs\" variant=\"text\">Docs</Button>\n * <Button type=\"submit\" pending={isPending}>Save</Button>\n */\nexport function Button({\n variant: variantProp,\n size: sizeProp,\n shape: shapeProp,\n disabled: disabledProp,\n leadingIcon,\n pendingIndicator,\n labels,\n classNames,\n className,\n children,\n ...rest\n}: ButtonProps) {\n // Only toggle props carry `selectedLeadingIcon`; the others read it as undefined.\n const { selectedLeadingIcon, ...base } = rest as typeof rest & {\n selectedLeadingIcon?: ReactElement;\n };\n const context = useContext(ButtonContext);\n const toggle = base.toggle === true;\n let variant = variantProp ?? context.variant ?? 'filled';\n if (toggle && variant === 'text') {\n if (process.env.NODE_ENV !== 'production') {\n console.warn('[@windmorph/react] Text buttons have no toggle form; using \"filled\".');\n }\n variant = 'filled';\n }\n const size = sizeProp ?? context.size ?? 'sm';\n const styles = buttonStyles({\n variant,\n size,\n shape: shapeProp ?? context.shape ?? 'round',\n toggle,\n connected: context.connected ?? 'none',\n });\n\n // Destructuring a union merges its members, so TypeScript loses the pairing of\n // `href`/`toggle` with their `ref` types. `base` still holds exactly one member's props.\n // The component tokens' hooks (button-tokens.ts), also public styling hooks for apps.\n const tokenAttributes = {\n 'data-tokens': mergeDataTokens('md-comp-button', base),\n 'data-variant': variant,\n 'data-size': size,\n };\n const pending = useButtonPending<HTMLElement>({\n pending: base.pending,\n pendingIndicator,\n labels,\n size,\n ariaLabel: base['aria-label'],\n ariaLabelledby: base['aria-labelledby'],\n ref: base.ref as Ref<HTMLElement> | undefined,\n indicatorClassName: cn(pendingClasses.indicator, classNames?.pendingIndicator),\n });\n const baseProps = {\n ...base,\n ...tokenAttributes,\n ...pending.rootProps,\n disabled: disabledProp ?? context.disabled ?? false,\n className: styles.root({ class: cn(classNames?.root, className) }),\n } as ButtonBaseProps;\n\n return (\n <ButtonBase {...baseProps}>\n {({ isSelected }) => {\n const icon = isSelected && selectedLeadingIcon ? selectedLeadingIcon : leadingIcon;\n const faces = (\n <>\n {icon && (\n <span aria-hidden=\"true\" className={styles.icon({ class: classNames?.icon })}>\n {icon}\n </span>\n )}\n <span className={styles.label({ class: classNames?.label })}>\n {children}\n {pending.nameSuffix}\n </span>\n </>\n );\n return (\n <span\n {...pending.contentProps}\n className={styles.content({\n class: cn(pending.takesPending && pendingClasses.content, classNames?.content),\n })}\n >\n {(size === 'xs' || size === 'sm') && <TouchTarget />}\n {pending.takesPending ? (\n <span className={cn(pendingClasses.faces, pendingClasses.replaced)}>{faces}</span>\n ) : (\n faces\n )}\n {pending.indicator}\n {pending.hiddenName}\n </span>\n );\n }}\n </ButtonBase>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA6HA,SAAgB,OAAO,EACrB,SAAS,aACT,MAAM,UACN,OAAO,WACP,UAAU,cACV,aACA,kBACA,QACA,YACA,WACA,UACA,GAAG,QACW;CAEd,MAAM,EAAE,qBAAqB,GAAG,SAAS;CAGzC,MAAM,UAAU,WAAW,aAAa;CACxC,MAAM,SAAS,KAAK,WAAW;CAC/B,IAAI,UAAU,eAAe,QAAQ,WAAW;CAChD,IAAI,UAAU,YAAY,QAAQ;EAChC,IAAI,QAAQ,IAAI,aAAa,cAC3B,QAAQ,KAAK,wEAAsE;EAErF,UAAU;CACZ;CACA,MAAM,OAAO,YAAY,QAAQ,QAAQ;CACzC,MAAM,SAAS,aAAa;EAC1B;EACA;EACA,OAAO,aAAa,QAAQ,SAAS;EACrC;EACA,WAAW,QAAQ,aAAa;CAClC,CAAC;CAKD,MAAM,kBAAkB;EACtB,eAAe,gBAAgB,kBAAkB,IAAI;EACrD,gBAAgB;EAChB,aAAa;CACf;CACA,MAAM,UAAU,iBAA8B;EAC5C,SAAS,KAAK;EACd;EACA;EACA;EACA,WAAW,KAAK;EAChB,gBAAgB,KAAK;EACrB,KAAK,KAAK;EACV,oBAAoB,GAAG,eAAe,WAAW,YAAY,gBAAgB;CAC/E,CAAC;CACD,MAAM,YAAY;EAChB,GAAG;EACH,GAAG;EACH,GAAG,QAAQ;EACX,UAAU,gBAAgB,QAAQ,YAAY;EAC9C,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;CACnE;CAEA,OACE,oBAAC,YAAD;EAAY,GAAI;EACZ,WAAA,EAAE,iBAAiB;GACnB,MAAM,OAAO,cAAc,sBAAsB,sBAAsB;GACvE,MAAM,QACJ,qBAAA,YAAA,EAAA,UAAA,CACG,QACC,oBAAC,QAAD;IAAM,eAAY;IAAO,WAAW,OAAO,KAAK,EAAE,OAAO,YAAY,KAAK,CAAC;IACxE,UAAA;GACG,CAAA,GAER,qBAAC,QAAD;IAAM,WAAW,OAAO,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;IAA1D,UAAA,CACG,UACA,QAAQ,UACL;GACN,CAAA,CAAA,EAAA,CAAA;GAEJ,OACE,qBAAC,QAAD;IACE,GAAI,QAAQ;IACZ,WAAW,OAAO,QAAQ,EACxB,OAAO,GAAG,QAAQ,gBAAgB,eAAe,SAAS,YAAY,OAAO,EAC/E,CAAC;IAJH,UAAA;MAMI,SAAS,QAAQ,SAAS,SAAS,oBAAC,aAAD,CAAc,CAAA;KAClD,QAAQ,eACP,oBAAC,QAAD;MAAM,WAAW,GAAG,eAAe,OAAO,eAAe,QAAQ;MAAI,UAAA;KAAY,CAAA,IAEjF;KAED,QAAQ;KACR,QAAQ;IACL;;EAEV;CACU,CAAA;AAEhB"}
1
+ {"version":3,"file":"Button.js","names":[],"sources":["../../../src/components/button/Button.tsx"],"sourcesContent":["'use client';\n\nimport { useContext, type ReactElement, type ReactNode, type Ref } from 'react';\nimport {\n ButtonBase,\n type ButtonBaseActionProps,\n type ButtonBaseLinkProps,\n type ButtonBaseProps,\n type ButtonBaseToggleProps,\n} from '../../primitives/ButtonBase';\nimport { TouchTarget } from '../../primitives/TouchTarget';\nimport { cn } from '../../utils/cn';\nimport {\n buttonStyles,\n type ButtonShape,\n type ButtonSize,\n type ButtonVariant,\n} from './button-styles';\nimport { ButtonContext } from './ButtonContext';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\nimport { pendingClasses, useButtonPending, type ButtonLabels } from './button-pending';\n\nexport interface ButtonClassNames {\n /** The button element itself; merged with `className`. */\n root?: string;\n /** Inner wrapper around the icons and label. */\n content?: string;\n /** The label text. */\n label?: string;\n /** The leading icon. */\n icon?: string;\n /** The slot holding the loading indicator while `pending`, centred over the hidden content. */\n pendingIndicator?: string;\n}\n\ninterface PendingProps {\n /**\n * Shows a loading indicator in place of the content while an action runs, and blocks\n * presses. The button keeps its width, its colours and keyboard focus; its accessible name\n * gains `labels.pending`, and a focused button announces it. The indicator appears after\n * 200ms, so a quick action doesn't flash. Use it for actions that take a moment, not for\n * every button in a list. A submit button doesn't submit again while pending.\n * @default false\n */\n pending?: boolean;\n /**\n * Replaces the loading indicator shown while `pending`, e.g. a `CircularProgressIndicator`\n * for a wait that may pass five seconds. It's centred over the content and takes the content\n * colour; a progress indicator also loses its track, as M3 asks inside a button.\n */\n pendingIndicator?: ReactElement;\n}\n\n/** A toggle's state is its selection: it has no pending state. */\ninterface NoPendingProps {\n /** Toggle buttons have no pending state. */\n pending?: never;\n /** Toggle buttons have no pending state. */\n pendingIndicator?: never;\n}\n\ninterface ButtonOwnProps {\n /** The M3 style: filled, tonal, outlined, elevated or text. @default \"filled\" */\n variant?: ButtonVariant;\n /** Height, padding and label size, from extra small to extra large. @default \"sm\" */\n size?: ButtonSize;\n /** Fully round corners, or square ones (still rounded, but less). @default \"round\" */\n shape?: ButtonShape;\n /**\n * Icon before the label, e.g. a Material Symbols SVG. Decorative; the label names the\n * button. M3 allows one icon, on the leading side (\"Don't use two icons in the same\n * button\"), so there's no trailing icon.\n */\n leadingIcon?: ReactElement;\n /** The visible label. Required: buttons without text are `IconButton`s. */\n children: Exclude<ReactNode, null | undefined | boolean>;\n /** Classes for each part. `classNames.root` is merged with `className`. */\n classNames?: ButtonClassNames;\n /**\n * Words the button announces. A {@link LabelsProvider} sets them for the whole app.\n */\n labels?: Partial<ButtonLabels>;\n}\n\nexport interface ButtonActionProps\n extends\n ButtonOwnProps,\n PendingProps,\n Omit<ButtonBaseActionProps, keyof ButtonOwnProps | keyof PendingProps> {}\n\nexport interface ButtonLinkProps\n extends\n ButtonOwnProps,\n PendingProps,\n Omit<ButtonBaseLinkProps, keyof ButtonOwnProps | keyof PendingProps> {}\n\nexport interface ButtonToggleProps\n extends\n Omit<ButtonOwnProps, 'variant'>,\n NoPendingProps,\n Omit<ButtonBaseToggleProps, keyof ButtonOwnProps | keyof NoPendingProps> {\n /** Text buttons have no toggle form. @default \"filled\" */\n variant?: Exclude<ButtonVariant, 'text'>;\n /**\n * Leading icon shown while selected, e.g. the filled form of `leadingIcon` (M3: \"use an\n * outlined icon when unselected, and a filled version of the icon when selected\").\n * @default leadingIcon\n */\n selectedLeadingIcon?: ReactElement;\n}\n\nexport type ButtonProps = ButtonActionProps | ButtonLinkProps | ButtonToggleProps;\n\n/**\n * M3 Expressive button: five variants, five sizes (XS–XL), round or square shape,\n * an optional toggle form, and a press morph that squares the corners while pressed.\n *\n * Renders a `<button>`, or an `<a>` when `href` is set. `className` and `style` go on\n * that element, and consumer classes always win.\n *\n * @example\n * <Button variant=\"tonal\" size=\"md\" leadingIcon={<EditIcon />} onPress={edit}>Edit</Button>\n * <Button toggle leadingIcon={<StarIcon />} selectedLeadingIcon={<StarFilledIcon />}\n * selected={starred} onSelectedChange={setStarred}>Starred</Button>\n * <Button href=\"/docs\" variant=\"text\">Docs</Button>\n * <Button type=\"submit\" pending={isPending}>Save</Button>\n */\nexport function Button({\n variant: variantProp,\n size: sizeProp,\n shape: shapeProp,\n disabled: disabledProp,\n leadingIcon,\n pendingIndicator,\n labels,\n classNames,\n className,\n children,\n ...rest\n}: ButtonProps) {\n // Only toggle props carry `selectedLeadingIcon`; the others read it as undefined.\n const { selectedLeadingIcon, ...base } = rest as typeof rest & {\n selectedLeadingIcon?: ReactElement;\n };\n const context = useContext(ButtonContext);\n const toggle = base.toggle === true;\n let variant = variantProp ?? context.variant ?? 'filled';\n if (toggle && variant === 'text') {\n if (process.env.NODE_ENV !== 'production') {\n console.warn('[@windmorph/react] Text buttons have no toggle form; using \"filled\".');\n }\n variant = 'filled';\n }\n const size = sizeProp ?? context.size ?? 'sm';\n const styles = buttonStyles({\n variant,\n size,\n shape: shapeProp ?? context.shape ?? 'round',\n toggle,\n connected: context.connected ?? 'none',\n });\n\n // Destructuring a union merges its members, so TypeScript loses the pairing of\n // `href`/`toggle` with their `ref` types. `base` still holds exactly one member's props.\n // The component tokens' hooks (button-tokens.ts), also public styling hooks for apps.\n const tokenAttributes = {\n 'data-tokens': mergeDataTokens('md-comp-button', base),\n 'data-variant': variant,\n 'data-size': size,\n };\n const pending = useButtonPending<HTMLElement>({\n pending: base.pending,\n pendingIndicator,\n labels,\n size,\n ariaLabel: base['aria-label'],\n ariaLabelledby: base['aria-labelledby'],\n ref: base.ref as Ref<HTMLElement> | undefined,\n indicatorClassName: cn(pendingClasses.indicator, classNames?.pendingIndicator),\n });\n const baseProps = {\n ...base,\n ...tokenAttributes,\n ...pending.rootProps,\n disabled: disabledProp ?? context.disabled ?? false,\n className: styles.root({ class: cn(classNames?.root, className) }),\n } as ButtonBaseProps;\n\n return (\n <ButtonBase {...baseProps}>\n {({ isSelected }) => {\n const icon = isSelected && selectedLeadingIcon ? selectedLeadingIcon : leadingIcon;\n const faces = (\n <>\n {icon && (\n <span aria-hidden=\"true\" className={styles.icon({ class: classNames?.icon })}>\n {icon}\n </span>\n )}\n <span className={styles.label({ class: classNames?.label })}>\n {children}\n {pending.nameSuffix}\n </span>\n </>\n );\n return (\n <span\n {...pending.contentProps}\n className={styles.content({\n class: cn(pending.takesPending && pendingClasses.content, classNames?.content),\n })}\n >\n {(size === 'xs' || size === 'sm') && <TouchTarget />}\n {pending.takesPending ? (\n <span className={cn(pendingClasses.faces, pendingClasses.replaced)}>{faces}</span>\n ) : (\n faces\n )}\n {pending.indicator}\n {pending.hiddenName}\n </span>\n );\n }}\n </ButtonBase>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA+HA,SAAgB,OAAO,EACrB,SAAS,aACT,MAAM,UACN,OAAO,WACP,UAAU,cACV,aACA,kBACA,QACA,YACA,WACA,UACA,GAAG,QACW;CAEd,MAAM,EAAE,qBAAqB,GAAG,SAAS;CAGzC,MAAM,UAAU,WAAW,aAAa;CACxC,MAAM,SAAS,KAAK,WAAW;CAC/B,IAAI,UAAU,eAAe,QAAQ,WAAW;CAChD,IAAI,UAAU,YAAY,QAAQ;EAChC,IAAI,QAAQ,IAAI,aAAa,cAC3B,QAAQ,KAAK,wEAAsE;EAErF,UAAU;CACZ;CACA,MAAM,OAAO,YAAY,QAAQ,QAAQ;CACzC,MAAM,SAAS,aAAa;EAC1B;EACA;EACA,OAAO,aAAa,QAAQ,SAAS;EACrC;EACA,WAAW,QAAQ,aAAa;CAClC,CAAC;CAKD,MAAM,kBAAkB;EACtB,eAAe,gBAAgB,kBAAkB,IAAI;EACrD,gBAAgB;EAChB,aAAa;CACf;CACA,MAAM,UAAU,iBAA8B;EAC5C,SAAS,KAAK;EACd;EACA;EACA;EACA,WAAW,KAAK;EAChB,gBAAgB,KAAK;EACrB,KAAK,KAAK;EACV,oBAAoB,GAAG,eAAe,WAAW,YAAY,gBAAgB;CAC/E,CAAC;CACD,MAAM,YAAY;EAChB,GAAG;EACH,GAAG;EACH,GAAG,QAAQ;EACX,UAAU,gBAAgB,QAAQ,YAAY;EAC9C,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;CACnE;CAEA,OACE,oBAAC,YAAD;EAAY,GAAI;EACZ,WAAA,EAAE,iBAAiB;GACnB,MAAM,OAAO,cAAc,sBAAsB,sBAAsB;GACvE,MAAM,QACJ,qBAAA,YAAA,EAAA,UAAA,CACG,QACC,oBAAC,QAAD;IAAM,eAAY;IAAO,WAAW,OAAO,KAAK,EAAE,OAAO,YAAY,KAAK,CAAC;IACxE,UAAA;GACG,CAAA,GAER,qBAAC,QAAD;IAAM,WAAW,OAAO,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;IAA1D,UAAA,CACG,UACA,QAAQ,UACL;GACN,CAAA,CAAA,EAAA,CAAA;GAEJ,OACE,qBAAC,QAAD;IACE,GAAI,QAAQ;IACZ,WAAW,OAAO,QAAQ,EACxB,OAAO,GAAG,QAAQ,gBAAgB,eAAe,SAAS,YAAY,OAAO,EAC/E,CAAC;IAJH,UAAA;MAMI,SAAS,QAAQ,SAAS,SAAS,oBAAC,aAAD,CAAc,CAAA;KAClD,QAAQ,eACP,oBAAC,QAAD;MAAM,WAAW,GAAG,eAAe,OAAO,eAAe,QAAQ;MAAI,UAAA;KAAY,CAAA,IAEjF;KAED,QAAQ;KACR,QAAQ;IACL;;EAEV;CACU,CAAA;AAEhB"}
@@ -1,6 +1,6 @@
1
1
  import { Density } from "../../tokens/density.js";
2
- import { ToggleGroupStateContext } from "../../primitives/ButtonBase.js";
3
2
  import { ButtonShape, ButtonSize, ButtonVariant } from "./button-styles.js";
3
+ import { ToggleGroupStateContext } from "../../primitives/ButtonBase.js";
4
4
  import { IconButtonVariant } from "../icon-button/icon-button-styles.js";
5
5
  //#region src/components/button/ButtonContext.d.ts
6
6
  /** Props a parent (such as a button group) shares with its buttons. A button's own props win. */
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { useLabels } from "../labels/use-labels.js";
2
3
  import { usePresence } from "../../primitives/use-presence.js";
3
4
  import { announcePolite, prepareAnnouncer } from "../../utils/announce.js";
4
5
  import { usePreloadOnIntent } from "../../utils/load-on-demand.js";
@@ -85,10 +86,7 @@ function useButtonPending({ pending: pendingProp, pendingIndicator, labels: labe
85
86
  const takesPending = pendingProp !== void 0;
86
87
  const pending = pendingProp === true;
87
88
  const custom = pendingIndicator !== void 0;
88
- const labels = {
89
- ...DEFAULT_LABELS,
90
- ...labelsProp
91
- };
89
+ const labels = useLabels("Button", DEFAULT_LABELS, labelsProp);
92
90
  const hiddenId = useId();
93
91
  const ref = useObjectRef(refProp);
94
92
  usePreloadOnIntent(ref, preloadLoadingIndicator, takesPending && !custom);
@@ -1 +1 @@
1
- {"version":3,"file":"button-pending.js","names":[],"sources":["../../../src/components/button/button-pending.tsx"],"sourcesContent":["'use client';\n\nimport {\n useEffect,\n useId,\n useState,\n type CSSProperties,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { usePresence } from '../../primitives/use-presence';\nimport { announcePolite, prepareAnnouncer } from '../../utils/announce';\nimport { usePreloadOnIntent } from '../../utils/load-on-demand';\nimport {\n LazyLoadingIndicator,\n preloadLoadingIndicator,\n} from '../loading-indicator/lazy-loading-indicator';\nimport type { ButtonSize } from './button-styles';\n\n/** Words a `Button` or `IconButton` announces. */\nexport interface ButtonLabels {\n /** Added to the accessible name while `pending` (\"Save, Loading\"). @default \"Loading\" */\n pending: string;\n}\n\nconst DEFAULT_LABELS: ButtonLabels = { pending: 'Loading' };\n\n/**\n * The indicator's size per button size: the icon's size, but never under M3's 24px minimum\n * for a loading indicator (\"It should be between 24dp to 240dp\").\n */\nexport const PENDING_INDICATOR_SIZE: Record<ButtonSize, number> = {\n xs: 24,\n sm: 24,\n md: 24,\n lg: 32,\n xl: 40,\n};\n\n/**\n * The swap between the content and the indicator: a crossfade on the fast effects spring\n * (opacity never overshoots), held back 200ms on the way in (M3's lower bound for a loading\n * indicator) so a quick action never flashes, and immediate on the way out. Both share the\n * content wrapper's one grid cell, so the button keeps its width.\n */\nconst fade =\n 'transition-opacity duration-(--md-sys-motion-spring-effects-fast-duration) ease-(--md-sys-motion-spring-effects-fast-easing)';\n\nexport const pendingClasses = {\n /** The content wrapper, once the button takes a `pending` prop: one grid cell. */\n content: 'inline-grid',\n /** What the indicator replaces (an icon button's icon), in the cell; hidden once it shows. */\n replaced: `[grid-area:1/1] ${fade} [[data-pending-shown]>&]:opacity-0 [[data-pending-shown]>&]:delay-200`,\n /** A button's icon and label, laid out as the content was, in the cell. */\n faces: 'inline-flex items-center justify-center gap-[inherit]',\n /**\n * The indicator's slot, in the same cell: a zero-size box centred in it, so the indicator\n * (as big as the icon, at least 24px) never changes the button's size, even where it's larger\n * than the icon. Its colour is the content colour (M3's 3:1 rule for an indicator in a button).\n */\n indicator: `pointer-events-none flex size-0 items-center justify-center self-center justify-self-center overflow-visible opacity-0 [grid-area:1/1] ${fade} [[data-pending-shown]>&]:opacity-100 [[data-pending-shown]>&]:delay-200`,\n} as const;\n\n/**\n * The indicator takes the content colour, so it reads wherever the label does (M3: an indicator\n * in a button needs 3:1 against it). A progress indicator given as `pendingIndicator` gets M3's\n * \"Progress indicators in buttons\" treatment too: the label's colour, and no track.\n */\nconst indicatorColor = {\n '--md-comp-loading-indicator-active-indicator-color': 'currentColor',\n '--md-comp-progress-indicator-active-indicator-color': 'currentColor',\n '--md-comp-progress-indicator-track-color': 'transparent',\n} as CSSProperties;\n\n/** The name suffix's style: text that takes no room (see `nameSuffix`). */\nconst HIDDEN_SUFFIX: CSSProperties = {\n fontSize: 0,\n lineHeight: 0,\n letterSpacing: 0,\n wordSpacing: 0,\n};\n\ninterface PendingOptions<E extends HTMLElement> {\n pending: boolean | undefined;\n pendingIndicator: ReactElement | undefined;\n labels: Partial<ButtonLabels> | undefined;\n size: ButtonSize;\n ariaLabel: string | undefined;\n ariaLabelledby: string | undefined;\n ref: Ref<E> | undefined;\n /** Classes for the indicator's slot (`classNames.pendingIndicator`). */\n indicatorClassName: string | undefined;\n}\n\n/** Text of `node` as a screen reader reads it: `aria-hidden` subtrees left out. */\nfunction readableText(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? '';\n if (node instanceof Element && node.getAttribute('aria-hidden') === 'true') return '';\n return Array.from(node.childNodes, readableText).join('');\n}\n\n/** The element's accessible name, for the cases a button can be named by. */\nfunction accessibleName(element: HTMLElement): string {\n const labelledby = element.getAttribute('aria-labelledby');\n if (labelledby) {\n return labelledby\n .split(/\\s+/)\n .map((id) => document.getElementById(id)?.textContent?.trim() ?? '')\n .filter(Boolean)\n .join(' ');\n }\n return element.getAttribute('aria-label') ?? readableText(element).trim();\n}\n\n/**\n * The pending state of `Button` and `IconButton` (React Aria's pending `Button`): the props for\n * the root, the accessible-name suffix, and the indicator's slot.\n *\n * - The name gains `labels.pending` while pending, from the first frame: appended to an\n * `aria-label`, an extra id on `aria-labelledby`, or a visually hidden suffix at the end of the label.\n * - When it turns on while focus is in the button, the new name is announced politely, once.\n * - The indicator (the core's `LoadingIndicator`, loaded on demand, or `pendingIndicator`) is\n * preloaded once the button takes a `pending` prop at all, when the browser is idle or the\n * pointer or focus arrives. The content hides only once the indicator is mounted\n * (`data-pending-shown`), so a server-rendered pending button shows its label until then.\n */\nexport function useButtonPending<E extends HTMLElement>({\n pending: pendingProp,\n pendingIndicator,\n labels: labelsProp,\n size,\n ariaLabel,\n ariaLabelledby,\n ref: refProp,\n indicatorClassName,\n}: PendingOptions<E>) {\n const takesPending = pendingProp !== undefined;\n const pending = pendingProp === true;\n const custom = pendingIndicator !== undefined;\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const hiddenId = useId();\n\n // The element, for the announcement and the preload; it also forwards to the app's ref.\n const ref = useObjectRef(refProp);\n\n usePreloadOnIntent(ref, preloadLoadingIndicator, takesPending && !custom);\n useEffect(() => {\n if (takesPending) prepareAnnouncer();\n }, [takesPending]);\n\n const [loaded, setLoaded] = useState(false);\n useEffect(() => {\n if (!pending || custom || loaded) return;\n let live = true;\n preloadLoadingIndicator().then(\n () => live && setLoaded(true),\n () => undefined,\n );\n return () => {\n live = false;\n };\n }, [pending, custom, loaded]);\n\n const shown = pending && (custom || loaded);\n const { isPresent, exitProps } = usePresence(shown);\n\n useEffect(() => {\n const element = ref.current;\n if (!pending || !element || !element.contains(document.activeElement)) return;\n // The name already carries the suffix, as it does for a screen reader.\n const name = accessibleName(element);\n if (name) announcePolite(name);\n }, [ref, pending]);\n\n let nameProps: { 'aria-label'?: string; 'aria-labelledby'?: string } = {};\n let hiddenName: ReactNode = null;\n let nameSuffix: ReactNode = null;\n if (pending) {\n if (ariaLabelledby) {\n nameProps = { 'aria-labelledby': `${ariaLabelledby} ${hiddenId}` };\n hiddenName = (\n <span id={hiddenId} hidden>\n {labels.pending}\n </span>\n );\n } else if (ariaLabel) {\n nameProps = { 'aria-label': `${ariaLabel}, ${labels.pending}` };\n } else {\n // Inline text at the label's end, sized to nothing: browsers join the text of a block,\n // a grid item or an absolutely positioned box (`sr-only`) to the label with a space\n // (\"Save , Loading\"), but run inline text on (\"Save, Loading\"). No tracking either (the\n // label's is in rem, so it would still space each hidden letter out), nor line height\n // (its half-leading, centred on the zero-size text, would grow the label's line).\n nameSuffix = <span style={HIDDEN_SUFFIX}>{`, ${labels.pending}`}</span>;\n }\n }\n\n const indicator = takesPending ? (\n <span\n aria-hidden=\"true\"\n data-slot=\"pending-indicator\"\n className={indicatorClassName}\n style={indicatorColor}\n {...exitProps}\n >\n {isPresent &&\n (pendingIndicator ?? (\n <LazyLoadingIndicator size={PENDING_INDICATOR_SIZE[size]} aria-label={labels.pending} />\n ))}\n </span>\n ) : null;\n\n return {\n /** Whether the content wrapper holds the grid cell (the button takes `pending`). */\n takesPending,\n rootProps: { ref, ...nameProps, 'data-pending': pending || undefined },\n contentProps: { 'data-pending-shown': shown || undefined },\n /** The element `aria-labelledby` adds while pending, anywhere in the button. */\n hiddenName,\n /** The suffix of a name taken from the content, to put at the end of the label's text. */\n nameSuffix,\n indicator,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"button-pending.js","names":[],"sources":["../../../src/components/button/button-pending.tsx"],"sourcesContent":["'use client';\n\nimport {\n useEffect,\n useId,\n useState,\n type CSSProperties,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { useObjectRef } from 'react-aria';\nimport { useLabels } from '../labels/use-labels';\nimport { usePresence } from '../../primitives/use-presence';\nimport { announcePolite, prepareAnnouncer } from '../../utils/announce';\nimport { usePreloadOnIntent } from '../../utils/load-on-demand';\nimport {\n LazyLoadingIndicator,\n preloadLoadingIndicator,\n} from '../loading-indicator/lazy-loading-indicator';\nimport type { ButtonSize } from './button-styles';\n\n/** Words a `Button` or `IconButton` announces. */\nexport interface ButtonLabels {\n /** Added to the accessible name while `pending` (\"Save, Loading\"). @default \"Loading\" */\n pending: string;\n}\n\nconst DEFAULT_LABELS: ButtonLabels = { pending: 'Loading' };\n\n/**\n * The indicator's size per button size: the icon's size, but never under M3's 24px minimum\n * for a loading indicator (\"It should be between 24dp to 240dp\").\n */\nexport const PENDING_INDICATOR_SIZE: Record<ButtonSize, number> = {\n xs: 24,\n sm: 24,\n md: 24,\n lg: 32,\n xl: 40,\n};\n\n/**\n * The swap between the content and the indicator: a crossfade on the fast effects spring\n * (opacity never overshoots), held back 200ms on the way in (M3's lower bound for a loading\n * indicator) so a quick action never flashes, and immediate on the way out. Both share the\n * content wrapper's one grid cell, so the button keeps its width.\n */\nconst fade =\n 'transition-opacity duration-(--md-sys-motion-spring-effects-fast-duration) ease-(--md-sys-motion-spring-effects-fast-easing)';\n\nexport const pendingClasses = {\n /** The content wrapper, once the button takes a `pending` prop: one grid cell. */\n content: 'inline-grid',\n /** What the indicator replaces (an icon button's icon), in the cell; hidden once it shows. */\n replaced: `[grid-area:1/1] ${fade} [[data-pending-shown]>&]:opacity-0 [[data-pending-shown]>&]:delay-200`,\n /** A button's icon and label, laid out as the content was, in the cell. */\n faces: 'inline-flex items-center justify-center gap-[inherit]',\n /**\n * The indicator's slot, in the same cell: a zero-size box centred in it, so the indicator\n * (as big as the icon, at least 24px) never changes the button's size, even where it's larger\n * than the icon. Its colour is the content colour (M3's 3:1 rule for an indicator in a button).\n */\n indicator: `pointer-events-none flex size-0 items-center justify-center self-center justify-self-center overflow-visible opacity-0 [grid-area:1/1] ${fade} [[data-pending-shown]>&]:opacity-100 [[data-pending-shown]>&]:delay-200`,\n} as const;\n\n/**\n * The indicator takes the content colour, so it reads wherever the label does (M3: an indicator\n * in a button needs 3:1 against it). A progress indicator given as `pendingIndicator` gets M3's\n * \"Progress indicators in buttons\" treatment too: the label's colour, and no track.\n */\nconst indicatorColor = {\n '--md-comp-loading-indicator-active-indicator-color': 'currentColor',\n '--md-comp-progress-indicator-active-indicator-color': 'currentColor',\n '--md-comp-progress-indicator-track-color': 'transparent',\n} as CSSProperties;\n\n/** The name suffix's style: text that takes no room (see `nameSuffix`). */\nconst HIDDEN_SUFFIX: CSSProperties = {\n fontSize: 0,\n lineHeight: 0,\n letterSpacing: 0,\n wordSpacing: 0,\n};\n\ninterface PendingOptions<E extends HTMLElement> {\n pending: boolean | undefined;\n pendingIndicator: ReactElement | undefined;\n labels: Partial<ButtonLabels> | undefined;\n size: ButtonSize;\n ariaLabel: string | undefined;\n ariaLabelledby: string | undefined;\n ref: Ref<E> | undefined;\n /** Classes for the indicator's slot (`classNames.pendingIndicator`). */\n indicatorClassName: string | undefined;\n}\n\n/** Text of `node` as a screen reader reads it: `aria-hidden` subtrees left out. */\nfunction readableText(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? '';\n if (node instanceof Element && node.getAttribute('aria-hidden') === 'true') return '';\n return Array.from(node.childNodes, readableText).join('');\n}\n\n/** The element's accessible name, for the cases a button can be named by. */\nfunction accessibleName(element: HTMLElement): string {\n const labelledby = element.getAttribute('aria-labelledby');\n if (labelledby) {\n return labelledby\n .split(/\\s+/)\n .map((id) => document.getElementById(id)?.textContent?.trim() ?? '')\n .filter(Boolean)\n .join(' ');\n }\n return element.getAttribute('aria-label') ?? readableText(element).trim();\n}\n\n/**\n * The pending state of `Button` and `IconButton` (React Aria's pending `Button`): the props for\n * the root, the accessible-name suffix, and the indicator's slot.\n *\n * - The name gains `labels.pending` while pending, from the first frame: appended to an\n * `aria-label`, an extra id on `aria-labelledby`, or a visually hidden suffix at the end of the label.\n * - When it turns on while focus is in the button, the new name is announced politely, once.\n * - The indicator (the core's `LoadingIndicator`, loaded on demand, or `pendingIndicator`) is\n * preloaded once the button takes a `pending` prop at all, when the browser is idle or the\n * pointer or focus arrives. The content hides only once the indicator is mounted\n * (`data-pending-shown`), so a server-rendered pending button shows its label until then.\n */\nexport function useButtonPending<E extends HTMLElement>({\n pending: pendingProp,\n pendingIndicator,\n labels: labelsProp,\n size,\n ariaLabel,\n ariaLabelledby,\n ref: refProp,\n indicatorClassName,\n}: PendingOptions<E>) {\n const takesPending = pendingProp !== undefined;\n const pending = pendingProp === true;\n const custom = pendingIndicator !== undefined;\n const labels = useLabels('Button', DEFAULT_LABELS, labelsProp);\n const hiddenId = useId();\n\n // The element, for the announcement and the preload; it also forwards to the app's ref.\n const ref = useObjectRef(refProp);\n\n usePreloadOnIntent(ref, preloadLoadingIndicator, takesPending && !custom);\n useEffect(() => {\n if (takesPending) prepareAnnouncer();\n }, [takesPending]);\n\n const [loaded, setLoaded] = useState(false);\n useEffect(() => {\n if (!pending || custom || loaded) return;\n let live = true;\n preloadLoadingIndicator().then(\n () => live && setLoaded(true),\n () => undefined,\n );\n return () => {\n live = false;\n };\n }, [pending, custom, loaded]);\n\n const shown = pending && (custom || loaded);\n const { isPresent, exitProps } = usePresence(shown);\n\n useEffect(() => {\n const element = ref.current;\n if (!pending || !element || !element.contains(document.activeElement)) return;\n // The name already carries the suffix, as it does for a screen reader.\n const name = accessibleName(element);\n if (name) announcePolite(name);\n }, [ref, pending]);\n\n let nameProps: { 'aria-label'?: string; 'aria-labelledby'?: string } = {};\n let hiddenName: ReactNode = null;\n let nameSuffix: ReactNode = null;\n if (pending) {\n if (ariaLabelledby) {\n nameProps = { 'aria-labelledby': `${ariaLabelledby} ${hiddenId}` };\n hiddenName = (\n <span id={hiddenId} hidden>\n {labels.pending}\n </span>\n );\n } else if (ariaLabel) {\n nameProps = { 'aria-label': `${ariaLabel}, ${labels.pending}` };\n } else {\n // Inline text at the label's end, sized to nothing: browsers join the text of a block,\n // a grid item or an absolutely positioned box (`sr-only`) to the label with a space\n // (\"Save , Loading\"), but run inline text on (\"Save, Loading\"). No tracking either (the\n // label's is in rem, so it would still space each hidden letter out), nor line height\n // (its half-leading, centred on the zero-size text, would grow the label's line).\n nameSuffix = <span style={HIDDEN_SUFFIX}>{`, ${labels.pending}`}</span>;\n }\n }\n\n const indicator = takesPending ? (\n <span\n aria-hidden=\"true\"\n data-slot=\"pending-indicator\"\n className={indicatorClassName}\n style={indicatorColor}\n {...exitProps}\n >\n {isPresent &&\n (pendingIndicator ?? (\n <LazyLoadingIndicator size={PENDING_INDICATOR_SIZE[size]} aria-label={labels.pending} />\n ))}\n </span>\n ) : null;\n\n return {\n /** Whether the content wrapper holds the grid cell (the button takes `pending`). */\n takesPending,\n rootProps: { ref, ...nameProps, 'data-pending': pending || undefined },\n contentProps: { 'data-pending-shown': shown || undefined },\n /** The element `aria-labelledby` adds while pending, anywhere in the button. */\n hiddenName,\n /** The suffix of a name taken from the content, to put at the end of the label's text. */\n nameSuffix,\n indicator,\n };\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { VariantProps } from "../../utils/tv.js";
2
- import { ButtonShape, ButtonSize, ButtonVariant } from "../../components/button/button-styles.js";
2
+ import { ButtonShape, ButtonSize, ButtonVariant } from "../button/button-styles.js";
3
3
  import { buttonGroupStyles } from "./button-group-styles.js";
4
4
  import { ComponentPropsWithRef, ReactNode } from "react";
5
- //#region src/composites/button-group/ButtonGroup.d.ts
5
+ //#region src/components/button-group/ButtonGroup.d.ts
6
6
  export type ButtonGroupVariant = NonNullable<VariantProps<typeof buttonGroupStyles>['variant']>;
7
7
  export type ButtonGroupSelectionMode = 'none' | 'single' | 'multiple';
8
8
  /** Text the group adds for assistive tech and its tooltip; translate it for your locale. */
@@ -91,8 +91,8 @@ export interface ButtonGroupProps extends Omit<ComponentPropsWithRef<'div'>, 'on
91
91
  */
92
92
  overflow?: boolean;
93
93
  /**
94
- * Text for assistive tech, e.g. the overflow indicator's name.
95
- * @default { more: "More options" }
94
+ * Text for assistive tech, e.g. the overflow indicator's name. @default { more: "More options" }
95
+ * A {@link LabelsProvider} sets them for the whole app.
96
96
  */
97
97
  labels?: Partial<ButtonGroupLabels>;
98
98
  /** Classes for each part. `classNames.root` is merged with `className`. */
@@ -1,12 +1,13 @@
1
1
  "use client";
2
2
  import { cn } from "../../utils/cn.js";
3
+ import { useLabels } from "../labels/use-labels.js";
3
4
  import { ToggleGroupStateContext } from "../../primitives/ButtonBase.js";
4
- import { ButtonContext } from "../../components/button/ButtonContext.js";
5
- import { Button } from "../../components/button/Button.js";
5
+ import { ButtonContext } from "../button/ButtonContext.js";
6
+ import { Button } from "../button/Button.js";
6
7
  import { useWarnOnce } from "../../utils/warn-once.js";
7
- import { IconButton } from "../../components/icon-button/IconButton.js";
8
+ import { IconButton } from "../icon-button/IconButton.js";
8
9
  import { buttonGroupStyles } from "./button-group-styles.js";
9
- import { MenuTrigger } from "../../components/menu/menu-trigger.js";
10
+ import { MenuTrigger } from "../menu/menu-trigger.js";
10
11
  import { DomDirectionLocale } from "../../primitives/DomDirectionLocale.js";
11
12
  import { focusCheckedRadio, notePointerDown, radioGroupKeyDown } from "../../utils/radio-group-keys.js";
12
13
  import { toolbarKeyDown } from "../../utils/toolbar-keys.js";
@@ -17,7 +18,7 @@ import { Children, isValidElement, useContext, useLayoutEffect, useMemo, useRef,
17
18
  import { jsx, jsxs } from "react/jsx-runtime";
18
19
  import { mergeProps, mergeRefs, useObjectRef, useToggleButtonGroup, useToolbar } from "react-aria";
19
20
  import { useToggleGroupState } from "react-stately";
20
- //#region src/composites/button-group/ButtonGroup.tsx
21
+ //#region src/components/button-group/ButtonGroup.tsx
21
22
  /** Material Symbols `more_vert` (Apache-2.0), the overflow indicator's icon. */
22
23
  function MoreVertIcon() {
23
24
  return /* @__PURE__ */ jsx("svg", {
@@ -72,7 +73,8 @@ function ButtonGroup(props) {
72
73
  directionRef
73
74
  }) });
74
75
  }
75
- function ButtonGroupRoot({ variant = "standard", size, shape, buttonVariant, disabled, selectionMode = "none", selectedKeys, defaultSelectedKeys, onSelectionChange, deselectable = true, expandedRatio, overflow = false, fullWidth = true, labels, className, classNames, children, ref, directionRef, ...props }) {
76
+ function ButtonGroupRoot({ variant = "standard", size, shape, buttonVariant, disabled, selectionMode = "none", selectedKeys, defaultSelectedKeys, onSelectionChange, deselectable = true, expandedRatio, overflow = false, fullWidth = true, labels: labelsProp, className, classNames, children, ref, directionRef, ...props }) {
77
+ const labels = useLabels("ButtonGroup", DEFAULT_LABELS, labelsProp);
76
78
  const domRef = useGroupRef(ref, directionRef);
77
79
  const parent = useContext(ButtonContext);
78
80
  const hasSelection = selectionMode !== "none";
@@ -140,7 +142,7 @@ function ButtonGroupRoot({ variant = "standard", size, shape, buttonVariant, dis
140
142
  groupRef: domRef,
141
143
  hidden: items.slice(visible),
142
144
  offset: visible,
143
- label: labels?.more ?? DEFAULT_LABELS.more,
145
+ label: labels.more,
144
146
  selectionMode,
145
147
  selectedKeys: state.selectedKeys,
146
148
  disabled: Boolean(disabled ?? parent.disabled),
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ButtonGroup.js","names":[],"sources":["../../../src/components/button-group/ButtonGroup.tsx"],"sourcesContent":["'use client';\n\nimport {\n Children,\n isValidElement,\n useContext,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type HTMLAttributes,\n type Key,\n type ReactElement,\n type ReactNode,\n type Ref,\n type RefCallback,\n type RefObject,\n} from 'react';\nimport { mergeProps, mergeRefs, useObjectRef, useToggleButtonGroup, useToolbar } from 'react-aria';\nimport { useToggleGroupState } from 'react-stately';\nimport { useLabels } from '../labels/use-labels';\nimport { Button } from '../button/Button';\nimport type { ButtonShape, ButtonSize, ButtonVariant } from '../button/button-styles';\nimport {\n ButtonContext,\n ToggleGroupStateContext,\n type ButtonContextValue,\n} from '../button/ButtonContext';\nimport { IconButton } from '../icon-button/IconButton';\nimport type { IconButtonVariant } from '../icon-button/icon-button-styles';\nimport { MenuTrigger } from '../menu/menu-trigger';\nimport { DomDirectionLocale } from '../../primitives/DomDirectionLocale';\nimport { cn } from '../../utils/cn';\nimport {\n focusCheckedRadio,\n notePointerDown,\n radioGroupKeyDown,\n} from '../../utils/radio-group-keys';\nimport { toolbarKeyDown } from '../../utils/toolbar-keys';\nimport type { VariantProps } from '../../utils/tv';\nimport { useWarnOnce } from '../../utils/warn-once';\nimport { buttonGroupStyles } from './button-group-styles';\nimport { LazyOverflowMenuList } from './lazy-overflow-menu';\nimport { useOverflow } from './use-overflow';\nimport { usePressExpansion } from './use-press-expansion';\n\n/** Material Symbols `more_vert` (Apache-2.0), the overflow indicator's icon. */\nfunction MoreVertIcon() {\n return (\n <svg viewBox=\"0 -960 960 960\" fill=\"currentColor\">\n <path d=\"M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z\" />\n </svg>\n );\n}\n\nexport type ButtonGroupVariant = NonNullable<VariantProps<typeof buttonGroupStyles>['variant']>;\nexport type ButtonGroupSelectionMode = 'none' | 'single' | 'multiple';\n\n/** Text the group adds for assistive tech and its tooltip; translate it for your locale. */\nexport interface ButtonGroupLabels {\n /** The overflow indicator's name and tooltip. @default \"More options\" */\n more: string;\n}\n\nconst DEFAULT_LABELS: ButtonGroupLabels = { more: 'More options' };\n\nexport interface ButtonGroupClassNames {\n /** The group's outer element; `className` is merged here too. */\n root?: string;\n /** The overflow indicator, the \"More options\" icon button (`overflow`). */\n overflowButton?: string;\n /** The overflow indicator's menu of folded buttons (`overflow`). */\n overflowMenu?: string;\n}\n\nexport interface ButtonGroupProps extends Omit<\n ComponentPropsWithRef<'div'>,\n 'onChange' | 'defaultValue'\n> {\n /**\n * `standard` spaces the buttons apart (18 / 12 / 8px by size); `connected` joins them 2px\n * apart with shared inner corners across the container's width (the M3 Expressive\n * replacement for segmented buttons). A standard group grows a pressed button into its\n * neighbours' space; a connected one only changes its shape unless `expandedRatio` is set.\n * @default \"standard\"\n */\n variant?: ButtonGroupVariant;\n /**\n * Whether a `connected` group spans its container (M3's guidance, and the default). `false`\n * sizes the group to its buttons, for one beside other controls; the buttons stay equally\n * wide. Has no effect on a `standard` group or one with `overflow`.\n * @default true\n */\n fullWidth?: boolean;\n /**\n * Size shared by every button and icon button in the group; it also sets the spacing.\n * Unset, the buttons keep their own (or an enclosing toolbar's) size.\n * @default \"sm\"\n */\n size?: ButtonSize;\n /**\n * Shape shared by the buttons. A `square` connected group gives its outer corners the\n * size of its inner ones (4 / 8 / 8 / 16 / 20px). Unset, the buttons keep their own.\n * @default \"round\"\n */\n shape?: ButtonShape;\n /**\n * Variant shared by every `Button` in the group. `filled`, `tonal` and `outlined` reach\n * the `IconButton`s too, so a group never mixes colour styles; a button's own `variant`\n * wins. Unset, the buttons keep their own.\n * @default \"filled\"\n */\n buttonVariant?: ButtonVariant;\n /** Disables every button in the group. @default false */\n disabled?: boolean;\n /**\n * Lets toggle buttons with a `value` act as one selection. `single` behaves like\n * radio buttons, `multiple` like checkboxes. @default \"none\"\n */\n selectionMode?: ButtonGroupSelectionMode;\n /** Controlled selection: the `value`s of the selected toggle buttons. */\n selectedKeys?: Iterable<string>;\n /** Initial selection when uncontrolled. */\n defaultSelectedKeys?: Iterable<string>;\n /** Called with the selected toggle buttons' `value`s when the selection changes. */\n onSelectionChange?: (keys: Set<string>) => void;\n /**\n * The last selected button can be deselected, leaving none. `false` keeps at least one\n * selected. @default true\n */\n deselectable?: boolean;\n /**\n * How much a pressed button grows, as a fraction of its width; its neighbours shrink by the\n * same amount and the group keeps its width. `0` turns it off. Standard groups default to\n * 0.15. Connected groups default to 0 (the spec: they \"don't affect adjacent buttons\", as\n * in Compose's connected sample); set a ratio to opt in, as Compose can by combining a\n * button group's `animateWidth` with the connected shapes. Under reduced motion there's\n * no bump. @default 0.15, or 0 when `variant` is \"connected\"\n */\n expandedRatio?: number;\n /**\n * Folds the trailing buttons that don't fit into a menu behind a trailing \"More options\"\n * icon button (M3: \"Buttons at the trailing edge of the button group can be customized to\n * collapse into an overflow menu at smaller breakpoints\"). The group then fills its\n * container's width and re-measures as it resizes. Each folded button becomes a menu\n * item with its label (an icon button's `aria-label`) and icon; disabled buttons stay\n * disabled, and toggles show their selection. Choosing an item presses the button.\n * @default false\n */\n overflow?: boolean;\n /**\n * Text for assistive tech, e.g. the overflow indicator's name. @default { more: \"More options\" }\n * A {@link LabelsProvider} sets them for the whole app.\n */\n labels?: Partial<ButtonGroupLabels>;\n /** Classes for each part. `classNames.root` is merged with `className`. */\n classNames?: ButtonGroupClassNames;\n children?: ReactNode;\n}\n\n/** The props the group reads from its children for the overflow menu and warnings. */\ninterface ChildProps {\n variant?: string;\n toggle?: boolean;\n value?: string;\n disabled?: boolean;\n children?: ReactNode;\n leadingIcon?: ReactElement;\n selectedLeadingIcon?: ReactElement;\n icon?: ReactElement;\n selectedIcon?: ReactElement;\n 'aria-label'?: string;\n}\n\nconst ICON_BUTTON_VARIANTS: readonly string[] = ['filled', 'tonal', 'outlined'];\n\n/** The icon button variant a group's `buttonVariant` implies, if it exists for icon buttons. */\nfunction iconVariantOf(variant: ButtonVariant | undefined): IconButtonVariant | undefined {\n return variant && ICON_BUTTON_VARIANTS.includes(variant)\n ? (variant as IconButtonVariant)\n : undefined;\n}\n\ntype Position = NonNullable<ButtonContextValue['connected']>;\n\nfunction positionOf(index: number, count: number): Position | undefined {\n if (count < 2) return undefined;\n if (index === 0) return 'leading';\n if (index === count - 1) return 'trailing';\n return 'middle';\n}\n\n/** The group's ref, also reporting its element to `DomDirectionLocale`. */\nfunction useGroupRef(ref: Ref<HTMLDivElement> | undefined, directionRef: RefCallback<HTMLElement>) {\n // Arrow keys follow the laid-out direction, which React Aria reads from its locale.\n const merged = useMemo(\n () => mergeRefs<HTMLDivElement>(ref, directionRef as RefCallback<HTMLDivElement>),\n [ref, directionRef],\n );\n return useObjectRef<HTMLDivElement>(merged);\n}\n\n/**\n * M3 Expressive button group. Shares size, shape, variant and disabled with its `Button`\n * and `IconButton` children. A standard group grows a pressed button while its neighbours\n * shrink; a `connected` group joins the buttons with shared inner corners and, by default,\n * only changes the pressed button's shape (`expandedRatio` opts it into the bump). Tab reaches the group and the arrow keys move\n * between its buttons, wrapping, with Home and End (a `toolbar`). With `selectionMode`, toggle\n * buttons that have a `value` form a single or multiple selection; a single selection is a\n * radio group, whose arrow keys select as they move.\n *\n * Children must be direct `Button` / `IconButton` elements so each gets its position.\n *\n * @example\n * <ButtonGroup variant=\"connected\" selectionMode=\"single\" defaultSelectedKeys={['week']}\n * aria-label=\"Calendar view\">\n * <Button toggle value=\"day\">Day</Button>\n * <Button toggle value=\"week\">Week</Button>\n * <Button toggle value=\"month\">Month</Button>\n * </ButtonGroup>\n */\nexport function ButtonGroup(props: ButtonGroupProps) {\n return (\n <DomDirectionLocale>\n {(directionRef) => <ButtonGroupRoot {...props} directionRef={directionRef} />}\n </DomDirectionLocale>\n );\n}\n\nfunction ButtonGroupRoot({\n variant = 'standard',\n size,\n shape,\n buttonVariant,\n disabled,\n selectionMode = 'none',\n selectedKeys,\n defaultSelectedKeys,\n onSelectionChange,\n deselectable = true,\n expandedRatio,\n overflow = false,\n fullWidth = true,\n labels: labelsProp,\n className,\n classNames,\n children,\n ref,\n directionRef,\n ...props\n}: ButtonGroupProps & { directionRef: RefCallback<HTMLElement> }) {\n const labels = useLabels('ButtonGroup', DEFAULT_LABELS, labelsProp);\n const domRef = useGroupRef(ref, directionRef);\n const parent = useContext(ButtonContext);\n const hasSelection = selectionMode !== 'none';\n\n // React Stately reports a change even when the selection is unchanged (e.g. clicking\n // the only selected button of a group that isn't `deselectable`); only real changes reach callers.\n const currentKeys = useRef<ReadonlySet<Key>>(new Set());\n const handleSelectionChange = (keys: Set<Key>) => {\n const previous = currentKeys.current;\n if (keys.size === previous.size && [...keys].every((key) => previous.has(key))) return;\n onSelectionChange?.(keys as Set<string>);\n };\n\n const state = useToggleGroupState({\n selectionMode: hasSelection ? selectionMode : 'single',\n selectedKeys,\n defaultSelectedKeys,\n onSelectionChange: handleSelectionChange,\n disallowEmptySelection: !deselectable,\n isDisabled: disabled,\n });\n const { groupProps } = useToggleButtonGroup(\n { ...props, isDisabled: disabled, orientation: 'horizontal' },\n state,\n domRef,\n );\n // Without a selection the group is a React Aria toolbar: Tab enters and leaves it as one\n // stop, and the arrow keys move between its buttons.\n const { toolbarProps } = useToolbar({ ...props, orientation: 'horizontal' }, domRef);\n const groupAria = withGroupKeys(\n hasSelection ? groupProps : toolbarProps,\n selectionMode === 'single',\n );\n\n useLayoutEffect(() => {\n currentKeys.current = state.selectedKeys;\n });\n\n usePressExpansion(domRef, expandedRatio ?? (variant === 'standard' ? 0.15 : 0));\n\n const groupSize = size ?? parent.size ?? 'sm';\n const iconButtonVariant = iconVariantOf(buttonVariant);\n const shared = useMemo<ButtonContextValue>(\n () => ({\n variant: buttonVariant ?? parent.variant,\n size: size ?? parent.size,\n shape: shape ?? parent.shape,\n disabled: disabled ?? parent.disabled,\n iconButtonVariant: iconButtonVariant ?? parent.iconButtonVariant,\n density: parent.density,\n }),\n [buttonVariant, iconButtonVariant, size, shape, disabled, parent],\n );\n\n const items = Children.toArray(children).filter(isValidElement) as ReactElement<ChildProps>[];\n const visible = useOverflow(domRef, overflow, items.length);\n const overflowing = overflow && visible < items.length;\n\n useGroupWarnings(variant, items, buttonVariant, iconButtonVariant);\n\n // Connected corners follow the buttons that show, the overflow indicator last.\n const shown = overflowing ? visible + 1 : items.length;\n const contextFor = (index: number): ButtonContextValue =>\n variant === 'connected' ? { ...shared, connected: positionOf(index, shown) } : shared;\n const content = items.map((child, index) => (\n <ButtonContext key={child.key ?? index} value={contextFor(Math.min(index, shown - 1))}>\n {child}\n </ButtonContext>\n ));\n if (overflow) {\n content.push(\n <ButtonContext key=\"overflow\" value={contextFor(shown - 1)}>\n <OverflowMenu\n groupRef={domRef}\n hidden={items.slice(visible)}\n offset={visible}\n label={labels.more}\n selectionMode={selectionMode}\n selectedKeys={state.selectedKeys}\n disabled={Boolean(disabled ?? parent.disabled)}\n classNames={classNames}\n />\n </ButtonContext>,\n );\n }\n\n return (\n <div\n {...mergeProps(props, groupAria)}\n ref={domRef}\n // Public styling hooks for apps, as on Button: the group's variant and the size it spaces\n // its buttons for (its own, an enclosing toolbar's, or `sm`).\n data-variant={variant}\n data-size={groupSize}\n className={cn(\n buttonGroupStyles({ variant, size: groupSize, overflow, fullWidth }),\n classNames?.root,\n className,\n )}\n >\n {hasSelection ? (\n <ToggleGroupStateContext value={state}>{content}</ToggleGroupStateContext>\n ) : (\n content\n )}\n </div>\n );\n}\n\n/**\n * The group's arrow keys. React Aria's toolbar keys (kept for Tab, which enters and leaves\n * the group as one stop) are replaced: a toolbar's arrows wrap and Home and End reach its\n * ends (WAI-ARIA APG toolbar), and a single selection is a radio group whose arrows also\n * select (APG radio group, as `RadioGroup` does) and which Tab enters at the selected\n * button. A group nested in a toolbar leaves its keys to the toolbar, as React Aria does.\n */\nfunction withGroupKeys(\n aria: HTMLAttributes<HTMLElement>,\n single: boolean,\n): HTMLAttributes<HTMLElement> {\n const ariaKeyDown = aria.onKeyDownCapture;\n if (!ariaKeyDown) return aria;\n return {\n ...aria,\n onKeyDownCapture: (event) => {\n if (event.key === 'Tab') ariaKeyDown(event);\n else if (single) radioGroupKeyDown(event);\n else toolbarKeyDown(event, 'horizontal');\n },\n onFocus: single ? (event) => void focusCheckedRadio(event) : undefined,\n onPointerDownCapture: single ? notePointerDown : undefined,\n };\n}\n\n/**\n * Development warnings, once per group, for what the M3 button group guidelines advise\n * against: standard icon buttons and text buttons in a group (\"they have no container\n * treatment\"), and a connected group with nothing to select (\"Avoid using a connected\n * group when none of the buttons can be toggled\"). Only the variants the group or the\n * buttons set count: a toolbar's standard default doesn't.\n */\nfunction useGroupWarnings(\n variant: ButtonGroupVariant,\n items: ReactElement<ChildProps>[],\n buttonVariant: ButtonVariant | undefined,\n iconButtonVariant: IconButtonVariant | undefined,\n) {\n const containerless = items.some((child) =>\n child.type === IconButton\n ? (child.props.variant ?? iconButtonVariant) === 'standard'\n : child.type === Button && (child.props.variant ?? buttonVariant) === 'text',\n );\n const nothingToSelect =\n variant === 'connected' && items.length > 0 && !items.some((child) => child.props.toggle);\n useWarnOnce(\n containerless,\n '[@windmorph/react] Avoid standard icon buttons and text buttons in a ButtonGroup (M3): they have no container, so the group has nothing to shape.',\n );\n useWarnOnce(\n nothingToSelect,\n '[@windmorph/react] A connected ButtonGroup is for selection (M3): use toggle buttons in it, or a standard group for actions.',\n );\n}\n\ninterface OverflowMenuProps {\n groupRef: RefObject<HTMLDivElement | null>;\n /** The folded buttons, in order. */\n hidden: ReactElement<ChildProps>[];\n /** The index of the first folded button among the group's children. */\n offset: number;\n label: string;\n selectionMode: ButtonGroupSelectionMode;\n selectedKeys: ReadonlySet<Key>;\n disabled: boolean;\n classNames: ButtonGroupClassNames | undefined;\n}\n\n/**\n * The overflow indicator (Compose `ButtonGroupDefaults.OverflowIndicator`: a filled icon\n * button with `more_vert` and a \"More options\" tooltip, here the group's icon button\n * variant) and its menu. Choosing an item clicks the folded button itself, so its own\n * press handler, link, toggle or group selection runs as if it had been pressed. The menu's\n * code loads once a button folds (see `LazyOverflowMenuList`); the indicator is always\n * rendered, so the group can measure it.\n */\nfunction OverflowMenu({\n groupRef,\n hidden,\n offset,\n label,\n selectionMode,\n selectedKeys,\n disabled,\n classNames,\n}: OverflowMenuProps) {\n // Folded standalone toggles keep their state inside them: read it when the menu opens\n // and after each choice.\n const [pressed, setPressed] = useState<ReadonlySet<string>>(new Set());\n const elementAt = (key: string) =>\n groupRef.current?.children[Number(key)] as HTMLElement | undefined;\n const keyOf = (index: number) => String(offset + index);\n const readPressed = () =>\n setPressed(\n new Set(\n hidden\n .map((_, index) => keyOf(index))\n .filter((key) => elementAt(key)?.getAttribute('aria-pressed') === 'true'),\n ),\n );\n\n // A choice made with the mouse closed the menu with focus still on its item, which then\n // left with the menu and dropped focus on <body>. Once the menu has closed, focus still in\n // it (or on the page) goes back to the indicator, as after a keyboard choice (2026-10-15).\n const indicator = useRef<HTMLElement | null>(null);\n const keepFocus = () =>\n requestAnimationFrame(() => {\n const active = document.activeElement;\n if (!active || active === document.body || active.closest('[role=\"menu\"]')) {\n indicator.current?.focus({ preventScroll: true });\n }\n });\n\n const grouped = selectionMode !== 'none';\n const hasToggles = hidden.some((child) => child.props.toggle);\n const menuSelectionMode = grouped ? selectionMode : hasToggles ? 'multiple' : 'none';\n const selected = hidden\n .map((child, index) => ({ child, key: keyOf(index) }))\n .filter(({ child, key }) =>\n grouped\n ? child.props.value !== undefined && selectedKeys.has(child.props.value)\n : pressed.has(key),\n )\n .map(({ key }) => key);\n const disabledKeys = hidden\n .map((child, index) => ({ child, key: keyOf(index) }))\n .filter(({ child }) => disabled || child.props.disabled)\n .map(({ key }) => key);\n\n return (\n <MenuTrigger onOpenChange={(open) => (open ? readPressed() : keepFocus())}>\n <IconButton\n icon={<MoreVertIcon />}\n aria-label={label}\n className={classNames?.overflowButton}\n onPressStart={(event) => {\n indicator.current = event.target as HTMLElement;\n }}\n />\n {hidden.length > 0 && (\n <LazyOverflowMenuList\n items={hidden.map((child, index) => ({\n key: keyOf(index),\n ...describe(child, selected.includes(keyOf(index))),\n // Only a toggle (or a button in the group's selection) is checkable; an action\n // stays a plain item even when the menu is selectable.\n checkable: grouped ? child.props.value !== undefined : Boolean(child.props.toggle),\n }))}\n selectionMode={menuSelectionMode}\n selectedKeys={selected}\n disabledKeys={disabledKeys}\n classNames={{ panel: classNames?.overflowMenu }}\n onAction={(key) => {\n elementAt(String(key))?.click();\n // The button updates once this event is handled.\n setTimeout(readPressed);\n }}\n />\n )}\n </MenuTrigger>\n );\n}\n\n/** A folded button's menu label and icon. */\nfunction describe(child: ReactElement<ChildProps>, isSelected: boolean) {\n const { props } = child;\n if (child.type === IconButton) {\n return {\n label: props['aria-label'],\n icon: (isSelected && props.selectedIcon) || props.icon,\n };\n }\n return {\n label: props.children,\n icon: (isSelected && props.selectedLeadingIcon) || props.leadingIcon,\n };\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- //#region src/composites/button-group/button-group-styles.d.ts
1
+ //#region src/components/button-group/button-group-styles.d.ts
2
2
  /**
3
3
  * Variant definitions for `ButtonGroup`, from the M3 Expressive button group specs
4
4
  * (2026-10-13). Standard groups space their buttons 18 / 12 / 8 / 8 / 8px apart (XS–XL), so
@@ -1,5 +1,5 @@
1
1
  import { tv } from "../../utils/tv.js";
2
- //#region src/composites/button-group/button-group-styles.ts
2
+ //#region src/components/button-group/button-group-styles.ts
3
3
  /**
4
4
  * Variant definitions for `ButtonGroup`, from the M3 Expressive button group specs
5
5
  * (2026-10-13). Standard groups space their buttons 18 / 12 / 8 / 8 / 8px apart (XS–XL), so