@windmorph/react 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/components/autocomplete/Autocomplete.d.ts +4 -1
  2. package/dist/components/autocomplete/Autocomplete.js +3 -5
  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/chip/Chip.d.ts +8 -2
  31. package/dist/components/chip/Chip.js +3 -8
  32. package/dist/components/chip/Chip.js.map +1 -1
  33. package/dist/components/chip/chip-tokens.d.ts +4 -4
  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 +12 -12
  44. package/dist/components/date-picker/date-picker-tokens.d.ts +1 -1
  45. package/dist/components/dialog/Dialog.d.ts +5 -2
  46. package/dist/components/dialog/Dialog.js +4 -2
  47. package/dist/components/dialog/Dialog.js.map +1 -1
  48. package/dist/components/dialog/dialog-tokens.d.ts +1 -1
  49. package/dist/components/fab-menu/FabMenu.js +2 -2
  50. package/dist/components/fab-menu/FabMenu.js.map +1 -1
  51. package/dist/components/icon-button/IconButton.d.ts +4 -2
  52. package/dist/components/icon-button/IconButton.js.map +1 -1
  53. package/dist/components/labels/LabelsProvider.d.ts +35 -0
  54. package/dist/components/labels/LabelsProvider.js +47 -0
  55. package/dist/components/labels/LabelsProvider.js.map +1 -0
  56. package/dist/components/labels/use-labels.d.ts +16 -0
  57. package/dist/components/labels/use-labels.js +42 -0
  58. package/dist/components/labels/use-labels.js.map +1 -0
  59. package/dist/components/menu/Menu.d.ts +17 -1
  60. package/dist/components/menu/Menu.js +21 -11
  61. package/dist/components/menu/Menu.js.map +1 -1
  62. package/dist/components/menu/menu-styles.js +12 -12
  63. package/dist/components/menu/menu-styles.js.map +1 -1
  64. package/dist/components/menu/menu-tokens.d.ts +1 -1
  65. package/dist/components/menu/menu-tokens.js.map +1 -1
  66. package/dist/components/navigation/NavItem.d.ts +1 -1
  67. package/dist/components/navigation/NavItem.js +9 -6
  68. package/dist/components/navigation/NavItem.js.map +1 -1
  69. package/dist/components/navigation/navigation-styles.js +1 -1
  70. package/dist/components/navigation/navigation-styles.js.map +1 -1
  71. package/dist/components/pull-to-refresh/PullToRefresh.d.ts +1 -1
  72. package/dist/components/pull-to-refresh/PullToRefresh.js +2 -4
  73. package/dist/components/pull-to-refresh/PullToRefresh.js.map +1 -1
  74. package/dist/components/search/SearchField.d.ts +4 -1
  75. package/dist/components/search/SearchField.js +2 -4
  76. package/dist/components/search/SearchField.js.map +1 -1
  77. package/dist/components/search/search-styles.js +2 -2
  78. package/dist/components/search/search-styles.js.map +1 -1
  79. package/dist/components/select/Select.d.ts +4 -1
  80. package/dist/components/select/Select.js +2 -4
  81. package/dist/components/select/Select.js.map +1 -1
  82. package/dist/components/sheet/BottomSheet.d.ts +4 -1
  83. package/dist/components/sheet/BottomSheet.js +2 -4
  84. package/dist/components/sheet/BottomSheet.js.map +1 -1
  85. package/dist/components/sheet/SideSheet.d.ts +4 -1
  86. package/dist/components/sheet/SideSheet.js +2 -4
  87. package/dist/components/sheet/SideSheet.js.map +1 -1
  88. package/dist/components/slider/Slider.d.ts +4 -1
  89. package/dist/components/slider/Slider.js +7 -5
  90. package/dist/components/slider/Slider.js.map +1 -1
  91. package/dist/components/snackbar/Snackbar.d.ts +8 -2
  92. package/dist/components/snackbar/Snackbar.js +2 -4
  93. package/dist/components/snackbar/Snackbar.js.map +1 -1
  94. package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
  95. package/dist/components/split-button/SplitButton.d.ts +4 -1
  96. package/dist/components/split-button/SplitButton.js +4 -2
  97. package/dist/components/split-button/SplitButton.js.map +1 -1
  98. package/dist/components/switch/switch-styles.d.ts +3 -3
  99. package/dist/components/text-field/TextField.d.ts +4 -1
  100. package/dist/components/text-field/TextField.js +2 -4
  101. package/dist/components/text-field/TextField.js.map +1 -1
  102. package/dist/components/text-field/text-field-styles.js +2 -2
  103. package/dist/components/text-field/text-field-styles.js.map +1 -1
  104. package/dist/components/time-picker/TimeField.d.ts +24 -2
  105. package/dist/components/time-picker/TimeField.js +40 -12
  106. package/dist/components/time-picker/TimeField.js.map +1 -1
  107. package/dist/components/time-picker/TimeFieldList.js +95 -0
  108. package/dist/components/time-picker/TimeFieldList.js.map +1 -0
  109. package/dist/components/time-picker/TimePicker.d.ts +9 -5
  110. package/dist/components/time-picker/TimePicker.js +5 -2
  111. package/dist/components/time-picker/TimePicker.js.map +1 -1
  112. package/dist/components/time-picker/time-list.js +33 -0
  113. package/dist/components/time-picker/time-list.js.map +1 -0
  114. package/dist/components/tooltip/Tooltip.d.ts +1 -1
  115. package/dist/components/tooltip/tooltip-tokens.d.ts +1 -1
  116. package/dist/index.d.ts +40 -38
  117. package/dist/index.js +4 -3
  118. package/dist/primitives/use-m3-interaction.js +10 -6
  119. package/dist/primitives/use-m3-interaction.js.map +1 -1
  120. package/dist/primitives.d.ts +7 -4
  121. package/dist/primitives.js +4 -1
  122. package/dist/styles/utilities.css +22 -15
  123. package/dist/styles.compiled.css +1 -1
  124. package/dist/utils/use-scroll-keys.d.ts +29 -0
  125. package/dist/utils/use-scroll-keys.js +66 -0
  126. package/dist/utils/use-scroll-keys.js.map +1 -0
  127. package/dist/utils/use-scroll-restoration.d.ts +43 -0
  128. package/dist/utils/use-scroll-restoration.js +173 -0
  129. package/dist/utils/use-scroll-restoration.js.map +1 -0
  130. package/dist/windmorph-labels.d.ts +101 -0
  131. package/package.json +1 -1
  132. package/dist/composites/button-group/ButtonGroup.js.map +0 -1
  133. package/dist/composites/button-group/lazy-overflow-menu.js.map +0 -1
  134. package/dist/composites/button-group/overflow-menu-list.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SearchField.js","names":[],"sources":["../../../src/components/search/SearchField.tsx"],"sourcesContent":["'use client';\n\nimport {\n type CSSProperties,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { mergeProps, useObjectRef, useSearchField } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { pickWidgetAria, splitRootProps, type WrapperRootDOMProps } from '../../utils/root-props';\nimport { ButtonContext, standardIconButtons } from '../button/ButtonContext';\nimport { IconButton } from '../icon-button/IconButton';\nimport type { SearchBarInputProps } from './SearchBar';\nimport { SearchFieldFrame } from './search-field-frame';\nimport { searchBarStyles, searchFieldStyles } from './search-styles';\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\nexport interface SearchFieldClassNames {\n /** The outer container, merged with `className`. */\n root?: string;\n /** The pill around the icons and the input. */\n field?: string;\n /** The text input. */\n input?: string;\n /** The leading icon slot. */\n leading?: string;\n /** The trailing slot, holding the clear button and `trailingIcon`. */\n trailing?: string;\n /** The clear button. */\n clear?: string;\n}\n\nexport interface SearchFieldLabels {\n /** The clear button's name, also its tooltip. @default \"Clear search\" */\n clear: string;\n}\n\nconst DEFAULT_LABELS: SearchFieldLabels = { clear: 'Clear search' };\n\n/**\n * Attributes for the input, e.g. `aria-keyshortcuts`, `enterKeyHint`, `autoComplete`,\n * `spellCheck`, `inputMode` or event handlers: those of {@link SearchBarInputProps}, less\n * `disabled` and `autoFocus`, which are the field's own props.\n */\nexport type SearchFieldInputProps = Omit<SearchBarInputProps, 'disabled' | 'autoFocus'>;\n\ntype Naming =\n | {\n /** The search box's accessible name. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the search box. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\ninterface SearchFieldOwnProps extends WrapperRootDOMProps {\n /** The input's id. */\n id?: string;\n /** The ids of elements that describe the search, before any `inputProps['aria-describedby']`. */\n 'aria-describedby'?: string;\n /** The id of an element with more details about the search. */\n 'aria-details'?: string;\n /** The id of what the search filters, e.g. a list of results. */\n 'aria-controls'?: string;\n /** Controlled query. */\n value?: string;\n /** Initial query when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the query as it changes, by typing, Escape or the clear button. */\n onChange?: (value: string) => void;\n /** Called with the query when the user presses Enter (the keyboard's search action). */\n onSubmit?: (value: string) => void;\n /** Hint text shown in the empty input, e.g. \"Search settings\". */\n placeholder?: string;\n /** Leading icon, usually a search icon, or a back button. */\n leadingIcon?: ReactNode;\n /**\n * Trailing content after the clear button: an icon button (e.g. voice search), or an avatar\n * (an `img`, drawn at 30px), as in the search bar.\n */\n trailingIcon?: ReactNode;\n /** Replaces the clear button's icon (Material Symbols `close`). */\n clearIcon?: ReactElement;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the pill (56px at 0, 44px at -3), as on the\n * other fields; icon buttons in it keep their 48px touch targets. Left out, the field takes the\n * density of the nearest `ThemeProvider` or `ThemeScope`. M3 asks for density to be the\n * reader's choice rather than a default, so prefer setting it there, from a setting.\n * @default 0\n */\n density?: Density;\n /** Whether the field is disabled: it takes no input and shows no clear button. */\n disabled?: boolean;\n /** Focus the input when the field mounts. */\n autoFocus?: boolean;\n /** The input element. */\n inputRef?: Ref<HTMLInputElement>;\n /**\n * Attributes for the input. They replace the field's defaults (`enterKeyHint=\"search\"`,\n * `autoComplete=\"off\"`, `spellCheck={false}`); event handlers run before the field's own, and\n * its role, name, value and type always win.\n */\n inputProps?: SearchFieldInputProps;\n /** Words the field shows or announces, in place of the English defaults. */\n labels?: Partial<SearchFieldLabels>;\n /** Ref to the root `<div>` element. */\n ref?: Ref<HTMLDivElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. */\n classNames?: SearchFieldClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SearchFieldProps = SearchFieldOwnProps & Naming;\n\n/** Presses on these keep their own behaviour; a press anywhere else on the pill focuses the input. */\nconst INTERACTIVE = 'input, button, a, [role=\"button\"]';\n\n/**\n * A search box that filters content in place (a settings page, a table, a list pane), drawn as\n * the M3 search bar's 56px pill and filling its container. It never opens a search view: use\n * `SearchBar` for suggestions or results. Escape clears the query, Enter calls `onSubmit`, and a\n * clear button shows while there is text.\n *\n * @example\n * <SearchField aria-label=\"Search settings\" placeholder=\"Search settings\"\n * leadingIcon={<SearchIcon />} value={query} onChange={setQuery} />\n */\nexport function SearchField(props: SearchFieldProps) {\n const {\n value,\n defaultValue,\n onChange,\n onSubmit,\n placeholder,\n leadingIcon,\n trailingIcon,\n clearIcon,\n density,\n disabled = false,\n autoFocus,\n inputRef,\n inputProps: consumerInputProps,\n labels: labelsProp,\n ref,\n className,\n style,\n classNames,\n ...rest\n } = props;\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const [query, setQuery] = useControlledState(value, defaultValue ?? '', onChange);\n const objectInputRef = useObjectRef(inputRef);\n const { root: data, rest: own } = splitRootProps(rest, { wrapper: true });\n const {\n id,\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n } = own as {\n id?: string;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-details'?: string;\n };\n const describedByInput =\n [describedBy, consumerInputProps?.['aria-describedby']].filter(Boolean).join(' ') || undefined;\n\n const { inputProps } = useSearchField(\n {\n id,\n 'aria-label': label,\n 'aria-labelledby': label == null ? labelledBy : undefined,\n 'aria-describedby': describedByInput,\n 'aria-details': details,\n placeholder,\n isDisabled: disabled,\n autoFocus,\n },\n { value: query, setValue: setQuery },\n objectInputRef,\n );\n\n // Enter submits, but not while an input method is composing (it confirms the composition).\n const submitOnEnter = (event: KeyboardEvent<HTMLInputElement>) => {\n if (!onSubmit || event.key !== 'Enter' || event.nativeEvent.isComposing) return;\n event.preventDefault();\n onSubmit(query);\n };\n\n const clear = () => {\n setQuery('');\n // The button goes with the text, so focus stays in the input.\n objectInputRef.current?.focus({ preventScroll: true });\n };\n\n // A press on the pill around the input (its padding, a decorative icon) puts the caret in it.\n const focusInput = (event: PointerEvent<HTMLDivElement>) => {\n if (disabled || event.button !== 0) return;\n if ((event.target as Element).closest(INTERACTIVE)) return;\n event.preventDefault();\n objectInputRef.current?.focus();\n };\n\n const showClear = query !== '' && !disabled;\n const hasTrailing = showClear || trailingIcon != null;\n const styles = searchBarStyles({\n hasLeading: leadingIcon != null,\n hasTrailing,\n disabled,\n });\n const rootStyles = searchFieldStyles();\n\n return (\n <div\n {...data}\n ref={ref}\n style={style}\n {...densityAttribute(density)}\n data-disabled={disabled || undefined}\n className={rootStyles.root({ class: cn(classNames?.root, className) })}\n >\n {/* Icon buttons in a search bar are standard (M3), not the filled default. */}\n <ButtonContext value={standardIconButtons}>\n <SearchFieldFrame\n disabled={disabled}\n onPointerDown={focusInput}\n className={classNames?.field}\n leading={\n leadingIcon != null && (\n <span\n data-slot=\"leading\"\n className={styles.leading({ class: cn(rootStyles.slot(), classNames?.leading) })}\n >\n {leadingIcon}\n </span>\n )\n }\n trailing={\n hasTrailing && (\n <span\n data-slot=\"trailing\"\n className={styles.trailing({ class: cn(rootStyles.slot(), classNames?.trailing) })}\n >\n {showClear && (\n <IconButton\n aria-label={labels.clear}\n icon={clearIcon ?? <ClearIcon />}\n data-slot=\"clear\"\n // A pointer press keeps focus (and a phone's keyboard) in the input.\n preventFocusOnPress\n onPress={clear}\n className={classNames?.clear}\n />\n )}\n {trailingIcon}\n </span>\n )\n }\n >\n <input\n {...mergeProps(\n { enterKeyHint: 'search', autoComplete: 'off', spellCheck: false },\n consumerInputProps,\n inputProps,\n pickWidgetAria(own),\n { onKeyDown: submitOnEnter },\n )}\n ref={objectInputRef}\n data-slot=\"input\"\n className={styles.input({ class: classNames?.input })}\n />\n </SearchFieldFrame>\n </ButtonContext>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SearchField.js","names":[],"sources":["../../../src/components/search/SearchField.tsx"],"sourcesContent":["'use client';\n\nimport {\n type CSSProperties,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { mergeProps, useObjectRef, useSearchField } from 'react-aria';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { useLabels } from '../labels/use-labels';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { pickWidgetAria, splitRootProps, type WrapperRootDOMProps } from '../../utils/root-props';\nimport { ButtonContext, standardIconButtons } from '../button/ButtonContext';\nimport { IconButton } from '../icon-button/IconButton';\nimport type { SearchBarInputProps } from './SearchBar';\nimport { SearchFieldFrame } from './search-field-frame';\nimport { searchBarStyles, searchFieldStyles } from './search-styles';\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\nexport interface SearchFieldClassNames {\n /** The outer container, merged with `className`. */\n root?: string;\n /** The pill around the icons and the input. */\n field?: string;\n /** The text input. */\n input?: string;\n /** The leading icon slot. */\n leading?: string;\n /** The trailing slot, holding the clear button and `trailingIcon`. */\n trailing?: string;\n /** The clear button. */\n clear?: string;\n}\n\nexport interface SearchFieldLabels {\n /** The clear button's name, also its tooltip. @default \"Clear search\" */\n clear: string;\n}\n\nconst DEFAULT_LABELS: SearchFieldLabels = { clear: 'Clear search' };\n\n/**\n * Attributes for the input, e.g. `aria-keyshortcuts`, `enterKeyHint`, `autoComplete`,\n * `spellCheck`, `inputMode` or event handlers: those of {@link SearchBarInputProps}, less\n * `disabled` and `autoFocus`, which are the field's own props.\n */\nexport type SearchFieldInputProps = Omit<SearchBarInputProps, 'disabled' | 'autoFocus'>;\n\ntype Naming =\n | {\n /** The search box's accessible name. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the search box. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\ninterface SearchFieldOwnProps extends WrapperRootDOMProps {\n /** The input's id. */\n id?: string;\n /** The ids of elements that describe the search, before any `inputProps['aria-describedby']`. */\n 'aria-describedby'?: string;\n /** The id of an element with more details about the search. */\n 'aria-details'?: string;\n /** The id of what the search filters, e.g. a list of results. */\n 'aria-controls'?: string;\n /** Controlled query. */\n value?: string;\n /** Initial query when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the query as it changes, by typing, Escape or the clear button. */\n onChange?: (value: string) => void;\n /** Called with the query when the user presses Enter (the keyboard's search action). */\n onSubmit?: (value: string) => void;\n /** Hint text shown in the empty input, e.g. \"Search settings\". */\n placeholder?: string;\n /** Leading icon, usually a search icon, or a back button. */\n leadingIcon?: ReactNode;\n /**\n * Trailing content after the clear button: an icon button (e.g. voice search), or an avatar\n * (an `img`, drawn at 30px), as in the search bar.\n */\n trailingIcon?: ReactNode;\n /** Replaces the clear button's icon (Material Symbols `close`). */\n clearIcon?: ReactElement;\n /**\n * Web density, `0` to `-3`: each step takes 4px off the pill (56px at 0, 44px at -3), as on the\n * other fields; icon buttons in it keep their 48px touch targets. Left out, the field takes the\n * density of the nearest `ThemeProvider` or `ThemeScope`. M3 asks for density to be the\n * reader's choice rather than a default, so prefer setting it there, from a setting.\n * @default 0\n */\n density?: Density;\n /** Whether the field is disabled: it takes no input and shows no clear button. */\n disabled?: boolean;\n /** Focus the input when the field mounts. */\n autoFocus?: boolean;\n /** The input element. */\n inputRef?: Ref<HTMLInputElement>;\n /**\n * Attributes for the input. They replace the field's defaults (`enterKeyHint=\"search\"`,\n * `autoComplete=\"off\"`, `spellCheck={false}`); event handlers run before the field's own, and\n * its role, name, value and type always win.\n */\n inputProps?: SearchFieldInputProps;\n /**\n * Words the field shows or announces, in place of the English defaults. A {@link LabelsProvider}\n * sets them for the whole app.\n */\n labels?: Partial<SearchFieldLabels>;\n /** Ref to the root `<div>` element. */\n ref?: Ref<HTMLDivElement>;\n /** Classes for the root element, merged with `classNames.root`; consumer classes win. */\n className?: string;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Classes for each part, by slot. */\n classNames?: SearchFieldClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SearchFieldProps = SearchFieldOwnProps & Naming;\n\n/** Presses on these keep their own behaviour; a press anywhere else on the pill focuses the input. */\nconst INTERACTIVE = 'input, button, a, [role=\"button\"]';\n\n/**\n * A search box that filters content in place (a settings page, a table, a list pane), drawn as\n * the M3 search bar's 56px pill and filling its container. It never opens a search view: use\n * `SearchBar` for suggestions or results. Escape clears the query, Enter calls `onSubmit`, and a\n * clear button shows while there is text.\n *\n * @example\n * <SearchField aria-label=\"Search settings\" placeholder=\"Search settings\"\n * leadingIcon={<SearchIcon />} value={query} onChange={setQuery} />\n */\nexport function SearchField(props: SearchFieldProps) {\n const {\n value,\n defaultValue,\n onChange,\n onSubmit,\n placeholder,\n leadingIcon,\n trailingIcon,\n clearIcon,\n density,\n disabled = false,\n autoFocus,\n inputRef,\n inputProps: consumerInputProps,\n labels: labelsProp,\n ref,\n className,\n style,\n classNames,\n ...rest\n } = props;\n const labels = useLabels('SearchField', DEFAULT_LABELS, labelsProp);\n const [query, setQuery] = useControlledState(value, defaultValue ?? '', onChange);\n const objectInputRef = useObjectRef(inputRef);\n const { root: data, rest: own } = splitRootProps(rest, { wrapper: true });\n const {\n id,\n 'aria-label': label,\n 'aria-labelledby': labelledBy,\n 'aria-describedby': describedBy,\n 'aria-details': details,\n } = own as {\n id?: string;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-details'?: string;\n };\n const describedByInput =\n [describedBy, consumerInputProps?.['aria-describedby']].filter(Boolean).join(' ') || undefined;\n\n const { inputProps } = useSearchField(\n {\n id,\n 'aria-label': label,\n 'aria-labelledby': label == null ? labelledBy : undefined,\n 'aria-describedby': describedByInput,\n 'aria-details': details,\n placeholder,\n isDisabled: disabled,\n autoFocus,\n },\n { value: query, setValue: setQuery },\n objectInputRef,\n );\n\n // Enter submits, but not while an input method is composing (it confirms the composition).\n const submitOnEnter = (event: KeyboardEvent<HTMLInputElement>) => {\n if (!onSubmit || event.key !== 'Enter' || event.nativeEvent.isComposing) return;\n event.preventDefault();\n onSubmit(query);\n };\n\n const clear = () => {\n setQuery('');\n // The button goes with the text, so focus stays in the input.\n objectInputRef.current?.focus({ preventScroll: true });\n };\n\n // A press on the pill around the input (its padding, a decorative icon) puts the caret in it.\n const focusInput = (event: PointerEvent<HTMLDivElement>) => {\n if (disabled || event.button !== 0) return;\n if ((event.target as Element).closest(INTERACTIVE)) return;\n event.preventDefault();\n objectInputRef.current?.focus();\n };\n\n const showClear = query !== '' && !disabled;\n const hasTrailing = showClear || trailingIcon != null;\n const styles = searchBarStyles({\n hasLeading: leadingIcon != null,\n hasTrailing,\n disabled,\n });\n const rootStyles = searchFieldStyles();\n\n return (\n <div\n {...data}\n ref={ref}\n style={style}\n {...densityAttribute(density)}\n data-disabled={disabled || undefined}\n className={rootStyles.root({ class: cn(classNames?.root, className) })}\n >\n {/* Icon buttons in a search bar are standard (M3), not the filled default. */}\n <ButtonContext value={standardIconButtons}>\n <SearchFieldFrame\n disabled={disabled}\n onPointerDown={focusInput}\n className={classNames?.field}\n leading={\n leadingIcon != null && (\n <span\n data-slot=\"leading\"\n className={styles.leading({ class: cn(rootStyles.slot(), classNames?.leading) })}\n >\n {leadingIcon}\n </span>\n )\n }\n trailing={\n hasTrailing && (\n <span\n data-slot=\"trailing\"\n className={styles.trailing({ class: cn(rootStyles.slot(), classNames?.trailing) })}\n >\n {showClear && (\n <IconButton\n aria-label={labels.clear}\n icon={clearIcon ?? <ClearIcon />}\n data-slot=\"clear\"\n // A pointer press keeps focus (and a phone's keyboard) in the input.\n preventFocusOnPress\n onPress={clear}\n className={classNames?.clear}\n />\n )}\n {trailingIcon}\n </span>\n )\n }\n >\n <input\n {...mergeProps(\n { enterKeyHint: 'search', autoComplete: 'off', spellCheck: false },\n consumerInputProps,\n inputProps,\n pickWidgetAria(own),\n { onKeyDown: submitOnEnter },\n )}\n ref={objectInputRef}\n data-slot=\"input\"\n className={styles.input({ class: classNames?.input })}\n />\n </SearchFieldFrame>\n </ButtonContext>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -9,8 +9,8 @@ const searchBarStyles = tv({
9
9
  leading: "ms-[4px] flex size-[48px] shrink-0 items-center justify-center text-[color:var(--_md-comp-search-bar-leading-icon-color)] [--md-comp-icon-button-icon-color:currentColor] [&>svg]:size-[24px]",
10
10
  trailing: ["me-[4px] flex h-[48px] min-w-[48px] shrink-0 items-center justify-center gap-[8px] px-[4px] text-[color:var(--_md-comp-search-bar-trailing-icon-color)] [--md-comp-icon-button-icon-color:currentColor]", "[&>svg]:size-[24px] [&>img]:size-[30px] [&>img]:rounded-full"],
11
11
  input: [
12
- "h-full min-w-0 flex-1 bg-transparent text-body-large text-[color:var(--_md-comp-search-bar-input-text-color)] caret-primary outline-none",
13
- "placeholder:text-on-surface-variant",
12
+ "h-full min-w-0 flex-1 bg-transparent text-body-large text-[color:var(--_md-comp-search-bar-input-text-color)] caret-primary outline-none text-ellipsis",
13
+ "placeholder:text-on-surface-variant placeholder:text-ellipsis",
14
14
  "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none"
15
15
  ],
16
16
  scrim: [
@@ -1 +1 @@
1
- {"version":3,"file":"search-styles.js","names":[],"sources":["../../../src/components/search/search-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (SearchBar.kt: SearchBar, AppBarWithSearch,\n * ExpandedDockedSearchBarWithGap, ExpandedFullScreenSearchBar, SearchBarDefaults.InputField;\n * SearchBar / SearchView tokens).\n *\n * Field: 56px, full corners, `surface-container-high`, 360px wide by default and at most\n * 720px. Input and placeholder `body-large`, `on-surface` / `on-surface-variant`. Icons sit\n * in 48px boxes shifted 4px inwards (leading `on-surface`, trailing `on-surface-variant`);\n * the text keeps 4px from the leading and trailing tap targets (the spec's \"Leading icon and\n * label padding (from tap target): 4dp\" and \"Label and trailing icon padding\"; Compose's\n * code leaves none), so it starts 56px in after a leading icon, 16px without one.\n *\n * Docked (with gap): the expanded field, 56px tall whatever the collapsed bar's height\n * (M3's focused search bar container), sits centred over the collapsed one and widens by 12px on\n * each side (M3 Expressive: the margins change from 24dp to 12dp when focused). It morphs from\n * the bar's bounds (position and size) on the default spatial spring and back on the same\n * spring floored at the bar, so it lands on the bar exactly; a dropdown 2px below it (12px corners,\n * `surface-container-high`; with the bar, at least 240px and at most 2/3 of the window\n * tall, per the M3 spec) slides down from half its height on the default spatial spring\n * (fast spatial back) while its content fades in over 100ms after 50ms, and its height follows\n * the results on the default spatial spring; a 32% scrim covers the page.\n *\n * Full screen, in M3 Expressive's contained style: a `surface-container-low` surface grows\n * from the bar's bounds (28px corners) to the whole window on the slow spatial spring\n * (default spatial back), clamped so nothing overshoots; the field keeps its\n * `surface-container-high` pill and moves and grows from the bar to the top, 8px down and\n * 16px from each side, and the content starts 72px down.\n *\n * In a search app bar the field is `surface-container` (`surface-container-highest` once\n * content scrolls under the bar) and fills the free space up to 312px, then half of it,\n * at most 720px (M3 app bar guidelines).\n *\n * The field's colours and corners come from the search bar's component tokens and the\n * views' from the search view's (`search-tokens.ts`, `--md-comp-search-bar-*` and\n * `--md-comp-search-view-*`); the pill, the dropdown and the full-screen view carry\n * `data-tokens`. The search app bar reads the app bar's tokens (`app-bar-tokens.ts`). A\n * consumer class still wins in `cn()`.\n *\n * Every class is written out in full so Tailwind can find it.\n */\n\nconst contentFade = [\n 'opacity-100 starting:opacity-0 data-exiting:opacity-0',\n // Compose: fade in over 100ms after 50ms (standard accelerate), out over 100ms.\n '[transition-delay:0s,50ms] data-exiting:[transition-delay:0s,0s]',\n];\n\n/** Variant definitions for {@link SearchBar}; extend them to add variants. */\nexport const searchBarStyles = tv({\n slots: {\n // The bar's height is set here alone (56px, Compose's); a consumer `h-*` resizes it.\n root: 'flex h-[56px] w-[360px] max-w-[min(720px,100%)] shrink-0',\n // The pill holding the icons and the input fills the bar.\n // Keyboard focus in the input draws Compose's inset focus ring around the pill.\n // Forced colours drop the pill's colour: a system-colour border keeps its edge.\n field: [\n 'focus-ring-inset flex h-full w-full min-w-0 items-center rounded-[var(--_md-comp-search-bar-container-shape)] text-on-surface',\n 'forced-colors:border forced-colors:border-solid forced-colors:border-[CanvasText]',\n ],\n // Icon buttons in the slots take the slot's colour (and so its icon colour token), as\n // in the app bar's slots: Compose colours the search bar's and view's leading icon\n // `on-surface` and trailing icons `on-surface-variant`, buttons included.\n leading:\n 'ms-[4px] flex size-[48px] shrink-0 items-center justify-center text-[color:var(--_md-comp-search-bar-leading-icon-color)] [--md-comp-icon-button-icon-color:currentColor] [&>svg]:size-[24px]',\n // One or two trailing icon buttons, or an avatar (30px) and an icon button (M3). Two\n // 40px buttons sit 8px apart inside 4px of padding, so their 48px targets meet edge to\n // edge within the slot (a single one is centred in 48px, as before).\n trailing: [\n 'me-[4px] flex h-[48px] min-w-[48px] shrink-0 items-center justify-center gap-[8px] px-[4px] text-[color:var(--_md-comp-search-bar-trailing-icon-color)] [--md-comp-icon-button-icon-color:currentColor]',\n '[&>svg]:size-[24px] [&>img]:size-[30px] [&>img]:rounded-full',\n ],\n input: [\n 'h-full min-w-0 flex-1 bg-transparent text-body-large text-[color:var(--_md-comp-search-bar-input-text-color)] caret-primary outline-none',\n 'placeholder:text-on-surface-variant',\n '[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none',\n ],\n scrim: [\n 'fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100 data-exiting:pointer-events-none',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n 'starting:opacity-0 data-exiting:opacity-0',\n ],\n /*\n * Docked view: the panel sits over the collapsed bar and morphs from the bar's bounds\n * (`--m3-search-left/top/width/height`) to the focused field's (the `-dx/-dy/-dw/-dh`\n * deltas) as `--m3-search-progress` runs 0 → 1, as Compose lerps the layout with its\n * search bar state's progress. `--m3-search-p` is that progress floored at 0 (Compose's\n * `coerceAtLeast(0f)`): the spring may overshoot past the focused bounds on the way in,\n * but never past the bar's on the way back, so the field lands on the bar exactly.\n * Reduced motion: the progress stays at 1, so the field is at its focused size at once.\n */\n dockedPanel: [\n // Leaving, the view takes no presses: the spring's tail runs on after the field has\n // landed on the bar, and a press there ran a suggestion of the invisible dropdown.\n 'fixed z-(--md-sys-z-sheet) flex flex-col outline-none data-exiting:pointer-events-none',\n '[--m3-search-progress:1] [--m3-search-p:max(0,var(--m3-search-progress))]',\n 'motion-safe:starting:[--m3-search-progress:0] motion-safe:data-exiting:[--m3-search-progress:0]',\n 'top-[calc(var(--m3-search-top)_+_var(--m3-search-p)_*_var(--m3-search-dy))]',\n 'left-[calc(var(--m3-search-left)_+_var(--m3-search-p)_*_var(--m3-search-dx))]',\n 'w-[calc(var(--m3-search-width)_+_var(--m3-search-p)_*_var(--m3-search-dw))]',\n // Both ways on the default spatial spring (Compose's collapse spec).\n '[transition-property:--m3-search-progress]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n ],\n // The focused field is 56px (M3), even when the collapsed bar is resized; it grows from\n // the bar's height with the panel.\n // The full-window layer the docked view sits in (React Aria's underlay).\n underlay: 'fixed inset-0 z-(--md-sys-z-sheet) data-exiting:pointer-events-none',\n dockedHeader:\n 'flex h-[calc(var(--m3-search-height)_+_var(--m3-search-p)_*_var(--m3-search-dh))] shrink-0',\n dropdown: [\n 'mt-[2px] overflow-y-auto overscroll-contain data-resizing:overflow-y-hidden data-exiting:pointer-events-none',\n // 58px is the field and the gap above the dropdown.\n 'min-h-[min(182px,calc(66.67dvh-58px))] max-h-[calc(66.67dvh-58px)]',\n // The height follows the content (`--m3-search-content-height`, measured) within those\n // bounds, so it animates as the results change. Without the measurement (before\n // hydration, or under reduced motion) it's `auto`.\n 'h-[clamp(min(182px,calc(66.67dvh_-_58px)),var(--m3-search-content-height),calc(66.67dvh_-_58px))]',\n 'rounded-[var(--_md-comp-search-view-docked-container-shape)] bg-[var(--_md-comp-search-view-container-color)] text-on-surface',\n 'forced-colors:border forced-colors:border-solid forced-colors:border-[CanvasText]',\n // Reduced motion: it only fades.\n 'translate-y-0 motion-safe:starting:-translate-y-1/2 motion-safe:data-exiting:-translate-y-1/2',\n // The delays below cycle, so the height (third) starts at once like the slide.\n '[transition-property:translate,opacity,height]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration),100ms,var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing),cubic-bezier(0.3,0,1,1),var(--md-sys-motion-spring-spatial-default-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),100ms,var(--md-sys-motion-spring-spatial-fast-duration)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),cubic-bezier(0,0,0,1),var(--md-sys-motion-spring-spatial-fast-easing)]',\n ...contentFade,\n ],\n /*\n * Full-screen view: the surface grows from the bar's bounds to the window as\n * `--m3-search-progress` runs 0 → 1, through a clip (nothing is transformed). Compose\n * clamps the full-screen progress to 0..1 (`coerceIn`), so `--m3-search-p` is clamped too:\n * the clip and the field never overshoot the window's or the bar's bounds.\n */\n fullScreenPanel: [\n 'fixed inset-0 z-(--md-sys-z-sheet) bg-[var(--_md-comp-search-view-container-color)] text-on-surface outline-none data-exiting:pointer-events-none',\n '[--m3-search-progress:1] [--m3-search-p:clamp(0,var(--m3-search-progress),1)]',\n 'motion-safe:starting:[--m3-search-progress:0] motion-safe:data-exiting:[--m3-search-progress:0]',\n '[clip-path:inset(calc((1_-_var(--m3-search-p))_*_var(--m3-search-top))_calc((1_-_var(--m3-search-p))_*_var(--m3-search-right))_calc((1_-_var(--m3-search-p))_*_var(--m3-search-bottom))_calc((1_-_var(--m3-search-p))_*_var(--m3-search-left))_round_calc((1_-_var(--m3-search-p))_*_28px))]',\n 'motion-safe:[transition-property:--m3-search-progress]',\n 'motion-safe:[transition-duration:var(--md-sys-motion-spring-spatial-slow-duration)]',\n 'motion-safe:[transition-timing-function:var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'motion-safe:data-exiting:[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n 'motion-safe:data-exiting:[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n // Reduced motion: the view fades in and out over the window instead of growing.\n 'motion-reduce:starting:opacity-0 motion-reduce:data-exiting:opacity-0',\n 'motion-reduce:[transition-property:opacity]',\n 'motion-reduce:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'motion-reduce:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n // Compose's full-screen field is 56px whatever the bar's size, 8px down and 16px from each\n // side; it moves and grows there from the bar's bounds with the surface.\n fullScreenHeader: [\n 'absolute',\n 'start-[calc(var(--m3-search-start)_+_var(--m3-search-p)_*_(16px_-_var(--m3-search-start)))]',\n 'top-[calc(var(--m3-search-top)_+_var(--m3-search-p)_*_(8px_-_var(--m3-search-top)))]',\n 'w-[calc(var(--m3-search-width)_+_var(--m3-search-p)_*_(100%_-_32px_-_var(--m3-search-width)))]',\n 'h-[calc(var(--m3-search-height)_+_var(--m3-search-p)_*_(56px_-_var(--m3-search-height)))]',\n ],\n fullScreenContent: [\n 'absolute inset-x-0 top-[72px] bottom-0 overflow-y-auto overscroll-contain',\n '[transition-property:opacity] [transition-duration:100ms]',\n '[transition-timing-function:cubic-bezier(0.3,0,1,1)]',\n 'data-exiting:[transition-timing-function:cubic-bezier(0,0,0,1)]',\n ...contentFade,\n ],\n },\n variants: {\n /**\n * Inside a search app bar the bar fills the free space up to 312px, then grows to half\n * of it (M3), up to 720px.\n */\n inAppBar: { true: { root: 'w-[max(min(100%,312px),50%)]' }, false: {} },\n // 4px from a tap target (M3), else 16px from the pill's edge.\n hasLeading: { true: { input: 'ps-[4px]' }, false: { input: 'ps-[16px]' } },\n hasTrailing: { true: { input: 'pe-[4px]' }, false: { input: 'pe-[16px]' } },\n /**\n * Container colour: the bar's own; in a search app bar `surface-container`, and one\n * step higher than the bar once content scrolls under it.\n */\n tone: {\n default: { field: 'bg-[var(--_md-comp-search-bar-container-color)]' },\n appBar: { field: 'bg-surface-container' },\n scrolled: { field: 'bg-surface-container-highest' },\n },\n /** Centre the placeholder and the query while the bar is collapsed. */\n textAlign: { start: {}, center: { input: 'text-center' } },\n /**\n * A disabled `SearchField`: M3's disabled colours, the content `on-surface` at 38% and the\n * container at 12%; forced colours draw it in `GrayText`.\n */\n disabled: {\n true: {\n field: 'bg-on-surface/12 forced-colors:border-[GrayText]',\n leading: 'text-on-surface/38 forced-colors:text-[GrayText]',\n trailing: 'text-on-surface/38 forced-colors:text-[GrayText]',\n input:\n 'cursor-default text-on-surface/38 placeholder:text-on-surface/38 forced-colors:text-[GrayText]',\n },\n false: {},\n },\n },\n defaultVariants: {\n inAppBar: false,\n hasLeading: false,\n hasTrailing: false,\n tone: 'default',\n textAlign: 'start',\n disabled: false,\n },\n});\n\n/**\n * Variant definitions for {@link SearchField}'s root. Its pill and the parts in it are the search\n * bar's ({@link searchBarStyles}), so the two can't drift.\n */\nexport const searchFieldStyles = tv({\n slots: {\n // 56px, the search bar's height, less 4px a density step (44px at -3), filling its\n // container: it filters a page, a list or a pane in place. A consumer `w-*` or `h-*` wins.\n root: 'flex h-[calc(56px+var(--md-sys-density,0)*4px)] w-full min-w-0',\n // The 48px icon slots keep to a dense pill's height; their buttons keep 48px targets.\n slot: 'max-h-full',\n },\n variants: {},\n});\n\n/** Variant definitions for {@link SearchAppBar}. */\nexport const searchAppBarStyles = tv({\n slots: {\n root: [\n 'flex min-h-[64px] w-full items-center text-on-surface',\n 'bg-[var(--_md-comp-app-bar-container-color)] data-scrolled:bg-[var(--_md-comp-app-bar-on-scroll-container-color)]',\n '[transition-property:background-color]',\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n ],\n // Icon buttons take each slot's content colour.\n navigation:\n 'flex shrink-0 items-center ps-[8px] pe-[4px] text-[color:var(--_md-comp-app-bar-leading-icon-color)] [--md-comp-icon-button-icon-color:currentColor]',\n // Centres the search bar in the free space, 8px from its neighbours and 4px vertically.\n search: 'flex min-w-0 flex-1 justify-center ps-[8px] pe-[8px] pt-[4px] pb-[4px]',\n actions:\n 'flex shrink-0 items-center gap-[8px] ps-[4px] pe-[8px] text-[color:var(--_md-comp-app-bar-trailing-icon-color)] [--md-comp-icon-button-icon-color:currentColor]',\n },\n variants: {\n scrolling: {\n true: { root: 'sticky top-(--m3-app-bar-offset) z-(--md-sys-z-sticky)' },\n false: {},\n },\n },\n defaultVariants: { scrolling: false },\n});\n\n/**\n * The variants of {@link searchBarStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type SearchBarStyleProps = VariantProps<typeof searchBarStyles>;\n/**\n * The variants of {@link searchAppBarStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type SearchAppBarStyleProps = VariantProps<typeof searchAppBarStyles>;\n/**\n * The variants of {@link searchFieldStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names follow the classes and may change.\n */\nexport type SearchFieldStyleProps = VariantProps<typeof searchFieldStyles>;\n"],"mappings":";;AA2CA,MAAM,cAAc,CAClB,yDAEA,kEACF;;AAGA,MAAa,kBAAkB,GAAG;CAChC,OAAO;EAEL,MAAM;EAIN,OAAO,CACL,iIACA,mFACF;EAIA,SACE;EAIF,UAAU,CACR,2MACA,8DACF;EACA,OAAO;GACL;GACA;GACA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;EACF;EAUA,aAAa;GAGX;GACA;GACA;GACA;GACA;GACA;GAEA;GACA;GACA;EACF;EAIA,UAAU;EACV,cACE;EACF,UAAU;GACR;GAEA;GAIA;GACA;GACA;GAEA;GAEA;GACA;GACA;GACA;GACA;GACA,GAAG;EACL;EAOA,iBAAiB;GACf;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA;GACA;GACA;GACA;EACF;EAGA,kBAAkB;GAChB;GACA;GACA;GACA;GACA;EACF;EACA,mBAAmB;GACjB;GACA;GACA;GACA;GACA,GAAG;EACL;CACF;CACA,UAAU;;;;;EAKR,UAAU;GAAE,MAAM,EAAE,MAAM,+BAA+B;GAAG,OAAO,CAAC;EAAE;EAEtE,YAAY;GAAE,MAAM,EAAE,OAAO,WAAW;GAAG,OAAO,EAAE,OAAO,YAAY;EAAE;EACzE,aAAa;GAAE,MAAM,EAAE,OAAO,WAAW;GAAG,OAAO,EAAE,OAAO,YAAY;EAAE;;;;;EAK1E,MAAM;GACJ,SAAS,EAAE,OAAO,kDAAkD;GACpE,QAAQ,EAAE,OAAO,uBAAuB;GACxC,UAAU,EAAE,OAAO,+BAA+B;EACpD;;EAEA,WAAW;GAAE,OAAO,CAAC;GAAG,QAAQ,EAAE,OAAO,cAAc;EAAE;;;;;EAKzD,UAAU;GACR,MAAM;IACJ,OAAO;IACP,SAAS;IACT,UAAU;IACV,OACE;GACJ;GACA,OAAO,CAAC;EACV;CACF;CACA,iBAAiB;EACf,UAAU;EACV,YAAY;EACZ,aAAa;EACb,MAAM;EACN,WAAW;EACX,UAAU;CACZ;AACF,CAAC;;;;;AAMD,MAAa,oBAAoB,GAAG;CAClC,OAAO;EAGL,MAAM;EAEN,MAAM;CACR;CACA,UAAU,CAAC;AACb,CAAC;;AAGD,MAAa,qBAAqB,GAAG;CACnC,OAAO;EACL,MAAM;GACJ;GACA;GACA;GACA;GACA;EACF;EAEA,YACE;EAEF,QAAQ;EACR,SACE;CACJ;CACA,UAAU,EACR,WAAW;EACT,MAAM,EAAE,MAAM,yDAAyD;EACvE,OAAO,CAAC;CACV,EACF;CACA,iBAAiB,EAAE,WAAW,MAAM;AACtC,CAAC"}
1
+ {"version":3,"file":"search-styles.js","names":[],"sources":["../../../src/components/search/search-styles.ts"],"sourcesContent":["import { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (SearchBar.kt: SearchBar, AppBarWithSearch,\n * ExpandedDockedSearchBarWithGap, ExpandedFullScreenSearchBar, SearchBarDefaults.InputField;\n * SearchBar / SearchView tokens).\n *\n * Field: 56px, full corners, `surface-container-high`, 360px wide by default and at most\n * 720px. Input and placeholder `body-large`, `on-surface` / `on-surface-variant`. Icons sit\n * in 48px boxes shifted 4px inwards (leading `on-surface`, trailing `on-surface-variant`);\n * the text keeps 4px from the leading and trailing tap targets (the spec's \"Leading icon and\n * label padding (from tap target): 4dp\" and \"Label and trailing icon padding\"; Compose's\n * code leaves none), so it starts 56px in after a leading icon, 16px without one.\n *\n * Docked (with gap): the expanded field, 56px tall whatever the collapsed bar's height\n * (M3's focused search bar container), sits centred over the collapsed one and widens by 12px on\n * each side (M3 Expressive: the margins change from 24dp to 12dp when focused). It morphs from\n * the bar's bounds (position and size) on the default spatial spring and back on the same\n * spring floored at the bar, so it lands on the bar exactly; a dropdown 2px below it (12px corners,\n * `surface-container-high`; with the bar, at least 240px and at most 2/3 of the window\n * tall, per the M3 spec) slides down from half its height on the default spatial spring\n * (fast spatial back) while its content fades in over 100ms after 50ms, and its height follows\n * the results on the default spatial spring; a 32% scrim covers the page.\n *\n * Full screen, in M3 Expressive's contained style: a `surface-container-low` surface grows\n * from the bar's bounds (28px corners) to the whole window on the slow spatial spring\n * (default spatial back), clamped so nothing overshoots; the field keeps its\n * `surface-container-high` pill and moves and grows from the bar to the top, 8px down and\n * 16px from each side, and the content starts 72px down.\n *\n * In a search app bar the field is `surface-container` (`surface-container-highest` once\n * content scrolls under the bar) and fills the free space up to 312px, then half of it,\n * at most 720px (M3 app bar guidelines).\n *\n * The field's colours and corners come from the search bar's component tokens and the\n * views' from the search view's (`search-tokens.ts`, `--md-comp-search-bar-*` and\n * `--md-comp-search-view-*`); the pill, the dropdown and the full-screen view carry\n * `data-tokens`. The search app bar reads the app bar's tokens (`app-bar-tokens.ts`). A\n * consumer class still wins in `cn()`.\n *\n * Every class is written out in full so Tailwind can find it.\n */\n\nconst contentFade = [\n 'opacity-100 starting:opacity-0 data-exiting:opacity-0',\n // Compose: fade in over 100ms after 50ms (standard accelerate), out over 100ms.\n '[transition-delay:0s,50ms] data-exiting:[transition-delay:0s,0s]',\n];\n\n/** Variant definitions for {@link SearchBar}; extend them to add variants. */\nexport const searchBarStyles = tv({\n slots: {\n // The bar's height is set here alone (56px, Compose's); a consumer `h-*` resizes it.\n root: 'flex h-[56px] w-[360px] max-w-[min(720px,100%)] shrink-0',\n // The pill holding the icons and the input fills the bar.\n // Keyboard focus in the input draws Compose's inset focus ring around the pill.\n // Forced colours drop the pill's colour: a system-colour border keeps its edge.\n field: [\n 'focus-ring-inset flex h-full w-full min-w-0 items-center rounded-[var(--_md-comp-search-bar-container-shape)] text-on-surface',\n 'forced-colors:border forced-colors:border-solid forced-colors:border-[CanvasText]',\n ],\n // Icon buttons in the slots take the slot's colour (and so its icon colour token), as\n // in the app bar's slots: Compose colours the search bar's and view's leading icon\n // `on-surface` and trailing icons `on-surface-variant`, buttons included.\n leading:\n 'ms-[4px] flex size-[48px] shrink-0 items-center justify-center text-[color:var(--_md-comp-search-bar-leading-icon-color)] [--md-comp-icon-button-icon-color:currentColor] [&>svg]:size-[24px]',\n // One or two trailing icon buttons, or an avatar (30px) and an icon button (M3). Two\n // 40px buttons sit 8px apart inside 4px of padding, so their 48px targets meet edge to\n // edge within the slot (a single one is centred in 48px, as before).\n trailing: [\n 'me-[4px] flex h-[48px] min-w-[48px] shrink-0 items-center justify-center gap-[8px] px-[4px] text-[color:var(--_md-comp-search-bar-trailing-icon-color)] [--md-comp-icon-button-icon-color:currentColor]',\n '[&>svg]:size-[24px] [&>img]:size-[30px] [&>img]:rounded-full',\n ],\n // A hint or query longer than the field ends in an ellipsis, not mid-letter (as a text\n // field's label); the query only while the field isn't focused, since focused it scrolls.\n input: [\n 'h-full min-w-0 flex-1 bg-transparent text-body-large text-[color:var(--_md-comp-search-bar-input-text-color)] caret-primary outline-none text-ellipsis',\n 'placeholder:text-on-surface-variant placeholder:text-ellipsis',\n '[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none',\n ],\n scrim: [\n 'fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100 data-exiting:pointer-events-none',\n '[transition-property:opacity]',\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n 'starting:opacity-0 data-exiting:opacity-0',\n ],\n /*\n * Docked view: the panel sits over the collapsed bar and morphs from the bar's bounds\n * (`--m3-search-left/top/width/height`) to the focused field's (the `-dx/-dy/-dw/-dh`\n * deltas) as `--m3-search-progress` runs 0 → 1, as Compose lerps the layout with its\n * search bar state's progress. `--m3-search-p` is that progress floored at 0 (Compose's\n * `coerceAtLeast(0f)`): the spring may overshoot past the focused bounds on the way in,\n * but never past the bar's on the way back, so the field lands on the bar exactly.\n * Reduced motion: the progress stays at 1, so the field is at its focused size at once.\n */\n dockedPanel: [\n // Leaving, the view takes no presses: the spring's tail runs on after the field has\n // landed on the bar, and a press there ran a suggestion of the invisible dropdown.\n 'fixed z-(--md-sys-z-sheet) flex flex-col outline-none data-exiting:pointer-events-none',\n '[--m3-search-progress:1] [--m3-search-p:max(0,var(--m3-search-progress))]',\n 'motion-safe:starting:[--m3-search-progress:0] motion-safe:data-exiting:[--m3-search-progress:0]',\n 'top-[calc(var(--m3-search-top)_+_var(--m3-search-p)_*_var(--m3-search-dy))]',\n 'left-[calc(var(--m3-search-left)_+_var(--m3-search-p)_*_var(--m3-search-dx))]',\n 'w-[calc(var(--m3-search-width)_+_var(--m3-search-p)_*_var(--m3-search-dw))]',\n // Both ways on the default spatial spring (Compose's collapse spec).\n '[transition-property:--m3-search-progress]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n ],\n // The focused field is 56px (M3), even when the collapsed bar is resized; it grows from\n // the bar's height with the panel.\n // The full-window layer the docked view sits in (React Aria's underlay).\n underlay: 'fixed inset-0 z-(--md-sys-z-sheet) data-exiting:pointer-events-none',\n dockedHeader:\n 'flex h-[calc(var(--m3-search-height)_+_var(--m3-search-p)_*_var(--m3-search-dh))] shrink-0',\n dropdown: [\n 'mt-[2px] overflow-y-auto overscroll-contain data-resizing:overflow-y-hidden data-exiting:pointer-events-none',\n // 58px is the field and the gap above the dropdown.\n 'min-h-[min(182px,calc(66.67dvh-58px))] max-h-[calc(66.67dvh-58px)]',\n // The height follows the content (`--m3-search-content-height`, measured) within those\n // bounds, so it animates as the results change. Without the measurement (before\n // hydration, or under reduced motion) it's `auto`.\n 'h-[clamp(min(182px,calc(66.67dvh_-_58px)),var(--m3-search-content-height),calc(66.67dvh_-_58px))]',\n 'rounded-[var(--_md-comp-search-view-docked-container-shape)] bg-[var(--_md-comp-search-view-container-color)] text-on-surface',\n 'forced-colors:border forced-colors:border-solid forced-colors:border-[CanvasText]',\n // Reduced motion: it only fades.\n 'translate-y-0 motion-safe:starting:-translate-y-1/2 motion-safe:data-exiting:-translate-y-1/2',\n // The delays below cycle, so the height (third) starts at once like the slide.\n '[transition-property:translate,opacity,height]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-default-duration),100ms,var(--md-sys-motion-spring-spatial-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing),cubic-bezier(0.3,0,1,1),var(--md-sys-motion-spring-spatial-default-easing)]',\n 'data-exiting:[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration),100ms,var(--md-sys-motion-spring-spatial-fast-duration)]',\n 'data-exiting:[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing),cubic-bezier(0,0,0,1),var(--md-sys-motion-spring-spatial-fast-easing)]',\n ...contentFade,\n ],\n /*\n * Full-screen view: the surface grows from the bar's bounds to the window as\n * `--m3-search-progress` runs 0 → 1, through a clip (nothing is transformed). Compose\n * clamps the full-screen progress to 0..1 (`coerceIn`), so `--m3-search-p` is clamped too:\n * the clip and the field never overshoot the window's or the bar's bounds.\n */\n fullScreenPanel: [\n 'fixed inset-0 z-(--md-sys-z-sheet) bg-[var(--_md-comp-search-view-container-color)] text-on-surface outline-none data-exiting:pointer-events-none',\n '[--m3-search-progress:1] [--m3-search-p:clamp(0,var(--m3-search-progress),1)]',\n 'motion-safe:starting:[--m3-search-progress:0] motion-safe:data-exiting:[--m3-search-progress:0]',\n '[clip-path:inset(calc((1_-_var(--m3-search-p))_*_var(--m3-search-top))_calc((1_-_var(--m3-search-p))_*_var(--m3-search-right))_calc((1_-_var(--m3-search-p))_*_var(--m3-search-bottom))_calc((1_-_var(--m3-search-p))_*_var(--m3-search-left))_round_calc((1_-_var(--m3-search-p))_*_28px))]',\n 'motion-safe:[transition-property:--m3-search-progress]',\n 'motion-safe:[transition-duration:var(--md-sys-motion-spring-spatial-slow-duration)]',\n 'motion-safe:[transition-timing-function:var(--md-sys-motion-spring-spatial-slow-easing)]',\n 'motion-safe:data-exiting:[transition-duration:var(--md-sys-motion-spring-spatial-default-duration)]',\n 'motion-safe:data-exiting:[transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n // Reduced motion: the view fades in and out over the window instead of growing.\n 'motion-reduce:starting:opacity-0 motion-reduce:data-exiting:opacity-0',\n 'motion-reduce:[transition-property:opacity]',\n 'motion-reduce:[transition-duration:var(--md-sys-motion-spring-effects-fast-duration)]',\n 'motion-reduce:[transition-timing-function:var(--md-sys-motion-spring-effects-fast-easing)]',\n ],\n // Compose's full-screen field is 56px whatever the bar's size, 8px down and 16px from each\n // side; it moves and grows there from the bar's bounds with the surface.\n fullScreenHeader: [\n 'absolute',\n 'start-[calc(var(--m3-search-start)_+_var(--m3-search-p)_*_(16px_-_var(--m3-search-start)))]',\n 'top-[calc(var(--m3-search-top)_+_var(--m3-search-p)_*_(8px_-_var(--m3-search-top)))]',\n 'w-[calc(var(--m3-search-width)_+_var(--m3-search-p)_*_(100%_-_32px_-_var(--m3-search-width)))]',\n 'h-[calc(var(--m3-search-height)_+_var(--m3-search-p)_*_(56px_-_var(--m3-search-height)))]',\n ],\n fullScreenContent: [\n 'absolute inset-x-0 top-[72px] bottom-0 overflow-y-auto overscroll-contain',\n '[transition-property:opacity] [transition-duration:100ms]',\n '[transition-timing-function:cubic-bezier(0.3,0,1,1)]',\n 'data-exiting:[transition-timing-function:cubic-bezier(0,0,0,1)]',\n ...contentFade,\n ],\n },\n variants: {\n /**\n * Inside a search app bar the bar fills the free space up to 312px, then grows to half\n * of it (M3), up to 720px.\n */\n inAppBar: { true: { root: 'w-[max(min(100%,312px),50%)]' }, false: {} },\n // 4px from a tap target (M3), else 16px from the pill's edge.\n hasLeading: { true: { input: 'ps-[4px]' }, false: { input: 'ps-[16px]' } },\n hasTrailing: { true: { input: 'pe-[4px]' }, false: { input: 'pe-[16px]' } },\n /**\n * Container colour: the bar's own; in a search app bar `surface-container`, and one\n * step higher than the bar once content scrolls under it.\n */\n tone: {\n default: { field: 'bg-[var(--_md-comp-search-bar-container-color)]' },\n appBar: { field: 'bg-surface-container' },\n scrolled: { field: 'bg-surface-container-highest' },\n },\n /** Centre the placeholder and the query while the bar is collapsed. */\n textAlign: { start: {}, center: { input: 'text-center' } },\n /**\n * A disabled `SearchField`: M3's disabled colours, the content `on-surface` at 38% and the\n * container at 12%; forced colours draw it in `GrayText`.\n */\n disabled: {\n true: {\n field: 'bg-on-surface/12 forced-colors:border-[GrayText]',\n leading: 'text-on-surface/38 forced-colors:text-[GrayText]',\n trailing: 'text-on-surface/38 forced-colors:text-[GrayText]',\n input:\n 'cursor-default text-on-surface/38 placeholder:text-on-surface/38 forced-colors:text-[GrayText]',\n },\n false: {},\n },\n },\n defaultVariants: {\n inAppBar: false,\n hasLeading: false,\n hasTrailing: false,\n tone: 'default',\n textAlign: 'start',\n disabled: false,\n },\n});\n\n/**\n * Variant definitions for {@link SearchField}'s root. Its pill and the parts in it are the search\n * bar's ({@link searchBarStyles}), so the two can't drift.\n */\nexport const searchFieldStyles = tv({\n slots: {\n // 56px, the search bar's height, less 4px a density step (44px at -3), filling its\n // container: it filters a page, a list or a pane in place. A consumer `w-*` or `h-*` wins.\n root: 'flex h-[calc(56px+var(--md-sys-density,0)*4px)] w-full min-w-0',\n // The 48px icon slots keep to a dense pill's height; their buttons keep 48px targets.\n slot: 'max-h-full',\n },\n variants: {},\n});\n\n/** Variant definitions for {@link SearchAppBar}. */\nexport const searchAppBarStyles = tv({\n slots: {\n root: [\n 'flex min-h-[64px] w-full items-center text-on-surface',\n 'bg-[var(--_md-comp-app-bar-container-color)] data-scrolled:bg-[var(--_md-comp-app-bar-on-scroll-container-color)]',\n '[transition-property:background-color]',\n '[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n ],\n // Icon buttons take each slot's content colour.\n navigation:\n 'flex shrink-0 items-center ps-[8px] pe-[4px] text-[color:var(--_md-comp-app-bar-leading-icon-color)] [--md-comp-icon-button-icon-color:currentColor]',\n // Centres the search bar in the free space, 8px from its neighbours and 4px vertically.\n search: 'flex min-w-0 flex-1 justify-center ps-[8px] pe-[8px] pt-[4px] pb-[4px]',\n actions:\n 'flex shrink-0 items-center gap-[8px] ps-[4px] pe-[8px] text-[color:var(--_md-comp-app-bar-trailing-icon-color)] [--md-comp-icon-button-icon-color:currentColor]',\n },\n variants: {\n scrolling: {\n true: { root: 'sticky top-(--m3-app-bar-offset) z-(--md-sys-z-sticky)' },\n false: {},\n },\n },\n defaultVariants: { scrolling: false },\n});\n\n/**\n * The variants of {@link searchBarStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type SearchBarStyleProps = VariantProps<typeof searchBarStyles>;\n/**\n * The variants of {@link searchAppBarStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names (`hasIcon`, `isOpen`…) follow the classes and may change.\n */\nexport type SearchAppBarStyleProps = VariantProps<typeof searchAppBarStyles>;\n/**\n * The variants of {@link searchFieldStyles}, for building on its styles. They are the recipe's keys, not\n * component props: their names follow the classes and may change.\n */\nexport type SearchFieldStyleProps = VariantProps<typeof searchFieldStyles>;\n"],"mappings":";;AA2CA,MAAM,cAAc,CAClB,yDAEA,kEACF;;AAGA,MAAa,kBAAkB,GAAG;CAChC,OAAO;EAEL,MAAM;EAIN,OAAO,CACL,iIACA,mFACF;EAIA,SACE;EAIF,UAAU,CACR,2MACA,8DACF;EAGA,OAAO;GACL;GACA;GACA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;EACF;EAUA,aAAa;GAGX;GACA;GACA;GACA;GACA;GACA;GAEA;GACA;GACA;EACF;EAIA,UAAU;EACV,cACE;EACF,UAAU;GACR;GAEA;GAIA;GACA;GACA;GAEA;GAEA;GACA;GACA;GACA;GACA;GACA,GAAG;EACL;EAOA,iBAAiB;GACf;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA;GACA;GACA;GACA;EACF;EAGA,kBAAkB;GAChB;GACA;GACA;GACA;GACA;EACF;EACA,mBAAmB;GACjB;GACA;GACA;GACA;GACA,GAAG;EACL;CACF;CACA,UAAU;;;;;EAKR,UAAU;GAAE,MAAM,EAAE,MAAM,+BAA+B;GAAG,OAAO,CAAC;EAAE;EAEtE,YAAY;GAAE,MAAM,EAAE,OAAO,WAAW;GAAG,OAAO,EAAE,OAAO,YAAY;EAAE;EACzE,aAAa;GAAE,MAAM,EAAE,OAAO,WAAW;GAAG,OAAO,EAAE,OAAO,YAAY;EAAE;;;;;EAK1E,MAAM;GACJ,SAAS,EAAE,OAAO,kDAAkD;GACpE,QAAQ,EAAE,OAAO,uBAAuB;GACxC,UAAU,EAAE,OAAO,+BAA+B;EACpD;;EAEA,WAAW;GAAE,OAAO,CAAC;GAAG,QAAQ,EAAE,OAAO,cAAc;EAAE;;;;;EAKzD,UAAU;GACR,MAAM;IACJ,OAAO;IACP,SAAS;IACT,UAAU;IACV,OACE;GACJ;GACA,OAAO,CAAC;EACV;CACF;CACA,iBAAiB;EACf,UAAU;EACV,YAAY;EACZ,aAAa;EACb,MAAM;EACN,WAAW;EACX,UAAU;CACZ;AACF,CAAC;;;;;AAMD,MAAa,oBAAoB,GAAG;CAClC,OAAO;EAGL,MAAM;EAEN,MAAM;CACR;CACA,UAAU,CAAC;AACb,CAAC;;AAGD,MAAa,qBAAqB,GAAG;CACnC,OAAO;EACL,MAAM;GACJ;GACA;GACA;GACA;GACA;EACF;EAEA,YACE;EAEF,QAAQ;EACR,SACE;CACJ;CACA,UAAU,EACR,WAAW;EACT,MAAM,EAAE,MAAM,yDAAyD;EACvE,OAAO,CAAC;CACV,EACF;CACA,iBAAiB,EAAE,WAAW,MAAM;AACtC,CAAC"}
@@ -156,7 +156,10 @@ type SelectProps$1<T extends object, M extends SelectionMode = 'single'> = Omit<
156
156
  * leaving it out) on a field that loads options.
157
157
  */
158
158
  loading?: boolean;
159
- /** Words the Select shows or announces, for other languages. */
159
+ /**
160
+ * Words the Select shows or announces, for other languages. A {@link LabelsProvider} sets them
161
+ * for the whole app.
162
+ */
160
163
  labels?: Partial<SelectLabels>;
161
164
  /** Classes for the root element, merged with `classNames.root`; consumer classes win. */
162
165
  className?: string;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { densityAttribute } from "../../tokens/density.js";
3
3
  import { cn } from "../../utils/cn.js";
4
+ import { useLabels } from "../labels/use-labels.js";
4
5
  import { usePreloadLoadingIndicator } from "../loading-indicator/lazy-loading-indicator.js";
5
6
  import { FOCUSABLE_DISABLED_OPTIONS } from "../../utils/focusable-disabled.js";
6
7
  import { assertCollectionChildren } from "../../utils/assert-collection-children.js";
@@ -67,10 +68,7 @@ function Select(props) {
67
68
  const { root: rootData, rest } = splitRootProps(otherProps, { wrapper: true });
68
69
  const invalid = invalidProp ?? ariaFlag(ariaInvalid);
69
70
  const required = requiredProp ?? ariaFlag(ariaRequired);
70
- const labels = {
71
- ...DEFAULT_LABELS,
72
- ...labelsProp
73
- };
71
+ const labels = useLabels("Select", DEFAULT_LABELS, labelsProp);
74
72
  const stately = rest;
75
73
  const cap = useSelectionCap({
76
74
  multiple: stately.selectionMode === "multiple",
@@ -1 +1 @@
1
- {"version":3,"file":"Select.js","names":[],"sources":["../../../src/components/select/Select.tsx"],"sourcesContent":["'use client';\n\nimport {\n useId,\n useRef,\n useSyncExternalStore,\n type AriaAttributes,\n type CSSProperties,\n type HTMLAttributes,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n HiddenSelect,\n mergeProps,\n useButton,\n useFocusRing,\n useHover,\n useObjectRef,\n useSelect,\n type AriaSelectOptions,\n} from 'react-aria';\nimport {\n useSelectState,\n type SelectionMode,\n type SelectProps as StatelySelectProps,\n} from 'react-stately/useSelectState';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport type { Key } from 'react-stately';\nimport { matchesSearch } from '../../utils/search';\nimport { assertCollectionChildren } from '../../utils/assert-collection-children';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport {\n ariaFlag,\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { useTypeaheadDelegate } from '../../utils/typeahead';\nimport { usePreloadLoadingIndicator } from '../loading-indicator/lazy-loading-indicator';\nimport { LazyBottomSheet, usePreloadBottomSheet } from '../sheet/lazy-bottom-sheet';\nimport type { TextFieldVariant } from '../text-field/text-field-styles';\nimport { DropdownArrow, FieldShell, fieldState, type FieldShellClassNames } from './field-shell';\nimport {\n FOCUSABLE_DISABLED_OPTIONS,\n OptionList,\n OptionPopover,\n type OptionListClassNames,\n} from './option-list';\nimport { SearchList } from './search-list';\nimport { useSelectionCap } from './selection-cap';\n\n/** How the options open: a menu under the field (M3), a bottom sheet, or a sheet on phones. */\nexport type SelectPresentation = 'menu' | 'sheet' | 'auto';\n\n/** Words the Select shows or announces; override them for other languages. */\nexport interface SelectLabels {\n /** Shown in an empty list. @default \"No options\" */\n empty: string;\n /** The loading indicator's name. @default \"Loading options\" */\n loading: string;\n /** For more chosen options than fit: `+{count}`. @default (count) => `+${count}` */\n more: (count: number) => string;\n /** With `searchable`: the search field's placeholder and name. @default \"Search\" */\n search: string;\n /** With `searchable`: shown when no option matches the search. @default \"No results\" */\n noResults: string;\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n}\n\nconst DEFAULT_LABELS: SelectLabels = {\n empty: 'No options',\n loading: 'Loading options',\n more: (count) => `+${count}`,\n search: 'Search',\n noResults: 'No results',\n error: 'Error',\n};\n\n/** A chosen option, as `renderValue` receives it. */\nexport interface SelectedOption<T> {\n key: Key;\n /** Its plain text (`textValue`, or the item's text). */\n textValue: string;\n /** Its object from `items`, if the options come from `items`. */\n value: T | null;\n}\n\nexport interface SelectClassNames extends FieldShellClassNames, OptionListClassNames {\n /** The text showing the chosen option(s) or the placeholder. */\n value?: string;\n /** With `searchable`: the search field. */\n search?: string;\n /**\n * The bottom sheet the options open in (`presentation=\"sheet\"`, or `\"auto\"` on compact\n * windows): the sheet's surface, as its `classNames.panel`.\n */\n sheet?: string;\n}\n\n/** React Aria props this API names the library's way (`disabled`, `open`…), or leaves out. */\ntype RenamedProps =\n | 'isDisabled'\n | 'isRequired'\n | 'isInvalid'\n | 'isOpen'\n | 'label'\n | 'description'\n | 'validationState'\n // React Stately's deprecated single-selection API; `value` / `defaultValue` / `onChange` replace it.\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange'\n | 'shouldCloseOnSelect'\n | 'allowsEmptyCollection';\n\nexport type SelectProps<T extends object, M extends SelectionMode = 'single'> = Omit<\n StatelySelectProps<T, M>,\n RenamedProps\n> &\n Pick<AriaSelectOptions<T, M>, 'id' | 'name' | 'autoComplete' | 'form'> &\n Omit<WrapperRootDOMProps, keyof StatelySelectProps<T, M>> & {\n /** The trigger button'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 * 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 /**\n * Shown in the field when nothing is chosen. With a `label`, it appears once the label\n * floats (on focus), as in `TextField`.\n */\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 choice is required: adds the `*` to the label and validates on submit. */\n required?: boolean;\n /**\n * Read as `invalid` when that's left out, for a form library that sets the attribute. The\n * trigger is a button, which ARIA doesn't let carry `aria-invalid`, so the state reaches\n * assistive technology as the field's error text, its description.\n */\n 'aria-invalid'?: AriaAttributes['aria-invalid'];\n /**\n * Read as `required` when that's left out, for a form library that sets the attribute: the\n * label's `*`, in the trigger's name (a button can't carry `aria-required`).\n */\n 'aria-required'?: AriaAttributes['aria-required'];\n /** Disables the field: it can't be focused or opened. @default false */\n disabled?: boolean;\n /**\n * The choice can be seen but not changed: the menu doesn't open and the keyboard doesn't\n * change it. The root gets `data-readonly`; say so in the supporting text (M3).\n * @default false\n */\n readOnly?: boolean;\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 /** Whether the menu is open (controlled); with `defaultOpen` and `onOpenChange`. */\n open?: boolean;\n /**\n * Whether choosing an option closes the menu.\n * @default true for a single choice, false with `selectionMode=\"multiple\"`\n */\n closeOnSelect?: 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 * `menu` (default) opens an M3 menu under the field on every window, as Compose's exposed\n * dropdown menu does; `sheet` always opens a bottom sheet; `auto` opens a sheet on\n * compact windows and a menu on larger ones, for long lists.\n * @default \"menu\"\n */\n presentation?: SelectPresentation;\n /**\n * A search field at the top of the menu or sheet that filters the options as you type,\n * for long lists (pair it with `presentation=\"auto\"`). The search takes focus in the menu;\n * a sheet shows the list first, so the keyboard doesn't cover it.\n * @default false\n */\n searchable?: boolean;\n /**\n * With `searchable`: whether an option matches the search. Defaults to a match anywhere in\n * its text, ignoring case and accents (\"thai\" finds \"Thailand\"). The option's `key` lets\n * it match on more than its text, e.g. a country's ISO code.\n */\n filter?: (textValue: string, search: string, key: Key) => boolean;\n /** With `searchable`: the typed text (controlled); cleared when the list closes. */\n searchValue?: string;\n /** With `searchable`: the initial typed text, when uncontrolled. @default \"\" */\n defaultSearchValue?: string;\n /**\n * With `searchable`: called as the search changes. Given it, the app filters `items` itself\n * (e.g. on a server, with `loading` meanwhile) and the built-in filter is off.\n */\n onSearchChange?: (search: string) => void;\n /**\n * What the field shows for the chosen option(s), e.g. a dialling code or a flag, instead of\n * their text. Screen readers still hear the options' text.\n */\n renderValue?: (selected: SelectedOption<T>[]) => ReactNode;\n /**\n * Shows a loading indicator in the list, e.g. while options are fetched.\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 /** Words the Select shows or announces, for other languages. */\n labels?: Partial<SelectLabels>;\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?: SelectClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Ref to the root `<div>` element. Use `triggerRef` for the field's button. */\n ref?: Ref<HTMLDivElement>;\n /** Ref to the field's button. */\n triggerRef?: Ref<HTMLButtonElement>;\n };\n\nconst COMPACT_WINDOW = '(max-width: 599.98px)';\n\n/**\n * A searchable list's sheet takes the full height it may (above an on-screen keyboard too), so\n * filtering changes the list inside, never the sheet: at its own height it shrank as the\n * results did, and sank behind the keyboard (2026-11-07).\n */\nconst SEARCH_SHEET_HEIGHT =\n 'h-[calc(100dvh_-_72px_-_var(--m3-sheet-keyboard-inset,0px))] min-[641px]:h-[calc(100dvh_-_56px_-_var(--m3-sheet-keyboard-inset,0px))]';\n\n/** Options scrolled to by the keyboard stop below the sticky search (52px at most). */\nconst SEARCH_SHEET_CONTENT = 'scroll-pt-[52px]';\n\n/** A read-only Select's keyboard finds no option, so arrows and typeahead change nothing. */\nconst READ_ONLY_DELEGATE: NonNullable<AriaSelectOptions<object>['keyboardDelegate']> = {\n getFirstKey: () => null,\n getLastKey: () => null,\n getKeyBelow: () => null,\n getKeyAbove: () => null,\n getKeyForSearch: () => null,\n};\n\nfunction subscribeToWindow(onChange: () => void) {\n const query = window.matchMedia(COMPACT_WINDOW);\n query.addEventListener('change', onChange);\n return () => query.removeEventListener('change', onChange);\n}\n\n/**\n * M3 exposed dropdown menu, read-only (Compose's `ExposedDropdownMenuBox` with a\n * `PrimaryNotEditable` anchor): a text-field-styled button that opens a menu of options.\n * Single or multiple selection, sections, descriptions and icons, typeahead, a hidden\n * native `<select>` for forms, and validation as in `TextField` (React Aria `useSelect`).\n *\n * @example\n * <Select label=\"Sort by\" defaultValue=\"newest\">\n * <SelectItem key=\"newest\">Newest first</SelectItem>\n * <SelectItem key=\"price\">Price, low to high</SelectItem>\n * </Select>\n */\nexport function Select<T extends object, M extends SelectionMode = 'single'>(\n props: SelectProps<T, M>,\n) {\n const {\n variant = 'filled',\n density,\n label,\n placeholder,\n supportingText,\n invalid: invalidProp,\n required: requiredProp,\n 'aria-invalid': ariaInvalid,\n 'aria-required': ariaRequired,\n disabled = false,\n readOnly = false,\n leadingIcon,\n errorIcon,\n open,\n maxSelections,\n presentation = 'menu',\n searchable = false,\n filter = matchesSearch,\n searchValue,\n defaultSearchValue,\n onSearchChange,\n renderValue,\n loading,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n triggerRef: triggerRefProp,\n name,\n autoComplete,\n form,\n errorMessage,\n closeOnSelect,\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(props.children, 'Select', 'SelectItem and SelectSection elements');\n }\n const { root: rootData, rest } = splitRootProps(otherProps, { wrapper: true });\n const invalid = invalidProp ?? ariaFlag(ariaInvalid);\n const required = requiredProp ?? ariaFlag(ariaRequired);\n const labels = { ...DEFAULT_LABELS, ...labelsProp };\n const stately = rest as Omit<StatelySelectProps<T, M>, RenamedProps>;\n const cap = useSelectionCap({\n multiple: stately.selectionMode === 'multiple',\n maxSelections,\n value: stately.value,\n defaultValue: stately.defaultValue,\n onChange: stately.onChange,\n });\n const [search, setSearch] = useControlledState(\n searchValue,\n defaultSearchValue ?? '',\n onSearchChange,\n );\n const ariaProps = {\n ...stately,\n ...cap.valueProps,\n // The search starts afresh each time the list opens.\n onOpenChange: (isOpen: boolean) => {\n if (!isOpen && search !== '') setSearch('');\n stately.onOpenChange?.(isOpen);\n },\n label,\n description: supportingText,\n errorMessage,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n ...(closeOnSelect !== undefined && { shouldCloseOnSelect: closeOnSelect }),\n ...FOCUSABLE_DISABLED_OPTIONS,\n ...(readOnly\n ? { isOpen: false, keyboardDelegate: READ_ONLY_DELEGATE }\n : open !== undefined && { isOpen: open }),\n placeholder,\n name,\n autoComplete,\n form,\n } as AriaSelectOptions<T, M>;\n\n const state = useSelectState<T, M>(ariaProps);\n const triggerRef = useObjectRef(triggerRefProp);\n // Typeahead on the closed field moves to the next option starting with the letter (M3),\n // and still skips disabled options.\n const typeaheadDelegate = useTypeaheadDelegate(state, triggerRef, {\n disabledBehavior: 'all',\n currentKey: state.selectionManager.selectionMode === 'single' ? state.selectedKey : null,\n });\n const containerRef = useRef<HTMLDivElement>(null);\n // Set by a press outside the menu and the field: the focus the menu hands back to the\n // field on closing (WAI-ARIA, right after Escape or a choice) is let go, so one press\n // outside leaves the field, as it does any other field.\n const releaseFocusRef = useRef(false);\n // Chrome's touch adjustment can split a tap between the trigger and the padding just above\n // or below it (the 8px either side of the value line): `pointerdown` reaches the trigger\n // while `pointerup` or the click lands beside it, so React Aria cancels the press or never\n // sees it, and about a third of the field ignored a finger (2026-10-17). A touch that\n // starts on the trigger but doesn't end there (its `pointerup` and its click both on it)\n // opens the menu on its click instead. The mouse never gets here: it opens on the press\n // start.\n const touchOnTriggerRef = useRef(false);\n const touchLeftTriggerRef = useRef(false);\n const onTrigger = (target: EventTarget) => Boolean(triggerRef.current?.contains(target as Node));\n const strayTouchProps: HTMLAttributes<HTMLDivElement> = {\n onPointerDownCapture: (event) => {\n touchOnTriggerRef.current = event.pointerType !== 'mouse' && onTrigger(event.target);\n touchLeftTriggerRef.current = false;\n },\n onPointerUpCapture: (event) => {\n touchLeftTriggerRef.current = !onTrigger(event.target);\n },\n onClickCapture: (event) => {\n if (!touchOnTriggerRef.current) return;\n touchOnTriggerRef.current = false;\n if (!touchLeftTriggerRef.current && onTrigger(event.target)) return;\n if (disabled || state.isOpen) return;\n releaseFocusRef.current = false;\n triggerRef.current?.focus({ preventScroll: true });\n if (!readOnly) state.open();\n },\n };\n const listBoxRef = useRef<HTMLUListElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const dialogId = useId();\n const {\n labelProps,\n triggerProps,\n valueProps,\n menuProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useSelect<T, M>(\n readOnly ? ariaProps : { ...ariaProps, keyboardDelegate: typeaheadDelegate },\n state,\n triggerRef,\n );\n const isCompact = useSyncExternalStore(\n subscribeToWindow,\n () => window.matchMedia(COMPACT_WINDOW).matches,\n () => false,\n );\n const asSheet = presentation === 'sheet' || (presentation === 'auto' && isCompact);\n // The sheet's code loads on first open, or ahead of it when the field is hovered or focused.\n usePreloadBottomSheet(containerRef, asSheet);\n // A field that takes `loading` (it loads options) preloads the indicator on hover or focus.\n usePreloadLoadingIndicator(containerRef, loading !== undefined);\n // `useSelect`'s trigger props are React Aria button props; `useButton` makes them DOM props.\n const { buttonProps } = useButton(\n // A searchable field opens a dialog holding the search and the list (as PhoneField's).\n searchable\n ? {\n ...triggerProps,\n 'aria-haspopup': 'dialog',\n 'aria-controls': state.isOpen && !asSheet ? dialogId : undefined,\n }\n : triggerProps,\n triggerRef,\n );\n const { focusProps, isFocusVisible, isFocused } = useFocusRing();\n // The trigger sets its own widget attributes; an app's description ones join them.\n const fieldButtonProps = mergeProps(pickDescriptionAria(rest), buttonProps, focusProps);\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n // In the list's order, not the order they were picked, so the field reads like the list.\n const chosenOptions: SelectedOption<T>[] = [...state.collection.getKeys()]\n .filter((key) => state.selectionManager.isSelected(key))\n .map((key) => {\n const item = state.collection.getItem(key);\n return { key, textValue: item?.textValue ?? '', value: item?.value ?? null };\n });\n const chosen = chosenOptions.map((option) => option.textValue);\n const valueText =\n chosen.length > 2\n ? `${chosen.slice(0, 2).join(', ')} ${labels.more(chosen.length - 2)}`\n : chosen.join(', ');\n const hasValue = chosen.length > 0;\n const hasLabel = label !== undefined && label !== null;\n const focused = isFocused || state.isOpen;\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n\n const searchLabelling = hasLabel\n ? { 'aria-labelledby': labelProps.id }\n : { 'aria-label': (rest as { 'aria-label'?: string })['aria-label'] };\n const list = searchable ? (\n <SearchList\n listProps={{\n ...(stately.items !== undefined && { items: stately.items }),\n children: stately.children,\n ...(stately.disabledKeys !== undefined && { disabledKeys: stately.disabledKeys }),\n selectionMode: stately.selectionMode ?? 'single',\n // A single choice stays chosen when pressed again, as in a non-searchable Select.\n disallowEmptySelection: stately.selectionMode !== 'multiple',\n selectedKeys: state.selectionManager.selectedKeys,\n onSelectionChange: (keys) => {\n state.selectionManager.setSelectedKeys(keys === 'all' ? [] : keys);\n if (stately.selectionMode !== 'multiple') state.close();\n },\n }}\n search={search}\n onSearchChange={setSearch}\n filter={onSearchChange ? null : filter}\n autoFocusSearch={!asSheet}\n labelling={searchLabelling}\n dialogProps={asSheet ? undefined : { id: dialogId, role: 'dialog', ...searchLabelling }}\n labels={{ search: labels.search, noResults: labels.noResults, loading: labels.loading }}\n loading={loading ?? false}\n isBlocked={cap.isBlocked}\n embedded={asSheet}\n classNames={classNames}\n />\n ) : (\n <OptionList\n embedded={asSheet}\n listBoxProps={menuProps}\n state={state}\n listBoxRef={listBoxRef}\n emptyLabel={labels.empty}\n loading={loading}\n loadingLabel={labels.loading}\n isBlocked={cap.isBlocked}\n classNames={classNames}\n />\n );\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={hasValue || focused}\n open={state.isOpen}\n leadingIcon={leadingIcon}\n trailing={<DropdownArrow open={state.isOpen} />}\n errorIcon={errorIcon}\n errorIconLabel={labels.error}\n readOnly={readOnly}\n supportingText={supportingText}\n descriptionProps={descriptionProps}\n errorText={resolvedError}\n errorMessageProps={errorMessageProps}\n showError={isInvalid && Boolean(resolvedError)}\n containerRef={containerRef}\n // No handler is shared: the hover ones and the touch fallback's capture ones.\n containerProps={{ ...hoverProps, ...strayTouchProps }}\n onContainerPointerDown={(event) => {\n // A press anywhere on the field (not only its text) opens it, like Compose.\n if (disabled || triggerRef.current?.contains(event.target as Node)) return;\n event.preventDefault();\n releaseFocusRef.current = false;\n triggerRef.current?.focus();\n if (!readOnly) state.toggle();\n }}\n rootProps={{ ...rootData, 'data-focus-visible': isFocusVisible || undefined }}\n rootRef={ref}\n className={className}\n classNames={classNames}\n style={style}\n >\n {/* The native select is for forms: submitting under `name`, autofill and native\n validation. Without them it's left out, so a long list's text (e.g. country names\n from Intl, which can differ between server and browser) isn't rendered twice. */}\n {name || autoComplete || form || stately.validationBehavior === 'native' ? (\n <HiddenSelect\n state={state}\n triggerRef={triggerRef}\n label={label}\n name={name}\n isDisabled={disabled}\n {...(autoComplete && { autoComplete })}\n {...(form && { form })}\n />\n ) : null}\n <button\n {...fieldButtonProps}\n aria-describedby={renderedDescription(fieldButtonProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !(isInvalid && resolvedError) && errorMessageProps.id,\n ])}\n onFocus={(event) => {\n fieldButtonProps.onFocus?.(event);\n if (!releaseFocusRef.current) return;\n releaseFocusRef.current = false;\n triggerRef.current?.blur();\n }}\n // Using the field again means it's wanted, whatever came before.\n onPointerDown={(event) => {\n releaseFocusRef.current = false;\n fieldButtonProps.onPointerDown?.(event);\n }}\n onKeyDown={(event) => {\n releaseFocusRef.current = false;\n fieldButtonProps.onKeyDown?.(event);\n }}\n ref={triggerRef}\n type=\"button\"\n className={cn(\n 'flex min-w-0 flex-1 cursor-pointer items-center self-stretch bg-transparent p-0 text-start outline-none',\n 'disabled:cursor-default',\n )}\n >\n <span\n {...valueProps}\n className={cn(\n 'min-w-0 flex-1 truncate text-body-large',\n hasValue\n ? 'text-[color:var(--_md-comp-text-field-input-text-color)]'\n : 'text-on-surface-variant',\n // With a label, the placeholder shows once the label floats (as TextField).\n !hasValue && hasLabel && 'opacity-0 group-data-floated/field:opacity-100',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n classNames?.value,\n )}\n >\n {hasValue && renderValue ? (\n <>\n <span aria-hidden=\"true\">{renderValue(chosenOptions)}</span>\n <span className=\"sr-only\">{valueText}</span>\n </>\n ) : hasValue ? (\n valueText\n ) : (\n (placeholder ?? ' ')\n )}\n </span>\n </button>\n </FieldShell>\n {asSheet ? (\n <LazyBottomSheet\n // Opens at its own height (up to the window, the list scrolling inside), never half\n // the window, which left the last options off-screen on a short phone.\n skipPartiallyExpanded\n open={state.isOpen}\n onOpenChange={(open) => (open ? state.open() : state.close())}\n classNames={\n searchable\n ? {\n panel: cn(SEARCH_SHEET_HEIGHT, classNames?.sheet),\n content: SEARCH_SHEET_CONTENT,\n }\n : classNames?.sheet\n ? { panel: classNames.sheet }\n : undefined\n }\n {...(typeof label === 'string'\n ? { 'aria-label': label }\n : { 'aria-label': (rest as { 'aria-label'?: string })['aria-label'] ?? '' })}\n >\n <div\n {...densityAttribute(density)}\n className=\"flex min-h-0 flex-1 flex-col px-[8px] pb-[16px]\"\n >\n {list}\n </div>\n </LazyBottomSheet>\n ) : (\n <OptionPopover\n state={state}\n triggerRef={containerRef}\n popoverRef={popoverRef}\n density={density}\n onPressOutside={() => {\n releaseFocusRef.current = true;\n }}\n // Room to type in a searchable menu, however narrow the field.\n minWidth={searchable ? 280 : 0}\n >\n {list}\n </OptionPopover>\n )}\n </>\n );\n}\n\nexport type { Key as SelectKey } from 'react-stately';\n"],"mappings":"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"}
1
+ {"version":3,"file":"Select.js","names":[],"sources":["../../../src/components/select/Select.tsx"],"sourcesContent":["'use client';\n\nimport {\n useId,\n useRef,\n useSyncExternalStore,\n type AriaAttributes,\n type CSSProperties,\n type HTMLAttributes,\n type ReactNode,\n type Ref,\n} from 'react';\nimport {\n HiddenSelect,\n mergeProps,\n useButton,\n useFocusRing,\n useHover,\n useObjectRef,\n useSelect,\n type AriaSelectOptions,\n} from 'react-aria';\nimport {\n useSelectState,\n type SelectionMode,\n type SelectProps as StatelySelectProps,\n} from 'react-stately/useSelectState';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport type { Key } from 'react-stately';\nimport { useLabels } from '../labels/use-labels';\nimport { matchesSearch } from '../../utils/search';\nimport { assertCollectionChildren } from '../../utils/assert-collection-children';\nimport { densityAttribute, type Density } from '../../tokens/density';\nimport { cn } from '../../utils/cn';\nimport { renderedDescription } from '../../utils/rendered-description';\nimport {\n ariaFlag,\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { useTypeaheadDelegate } from '../../utils/typeahead';\nimport { usePreloadLoadingIndicator } from '../loading-indicator/lazy-loading-indicator';\nimport { LazyBottomSheet, usePreloadBottomSheet } from '../sheet/lazy-bottom-sheet';\nimport type { TextFieldVariant } from '../text-field/text-field-styles';\nimport { DropdownArrow, FieldShell, fieldState, type FieldShellClassNames } from './field-shell';\nimport {\n FOCUSABLE_DISABLED_OPTIONS,\n OptionList,\n OptionPopover,\n type OptionListClassNames,\n} from './option-list';\nimport { SearchList } from './search-list';\nimport { useSelectionCap } from './selection-cap';\n\n/** How the options open: a menu under the field (M3), a bottom sheet, or a sheet on phones. */\nexport type SelectPresentation = 'menu' | 'sheet' | 'auto';\n\n/** Words the Select shows or announces; override them for other languages. */\nexport interface SelectLabels {\n /** Shown in an empty list. @default \"No options\" */\n empty: string;\n /** The loading indicator's name. @default \"Loading options\" */\n loading: string;\n /** For more chosen options than fit: `+{count}`. @default (count) => `+${count}` */\n more: (count: number) => string;\n /** With `searchable`: the search field's placeholder and name. @default \"Search\" */\n search: string;\n /** With `searchable`: shown when no option matches the search. @default \"No results\" */\n noResults: string;\n /** The error icon's name (M3: \"Error\"). @default \"Error\" */\n error: string;\n}\n\nconst DEFAULT_LABELS: SelectLabels = {\n empty: 'No options',\n loading: 'Loading options',\n more: (count) => `+${count}`,\n search: 'Search',\n noResults: 'No results',\n error: 'Error',\n};\n\n/** A chosen option, as `renderValue` receives it. */\nexport interface SelectedOption<T> {\n key: Key;\n /** Its plain text (`textValue`, or the item's text). */\n textValue: string;\n /** Its object from `items`, if the options come from `items`. */\n value: T | null;\n}\n\nexport interface SelectClassNames extends FieldShellClassNames, OptionListClassNames {\n /** The text showing the chosen option(s) or the placeholder. */\n value?: string;\n /** With `searchable`: the search field. */\n search?: string;\n /**\n * The bottom sheet the options open in (`presentation=\"sheet\"`, or `\"auto\"` on compact\n * windows): the sheet's surface, as its `classNames.panel`.\n */\n sheet?: string;\n}\n\n/** React Aria props this API names the library's way (`disabled`, `open`…), or leaves out. */\ntype RenamedProps =\n | 'isDisabled'\n | 'isRequired'\n | 'isInvalid'\n | 'isOpen'\n | 'label'\n | 'description'\n | 'validationState'\n // React Stately's deprecated single-selection API; `value` / `defaultValue` / `onChange` replace it.\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange'\n | 'shouldCloseOnSelect'\n | 'allowsEmptyCollection';\n\nexport type SelectProps<T extends object, M extends SelectionMode = 'single'> = Omit<\n StatelySelectProps<T, M>,\n RenamedProps\n> &\n Pick<AriaSelectOptions<T, M>, 'id' | 'name' | 'autoComplete' | 'form'> &\n Omit<WrapperRootDOMProps, keyof StatelySelectProps<T, M>> & {\n /** The trigger button'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 * 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 /**\n * Shown in the field when nothing is chosen. With a `label`, it appears once the label\n * floats (on focus), as in `TextField`.\n */\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 choice is required: adds the `*` to the label and validates on submit. */\n required?: boolean;\n /**\n * Read as `invalid` when that's left out, for a form library that sets the attribute. The\n * trigger is a button, which ARIA doesn't let carry `aria-invalid`, so the state reaches\n * assistive technology as the field's error text, its description.\n */\n 'aria-invalid'?: AriaAttributes['aria-invalid'];\n /**\n * Read as `required` when that's left out, for a form library that sets the attribute: the\n * label's `*`, in the trigger's name (a button can't carry `aria-required`).\n */\n 'aria-required'?: AriaAttributes['aria-required'];\n /** Disables the field: it can't be focused or opened. @default false */\n disabled?: boolean;\n /**\n * The choice can be seen but not changed: the menu doesn't open and the keyboard doesn't\n * change it. The root gets `data-readonly`; say so in the supporting text (M3).\n * @default false\n */\n readOnly?: boolean;\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 /** Whether the menu is open (controlled); with `defaultOpen` and `onOpenChange`. */\n open?: boolean;\n /**\n * Whether choosing an option closes the menu.\n * @default true for a single choice, false with `selectionMode=\"multiple\"`\n */\n closeOnSelect?: 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 * `menu` (default) opens an M3 menu under the field on every window, as Compose's exposed\n * dropdown menu does; `sheet` always opens a bottom sheet; `auto` opens a sheet on\n * compact windows and a menu on larger ones, for long lists.\n * @default \"menu\"\n */\n presentation?: SelectPresentation;\n /**\n * A search field at the top of the menu or sheet that filters the options as you type,\n * for long lists (pair it with `presentation=\"auto\"`). The search takes focus in the menu;\n * a sheet shows the list first, so the keyboard doesn't cover it.\n * @default false\n */\n searchable?: boolean;\n /**\n * With `searchable`: whether an option matches the search. Defaults to a match anywhere in\n * its text, ignoring case and accents (\"thai\" finds \"Thailand\"). The option's `key` lets\n * it match on more than its text, e.g. a country's ISO code.\n */\n filter?: (textValue: string, search: string, key: Key) => boolean;\n /** With `searchable`: the typed text (controlled); cleared when the list closes. */\n searchValue?: string;\n /** With `searchable`: the initial typed text, when uncontrolled. @default \"\" */\n defaultSearchValue?: string;\n /**\n * With `searchable`: called as the search changes. Given it, the app filters `items` itself\n * (e.g. on a server, with `loading` meanwhile) and the built-in filter is off.\n */\n onSearchChange?: (search: string) => void;\n /**\n * What the field shows for the chosen option(s), e.g. a dialling code or a flag, instead of\n * their text. Screen readers still hear the options' text.\n */\n renderValue?: (selected: SelectedOption<T>[]) => ReactNode;\n /**\n * Shows a loading indicator in the list, e.g. while options are fetched.\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 * Words the Select shows or announces, for other languages. A {@link LabelsProvider} sets them\n * for the whole app.\n */\n labels?: Partial<SelectLabels>;\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?: SelectClassNames;\n /** Inline styles for the root element. */\n style?: CSSProperties;\n /** Ref to the root `<div>` element. Use `triggerRef` for the field's button. */\n ref?: Ref<HTMLDivElement>;\n /** Ref to the field's button. */\n triggerRef?: Ref<HTMLButtonElement>;\n };\n\nconst COMPACT_WINDOW = '(max-width: 599.98px)';\n\n/**\n * A searchable list's sheet takes the full height it may (above an on-screen keyboard too), so\n * filtering changes the list inside, never the sheet: at its own height it shrank as the\n * results did, and sank behind the keyboard (2026-11-07).\n */\nconst SEARCH_SHEET_HEIGHT =\n 'h-[calc(100dvh_-_72px_-_var(--m3-sheet-keyboard-inset,0px))] min-[641px]:h-[calc(100dvh_-_56px_-_var(--m3-sheet-keyboard-inset,0px))]';\n\n/** Options scrolled to by the keyboard stop below the sticky search (52px at most). */\nconst SEARCH_SHEET_CONTENT = 'scroll-pt-[52px]';\n\n/** A read-only Select's keyboard finds no option, so arrows and typeahead change nothing. */\nconst READ_ONLY_DELEGATE: NonNullable<AriaSelectOptions<object>['keyboardDelegate']> = {\n getFirstKey: () => null,\n getLastKey: () => null,\n getKeyBelow: () => null,\n getKeyAbove: () => null,\n getKeyForSearch: () => null,\n};\n\nfunction subscribeToWindow(onChange: () => void) {\n const query = window.matchMedia(COMPACT_WINDOW);\n query.addEventListener('change', onChange);\n return () => query.removeEventListener('change', onChange);\n}\n\n/**\n * M3 exposed dropdown menu, read-only (Compose's `ExposedDropdownMenuBox` with a\n * `PrimaryNotEditable` anchor): a text-field-styled button that opens a menu of options.\n * Single or multiple selection, sections, descriptions and icons, typeahead, a hidden\n * native `<select>` for forms, and validation as in `TextField` (React Aria `useSelect`).\n *\n * @example\n * <Select label=\"Sort by\" defaultValue=\"newest\">\n * <SelectItem key=\"newest\">Newest first</SelectItem>\n * <SelectItem key=\"price\">Price, low to high</SelectItem>\n * </Select>\n */\nexport function Select<T extends object, M extends SelectionMode = 'single'>(\n props: SelectProps<T, M>,\n) {\n const {\n variant = 'filled',\n density,\n label,\n placeholder,\n supportingText,\n invalid: invalidProp,\n required: requiredProp,\n 'aria-invalid': ariaInvalid,\n 'aria-required': ariaRequired,\n disabled = false,\n readOnly = false,\n leadingIcon,\n errorIcon,\n open,\n maxSelections,\n presentation = 'menu',\n searchable = false,\n filter = matchesSearch,\n searchValue,\n defaultSearchValue,\n onSearchChange,\n renderValue,\n loading,\n labels: labelsProp,\n className,\n classNames,\n style,\n ref,\n triggerRef: triggerRefProp,\n name,\n autoComplete,\n form,\n errorMessage,\n closeOnSelect,\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(props.children, 'Select', 'SelectItem and SelectSection elements');\n }\n const { root: rootData, rest } = splitRootProps(otherProps, { wrapper: true });\n const invalid = invalidProp ?? ariaFlag(ariaInvalid);\n const required = requiredProp ?? ariaFlag(ariaRequired);\n const labels = useLabels('Select', DEFAULT_LABELS, labelsProp);\n const stately = rest as Omit<StatelySelectProps<T, M>, RenamedProps>;\n const cap = useSelectionCap({\n multiple: stately.selectionMode === 'multiple',\n maxSelections,\n value: stately.value,\n defaultValue: stately.defaultValue,\n onChange: stately.onChange,\n });\n const [search, setSearch] = useControlledState(\n searchValue,\n defaultSearchValue ?? '',\n onSearchChange,\n );\n const ariaProps = {\n ...stately,\n ...cap.valueProps,\n // The search starts afresh each time the list opens.\n onOpenChange: (isOpen: boolean) => {\n if (!isOpen && search !== '') setSearch('');\n stately.onOpenChange?.(isOpen);\n },\n label,\n description: supportingText,\n errorMessage,\n isInvalid: invalid,\n isRequired: required,\n isDisabled: disabled,\n ...(closeOnSelect !== undefined && { shouldCloseOnSelect: closeOnSelect }),\n ...FOCUSABLE_DISABLED_OPTIONS,\n ...(readOnly\n ? { isOpen: false, keyboardDelegate: READ_ONLY_DELEGATE }\n : open !== undefined && { isOpen: open }),\n placeholder,\n name,\n autoComplete,\n form,\n } as AriaSelectOptions<T, M>;\n\n const state = useSelectState<T, M>(ariaProps);\n const triggerRef = useObjectRef(triggerRefProp);\n // Typeahead on the closed field moves to the next option starting with the letter (M3),\n // and still skips disabled options.\n const typeaheadDelegate = useTypeaheadDelegate(state, triggerRef, {\n disabledBehavior: 'all',\n currentKey: state.selectionManager.selectionMode === 'single' ? state.selectedKey : null,\n });\n const containerRef = useRef<HTMLDivElement>(null);\n // Set by a press outside the menu and the field: the focus the menu hands back to the\n // field on closing (WAI-ARIA, right after Escape or a choice) is let go, so one press\n // outside leaves the field, as it does any other field.\n const releaseFocusRef = useRef(false);\n // Chrome's touch adjustment can split a tap between the trigger and the padding just above\n // or below it (the 8px either side of the value line): `pointerdown` reaches the trigger\n // while `pointerup` or the click lands beside it, so React Aria cancels the press or never\n // sees it, and about a third of the field ignored a finger (2026-10-17). A touch that\n // starts on the trigger but doesn't end there (its `pointerup` and its click both on it)\n // opens the menu on its click instead. The mouse never gets here: it opens on the press\n // start.\n const touchOnTriggerRef = useRef(false);\n const touchLeftTriggerRef = useRef(false);\n const onTrigger = (target: EventTarget) => Boolean(triggerRef.current?.contains(target as Node));\n const strayTouchProps: HTMLAttributes<HTMLDivElement> = {\n onPointerDownCapture: (event) => {\n touchOnTriggerRef.current = event.pointerType !== 'mouse' && onTrigger(event.target);\n touchLeftTriggerRef.current = false;\n },\n onPointerUpCapture: (event) => {\n touchLeftTriggerRef.current = !onTrigger(event.target);\n },\n onClickCapture: (event) => {\n if (!touchOnTriggerRef.current) return;\n touchOnTriggerRef.current = false;\n if (!touchLeftTriggerRef.current && onTrigger(event.target)) return;\n if (disabled || state.isOpen) return;\n releaseFocusRef.current = false;\n triggerRef.current?.focus({ preventScroll: true });\n if (!readOnly) state.open();\n },\n };\n const listBoxRef = useRef<HTMLUListElement>(null);\n const popoverRef = useRef<HTMLDivElement>(null);\n const dialogId = useId();\n const {\n labelProps,\n triggerProps,\n valueProps,\n menuProps,\n descriptionProps,\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n } = useSelect<T, M>(\n readOnly ? ariaProps : { ...ariaProps, keyboardDelegate: typeaheadDelegate },\n state,\n triggerRef,\n );\n const isCompact = useSyncExternalStore(\n subscribeToWindow,\n () => window.matchMedia(COMPACT_WINDOW).matches,\n () => false,\n );\n const asSheet = presentation === 'sheet' || (presentation === 'auto' && isCompact);\n // The sheet's code loads on first open, or ahead of it when the field is hovered or focused.\n usePreloadBottomSheet(containerRef, asSheet);\n // A field that takes `loading` (it loads options) preloads the indicator on hover or focus.\n usePreloadLoadingIndicator(containerRef, loading !== undefined);\n // `useSelect`'s trigger props are React Aria button props; `useButton` makes them DOM props.\n const { buttonProps } = useButton(\n // A searchable field opens a dialog holding the search and the list (as PhoneField's).\n searchable\n ? {\n ...triggerProps,\n 'aria-haspopup': 'dialog',\n 'aria-controls': state.isOpen && !asSheet ? dialogId : undefined,\n }\n : triggerProps,\n triggerRef,\n );\n const { focusProps, isFocusVisible, isFocused } = useFocusRing();\n // The trigger sets its own widget attributes; an app's description ones join them.\n const fieldButtonProps = mergeProps(pickDescriptionAria(rest), buttonProps, focusProps);\n const { hoverProps, isHovered } = useHover({ isDisabled: disabled });\n\n // In the list's order, not the order they were picked, so the field reads like the list.\n const chosenOptions: SelectedOption<T>[] = [...state.collection.getKeys()]\n .filter((key) => state.selectionManager.isSelected(key))\n .map((key) => {\n const item = state.collection.getItem(key);\n return { key, textValue: item?.textValue ?? '', value: item?.value ?? null };\n });\n const chosen = chosenOptions.map((option) => option.textValue);\n const valueText =\n chosen.length > 2\n ? `${chosen.slice(0, 2).join(', ')} ${labels.more(chosen.length - 2)}`\n : chosen.join(', ');\n const hasValue = chosen.length > 0;\n const hasLabel = label !== undefined && label !== null;\n const focused = isFocused || state.isOpen;\n const resolvedError =\n typeof errorMessage === 'function'\n ? errorMessage({ isInvalid, validationErrors, validationDetails })\n : (errorMessage ?? validationErrors.join(' '));\n\n const searchLabelling = hasLabel\n ? { 'aria-labelledby': labelProps.id }\n : { 'aria-label': (rest as { 'aria-label'?: string })['aria-label'] };\n const list = searchable ? (\n <SearchList\n listProps={{\n ...(stately.items !== undefined && { items: stately.items }),\n children: stately.children,\n ...(stately.disabledKeys !== undefined && { disabledKeys: stately.disabledKeys }),\n selectionMode: stately.selectionMode ?? 'single',\n // A single choice stays chosen when pressed again, as in a non-searchable Select.\n disallowEmptySelection: stately.selectionMode !== 'multiple',\n selectedKeys: state.selectionManager.selectedKeys,\n onSelectionChange: (keys) => {\n state.selectionManager.setSelectedKeys(keys === 'all' ? [] : keys);\n if (stately.selectionMode !== 'multiple') state.close();\n },\n }}\n search={search}\n onSearchChange={setSearch}\n filter={onSearchChange ? null : filter}\n autoFocusSearch={!asSheet}\n labelling={searchLabelling}\n dialogProps={asSheet ? undefined : { id: dialogId, role: 'dialog', ...searchLabelling }}\n labels={{ search: labels.search, noResults: labels.noResults, loading: labels.loading }}\n loading={loading ?? false}\n isBlocked={cap.isBlocked}\n embedded={asSheet}\n classNames={classNames}\n />\n ) : (\n <OptionList\n embedded={asSheet}\n listBoxProps={menuProps}\n state={state}\n listBoxRef={listBoxRef}\n emptyLabel={labels.empty}\n loading={loading}\n loadingLabel={labels.loading}\n isBlocked={cap.isBlocked}\n classNames={classNames}\n />\n );\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={hasValue || focused}\n open={state.isOpen}\n leadingIcon={leadingIcon}\n trailing={<DropdownArrow open={state.isOpen} />}\n errorIcon={errorIcon}\n errorIconLabel={labels.error}\n readOnly={readOnly}\n supportingText={supportingText}\n descriptionProps={descriptionProps}\n errorText={resolvedError}\n errorMessageProps={errorMessageProps}\n showError={isInvalid && Boolean(resolvedError)}\n containerRef={containerRef}\n // No handler is shared: the hover ones and the touch fallback's capture ones.\n containerProps={{ ...hoverProps, ...strayTouchProps }}\n onContainerPointerDown={(event) => {\n // A press anywhere on the field (not only its text) opens it, like Compose.\n if (disabled || triggerRef.current?.contains(event.target as Node)) return;\n event.preventDefault();\n releaseFocusRef.current = false;\n triggerRef.current?.focus();\n if (!readOnly) state.toggle();\n }}\n rootProps={{ ...rootData, 'data-focus-visible': isFocusVisible || undefined }}\n rootRef={ref}\n className={className}\n classNames={classNames}\n style={style}\n >\n {/* The native select is for forms: submitting under `name`, autofill and native\n validation. Without them it's left out, so a long list's text (e.g. country names\n from Intl, which can differ between server and browser) isn't rendered twice. */}\n {name || autoComplete || form || stately.validationBehavior === 'native' ? (\n <HiddenSelect\n state={state}\n triggerRef={triggerRef}\n label={label}\n name={name}\n isDisabled={disabled}\n {...(autoComplete && { autoComplete })}\n {...(form && { form })}\n />\n ) : null}\n <button\n {...fieldButtonProps}\n aria-describedby={renderedDescription(fieldButtonProps['aria-describedby'], [\n !supportingText && descriptionProps.id,\n !(isInvalid && resolvedError) && errorMessageProps.id,\n ])}\n onFocus={(event) => {\n fieldButtonProps.onFocus?.(event);\n if (!releaseFocusRef.current) return;\n releaseFocusRef.current = false;\n triggerRef.current?.blur();\n }}\n // Using the field again means it's wanted, whatever came before.\n onPointerDown={(event) => {\n releaseFocusRef.current = false;\n fieldButtonProps.onPointerDown?.(event);\n }}\n onKeyDown={(event) => {\n releaseFocusRef.current = false;\n fieldButtonProps.onKeyDown?.(event);\n }}\n ref={triggerRef}\n type=\"button\"\n className={cn(\n 'flex min-w-0 flex-1 cursor-pointer items-center self-stretch bg-transparent p-0 text-start outline-none',\n 'disabled:cursor-default',\n )}\n >\n <span\n {...valueProps}\n className={cn(\n 'min-w-0 flex-1 truncate text-body-large',\n hasValue\n ? 'text-[color:var(--_md-comp-text-field-input-text-color)]'\n : 'text-on-surface-variant',\n // With a label, the placeholder shows once the label floats (as TextField).\n !hasValue && hasLabel && 'opacity-0 group-data-floated/field:opacity-100',\n 'group-data-[field-state=disabled]/field:text-on-surface/38',\n classNames?.value,\n )}\n >\n {hasValue && renderValue ? (\n <>\n <span aria-hidden=\"true\">{renderValue(chosenOptions)}</span>\n <span className=\"sr-only\">{valueText}</span>\n </>\n ) : hasValue ? (\n valueText\n ) : (\n (placeholder ?? ' ')\n )}\n </span>\n </button>\n </FieldShell>\n {asSheet ? (\n <LazyBottomSheet\n // Opens at its own height (up to the window, the list scrolling inside), never half\n // the window, which left the last options off-screen on a short phone.\n skipPartiallyExpanded\n open={state.isOpen}\n onOpenChange={(open) => (open ? state.open() : state.close())}\n classNames={\n searchable\n ? {\n panel: cn(SEARCH_SHEET_HEIGHT, classNames?.sheet),\n content: SEARCH_SHEET_CONTENT,\n }\n : classNames?.sheet\n ? { panel: classNames.sheet }\n : undefined\n }\n {...(typeof label === 'string'\n ? { 'aria-label': label }\n : { 'aria-label': (rest as { 'aria-label'?: string })['aria-label'] ?? '' })}\n >\n <div\n {...densityAttribute(density)}\n className=\"flex min-h-0 flex-1 flex-col px-[8px] pb-[16px]\"\n >\n {list}\n </div>\n </LazyBottomSheet>\n ) : (\n <OptionPopover\n state={state}\n triggerRef={containerRef}\n popoverRef={popoverRef}\n density={density}\n onPressOutside={() => {\n releaseFocusRef.current = true;\n }}\n // Room to type in a searchable menu, however narrow the field.\n minWidth={searchable ? 280 : 0}\n >\n {list}\n </OptionPopover>\n )}\n </>\n );\n}\n\nexport type { Key as SelectKey } from 'react-stately';\n"],"mappings":"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"}
@@ -115,7 +115,10 @@ interface BottomSheetOwnProps {
115
115
  closeIcon?: ReactElement;
116
116
  /** Replaces the collapse button's icon. */
117
117
  collapseIcon?: ReactElement;
118
- /** Words the sheet announces, with English defaults. */
118
+ /**
119
+ * Words the sheet announces, with English defaults. A {@link LabelsProvider} sets them for the
120
+ * whole app.
121
+ */
119
122
  labels?: Partial<BottomSheetLabels>;
120
123
  children?: ReactNode | ((props: {
121
124
  close: () => void;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { useThemeScope } from "../../theme/context.js";
3
3
  import { cn } from "../../utils/cn.js";
4
+ import { useLabels } from "../labels/use-labels.js";
4
5
  import { TriggerContext } from "../../primitives/TriggerContext.js";
5
6
  import { mergeDataTokens } from "../../utils/merge-data-tokens.js";
6
7
  import { usePresence } from "../../primitives/use-presence.js";
@@ -101,10 +102,7 @@ function ModalBottomSheet({ state, isExiting, triggerOverlayProps, closedBy = "a
101
102
  }
102
103
  /** The sheet itself: the moving wrapper, the panel, its handle and its detents. */
103
104
  function BottomSheetPanel({ variant, state, isExiting, exitProps, panelProps: overlayPanelProps, dragHandle = true, swipeable = true, skipPartiallyExpanded = false, peekHeight = 56, detent: detentProp, defaultDetent = "collapsed", onDetentChange, closedBy = "any", closeButton = true, closeIcon, collapseIcon, labels: labelsProp, children, className, classNames, style, ref, ...rest }) {
104
- const labels = {
105
- ...DEFAULT_LABELS,
106
- ...labelsProp
107
- };
105
+ const labels = useLabels("BottomSheet", DEFAULT_LABELS, labelsProp);
108
106
  const standard = variant === "standard";
109
107
  const panelRef = useObjectRef(ref);
110
108
  const motionRef = useRef(null);