@windmorph/react 0.2.0 → 0.2.2

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 (60) hide show
  1. package/dist/components/carousel/carousel-styles.d.ts +6 -15
  2. package/dist/components/carousel/carousel-styles.js +4 -7
  3. package/dist/components/carousel/carousel-styles.js.map +1 -1
  4. package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
  5. package/dist/components/chip/chip-tokens.d.ts +4 -4
  6. package/dist/components/date-picker/DatePicker.js +11 -4
  7. package/dist/components/date-picker/DatePicker.js.map +1 -1
  8. package/dist/components/date-picker/date-picker-styles.d.ts +6 -6
  9. package/dist/components/date-picker/date-picker-tokens.d.ts +1 -1
  10. package/dist/components/dialog/Dialog.d.ts +4 -2
  11. package/dist/components/dialog/Dialog.js +2 -2
  12. package/dist/components/dialog/Dialog.js.map +1 -1
  13. package/dist/components/dialog/dialog-tokens.d.ts +1 -1
  14. package/dist/components/icon-button/IconButton.d.ts +3 -0
  15. package/dist/components/icon-button/IconButton.js +3 -0
  16. package/dist/components/icon-button/IconButton.js.map +1 -1
  17. package/dist/components/list/List.d.ts +9 -0
  18. package/dist/components/list/List.js +7 -4
  19. package/dist/components/list/List.js.map +1 -1
  20. package/dist/components/list/list-styles.js +6 -6
  21. package/dist/components/list/list-styles.js.map +1 -1
  22. package/dist/components/list/list-tokens.d.ts +1 -1
  23. package/dist/components/list/list-tokens.js +50 -0
  24. package/dist/components/list/list-tokens.js.map +1 -1
  25. package/dist/components/menu/menu-tokens.d.ts +1 -1
  26. package/dist/components/navigation/NavigationRail.js +52 -15
  27. package/dist/components/navigation/NavigationRail.js.map +1 -1
  28. package/dist/components/navigation/nav-item-styles.js +4 -4
  29. package/dist/components/navigation/nav-item-styles.js.map +1 -1
  30. package/dist/components/search/SearchBar.js +19 -34
  31. package/dist/components/search/SearchBar.js.map +1 -1
  32. package/dist/components/search/SearchField.d.ts +111 -0
  33. package/dist/components/search/SearchField.js +128 -0
  34. package/dist/components/search/SearchField.js.map +1 -0
  35. package/dist/components/search/search-field-frame.js +36 -0
  36. package/dist/components/search/search-field-frame.js.map +1 -0
  37. package/dist/components/search/search-styles.d.ts +58 -0
  38. package/dist/components/search/search-styles.js +27 -2
  39. package/dist/components/search/search-styles.js.map +1 -1
  40. package/dist/components/select/search-list.js +8 -13
  41. package/dist/components/select/search-list.js.map +1 -1
  42. package/dist/components/sheet/SideSheet.d.ts +2 -0
  43. package/dist/components/sheet/SideSheet.js +1 -1
  44. package/dist/components/sheet/SideSheet.js.map +1 -1
  45. package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
  46. package/dist/components/text-field/TextField.js +19 -5
  47. package/dist/components/text-field/TextField.js.map +1 -1
  48. package/dist/components/toolbar/toolbar-styles.js +2 -2
  49. package/dist/components/toolbar/toolbar-styles.js.map +1 -1
  50. package/dist/components/tooltip/tooltip-tokens.d.ts +1 -1
  51. package/dist/index.d.ts +3 -2
  52. package/dist/index.js +3 -2
  53. package/dist/primitives/ButtonBase.js +1 -0
  54. package/dist/primitives/ButtonBase.js.map +1 -1
  55. package/dist/styles/generated/components.css +5 -0
  56. package/dist/styles/utilities.css +8 -0
  57. package/dist/styles.compiled.css +1 -1
  58. package/dist/theme/ThemeProvider.js +17 -0
  59. package/dist/theme/ThemeProvider.js.map +1 -1
  60. package/package.json +1 -1
@@ -6,10 +6,11 @@ import { Overlay as Overlay$1 } from "../../primitives/Overlay.js";
6
6
  import { pickDescriptionAria, splitRootProps } from "../../utils/root-props.js";
7
7
  import { COMPACT_WINDOW, useMediaQuery } from "../date-picker/use-media-query.js";
8
8
  import { searchBarStyles } from "./search-styles.js";
9
+ import { SearchFieldFrame } from "./search-field-frame.js";
9
10
  import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
10
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
12
  import { useReducedMotion } from "motion/react";
12
- import { mergeProps, mergeRefs, useDialog, useFocusRing, useModalOverlay, useObjectRef } from "react-aria";
13
+ import { mergeProps, mergeRefs, useDialog, useModalOverlay, useObjectRef } from "react-aria";
13
14
  import { useOverlayTriggerState } from "react-stately";
14
15
  import { useControlledState } from "react-stately/useControlledState";
15
16
  //#region src/components/search/SearchBar.tsx
@@ -93,19 +94,23 @@ function SearchBar(props) {
93
94
  const { "aria-describedby": describedBy, "aria-details": details } = own;
94
95
  const describedByInput = [describedBy, consumerInputProps?.["aria-describedby"]].filter(Boolean).join(" ") || void 0;
95
96
  const collapsedTone = appBar ? appBar.scrolled ? "scrolled" : "appBar" : "default";
96
- const field = (input, tone) => /* @__PURE__ */ jsx(SearchField, {
97
- className: searchBarStyles({ tone }).field({ class: classNames?.field }),
98
- leading: leading != null && /* @__PURE__ */ jsx("span", {
99
- "data-slot": "leading",
100
- className: styles.leading({ class: classNames?.leading }),
101
- children: leading
102
- }),
103
- trailing: trailing != null && /* @__PURE__ */ jsx("span", {
104
- "data-slot": "trailing",
105
- className: styles.trailing({ class: classNames?.trailing }),
106
- children: trailing
107
- }),
108
- children: input
97
+ const field = (input, tone) => /* @__PURE__ */ jsx(ButtonContext, {
98
+ value: standardIconButtons,
99
+ children: /* @__PURE__ */ jsx(SearchFieldFrame, {
100
+ tone,
101
+ className: classNames?.field,
102
+ leading: leading != null && /* @__PURE__ */ jsx("span", {
103
+ "data-slot": "leading",
104
+ className: styles.leading({ class: classNames?.leading }),
105
+ children: leading
106
+ }),
107
+ trailing: trailing != null && /* @__PURE__ */ jsx("span", {
108
+ "data-slot": "trailing",
109
+ className: styles.trailing({ class: classNames?.trailing }),
110
+ children: trailing
111
+ }),
112
+ children: input
113
+ })
109
114
  });
110
115
  const sharedInputProps = (handlers) => ({
111
116
  ...mergeProps({
@@ -209,26 +214,6 @@ function SearchBar(props) {
209
214
  })]
210
215
  });
211
216
  }
212
- function SearchField({ className, leading, trailing, children }) {
213
- const { isFocusVisible, focusProps } = useFocusRing({
214
- within: true,
215
- isTextInput: true
216
- });
217
- return /* @__PURE__ */ jsx("div", {
218
- ...focusProps,
219
- "data-tokens": "md-comp-search-bar",
220
- "data-focus-visible": isFocusVisible || void 0,
221
- className,
222
- children: /* @__PURE__ */ jsxs(ButtonContext, {
223
- value: standardIconButtons,
224
- children: [
225
- leading,
226
- children,
227
- trailing
228
- ]
229
- })
230
- });
231
- }
232
217
  function ExpandedInput({ inputRef, open, ...props }) {
233
218
  const ref = useObjectRef(inputRef);
234
219
  useEffect(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"SearchBar.js","names":["Overlay"],"sources":["../../../src/components/search/SearchBar.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type ChangeEvent,\n type ComponentProps,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n} from 'react';\nimport { useReducedMotion } from 'motion/react';\nimport {\n mergeProps,\n mergeRefs,\n useDialog,\n useFocusRing,\n useModalOverlay,\n useObjectRef,\n} from 'react-aria';\nimport { useOverlayTriggerState, type OverlayTriggerState } from 'react-stately';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { Overlay } from '../../primitives/Overlay';\nimport { usePresence } from '../../primitives/use-presence';\nimport { cn } from '../../utils/cn';\nimport {\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { ButtonContext, standardIconButtons } from '../button/ButtonContext';\nimport { COMPACT_WINDOW, useMediaQuery } from '../date-picker/use-media-query';\nimport { searchBarStyles } from './search-styles';\n\n/** Set by `SearchAppBar` for the search bar inside it. */\nexport const SearchAppBarContext = createContext<{ scrolled: boolean } | null>(null);\n\n/** What a search bar's icons can depend on. */\nexport interface SearchBarIconState {\n expanded: boolean;\n /** Collapses the search view, e.g. from a back button. */\n collapse: () => void;\n /**\n * The view in effect (`auto` resolved for the window), so a back button can show in the\n * full-screen view only.\n */\n view: 'docked' | 'full-screen';\n}\n\ntype SearchBarIcon = ReactNode | ((state: SearchBarIconState) => ReactNode);\n\nexport interface SearchBarClassNames {\n /** The outer container, merged with `className`. */\n root?: string;\n /** The pill around the icons and the input (collapsed and expanded). */\n field?: string;\n /** The text input (collapsed and expanded). */\n input?: string;\n /** The leading icon slot. */\n leading?: string;\n /** The trailing icon slot. */\n trailing?: string;\n /** The scrim behind the docked view. */\n scrim?: string;\n /** The expanded view: the docked panel or the full-screen surface. */\n view?: string;\n /** The expanded content: the docked dropdown or the full-screen content area. */\n content?: string;\n}\n\ntype Naming =\n | {\n /** Accessible name for the search bar and its input. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the search bar and its input. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\n/**\n * Attributes for the search inputs, e.g. `aria-keyshortcuts`, `aria-describedby`,\n * `enterKeyHint`, `autoComplete`, `spellCheck`, `inputMode` or event handlers. The bar owns\n * its value, name, role and popup state, so those keys are left out; an `id` or `name`\n * would be duplicated, since the bar renders a second input while expanded.\n */\nexport type SearchBarInputProps = Omit<\n ComponentProps<'input'>,\n | 'ref'\n | 'id'\n | 'name'\n | 'type'\n | 'role'\n | 'value'\n | 'defaultValue'\n | 'onChange'\n | 'placeholder'\n | 'className'\n | 'children'\n | 'dangerouslySetInnerHTML'\n | 'inert'\n | 'aria-label'\n | 'aria-labelledby'\n | 'aria-expanded'\n | 'aria-haspopup'\n | 'aria-controls'\n | 'aria-hidden'\n> & {\n [data: `data-${string}`]: string | number | boolean | undefined;\n};\n\ninterface SearchBarOwnProps extends WrapperRootDOMProps {\n /** The root element's id. */\n id?: string;\n /**\n * The ids of elements that describe the search, for both inputs (the bar's and the open\n * view's), before any `inputProps['aria-describedby']`.\n */\n 'aria-describedby'?: string;\n /** The id of an element with more details about the search, for both inputs. */\n 'aria-details'?: string;\n /** Controlled query. */\n value?: string;\n /** Initial query when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the query as it changes. */\n onChange?: (value: string) => void;\n /**\n * Called with the query when the user presses Enter (the keyboard's search action) in the\n * open search view. Enter in the collapsed bar opens the view.\n */\n onSubmit?: (value: string) => void;\n /** Controlled expansion of the search view. */\n expanded?: boolean;\n /** Whether the search view starts expanded (uncontrolled). @default false */\n defaultExpanded?: boolean;\n /** Called when the search view expands or collapses. */\n onExpandedChange?: (expanded: boolean) => void;\n /**\n * How the bar expands: `docked`, a dropdown below it over a scrim; `full-screen`, a view\n * that fills the window; `auto`, full screen on compact windows (below 600px wide) and\n * docked on larger ones, as M3 recommends (\"Full-screen layout: Default for compact\n * breakpoints\"; docked is \"Best for medium and expanded windows\"). It follows the window\n * as it resizes.\n * @default \"auto\"\n */\n view?: SearchBarView;\n /** Hint text shown in the empty input, e.g. \"Search mail\". */\n placeholder?: string;\n /** Leading icon, e.g. a search icon, or a back button while expanded. */\n leadingIcon?: SearchBarIcon;\n /**\n * Trailing content: one or two icon buttons (e.g. voice search and a clear button), or an\n * avatar (an `img`, drawn at 30px) with up to one icon button (M3).\n */\n trailingIcon?: SearchBarIcon;\n /**\n * Centre the placeholder and the query while the bar is collapsed (M3's search app bar\n * supports centred text). @default \"start\"\n */\n textAlign?: 'start' | 'center';\n /**\n * What screen readers hear when the suggestions or results change while the view is open\n * (M3: \"the screen reader must announce the change\"), e.g. \"5 results\". Announced\n * politely whenever it changes.\n */\n resultsAnnouncement?: string;\n /**\n * The expanded content: suggestions or results, usually a list. ↓ from the input moves\n * focus into it.\n */\n children?: ReactNode;\n /** The input element (collapsed, or expanded while the view is open). */\n inputRef?: Ref<HTMLInputElement>;\n /**\n * Attributes for both inputs, the collapsed one and the expanded view's. They replace the\n * bar's defaults (`enterKeyHint=\"search\"`, `autoComplete=\"off\"`, `spellCheck={false}`);\n * event handlers run before the bar's own, and the bar's role, name, value and popup state\n * always win.\n */\n inputProps?: SearchBarInputProps;\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?: SearchBarClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SearchBarProps = SearchBarOwnProps & Naming;\n\n/** How a {@link SearchBar} expands. */\nexport type SearchBarView = 'auto' | 'docked' | 'full-screen';\n\ninterface BarRect {\n top: number;\n left: number;\n right: number;\n bottom: number;\n width: number;\n height: number;\n rtl: boolean;\n}\n\n/** How far the docked field widens on each side when focused (24dp → 12dp margins). */\nconst FOCUSED_GROWTH = 12;\n/** The search bar's maximum width (M3). */\nconst MAX_WIDTH = 720;\n/** The focused search bar's height (M3: \"Search bar container Height 56dp\"). */\nconst FIELD_HEIGHT = 56;\n\nconst FOCUSABLE =\n 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * M3 Expressive search bar: a 56px pill that expands into a search view, either a dropdown\n * below the bar (docked) or a full-screen view, holding suggestions or results. Pressing\n * the bar, typing into it or ↓ expands it; Escape, a press outside or `collapse()` closes it.\n *\n * @example\n * <SearchBar aria-label=\"Search mail\" placeholder=\"Search mail\" leadingIcon={<SearchIcon />}\n * value={query} onChange={setQuery} onSubmit={search}>\n * <SuggestionList … />\n * </SearchBar>\n */\nexport function SearchBar(props: SearchBarProps) {\n const {\n value,\n defaultValue,\n onChange,\n onSubmit,\n expanded,\n defaultExpanded,\n onExpandedChange,\n view: requestedView = 'auto',\n placeholder,\n leadingIcon,\n trailingIcon,\n textAlign = 'start',\n resultsAnnouncement,\n children,\n inputRef,\n inputProps: consumerInputProps,\n ref,\n className,\n style,\n classNames,\n ...rest\n } = props;\n const { 'aria-label': label, 'aria-labelledby': labelledBy } = props as {\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n };\n // The server and the first client render see a docked view, so hydration matches.\n const compact = useMediaQuery(COMPACT_WINDOW);\n const view = requestedView === 'auto' ? (compact ? 'full-screen' : 'docked') : requestedView;\n const [query, setQuery] = useControlledState(value, defaultValue ?? '', onChange);\n const state = useOverlayTriggerState({\n isOpen: expanded,\n defaultOpen: defaultExpanded,\n onOpenChange: onExpandedChange,\n });\n const appBar = useContext(SearchAppBarContext);\n const rootRef = useObjectRef(ref);\n const [rect, setRect] = useState<BarRect | null>(null);\n // The views grow from the bar as `--m3-search-progress` runs; under reduced motion they\n // only fade (the scrim, or the full-screen view).\n const reduceMotion = useReducedMotion();\n const { isPresent, isExiting, exitProps } = usePresence(\n state.isOpen,\n reduceMotion ? 'opacity' : '--m3-search-progress',\n );\n\n // As the view starts leaving, focus goes back at once to what had it when the view opened\n // (WAI-ARIA: a closing dialog returns focus to what opened it), not when the view unmounts\n // ~450ms later: typing meanwhile reaches the bar, which opens the view again (2026-10-17).\n // That's the bar's input when the bar opened it, and whatever had focus when an app's\n // shortcut did (a link stays focused after ⌘K and Escape); the bar's input otherwise.\n const collapsedRef = useRef<HTMLInputElement>(null);\n const collapsedInputRef = useMemo(\n () => mergeRefs(collapsedRef, isPresent ? undefined : inputRef),\n [isPresent, inputRef],\n );\n // Read before the view's input takes focus (a passive effect) and before React Aria's\n // focus scope would restore it on unmount, which this hand-over replaces.\n const restoreRef = useRef<HTMLElement | null>(null);\n useLayoutEffect(() => {\n const doc = collapsedRef.current?.ownerDocument;\n if (!state.isOpen || !doc) return;\n const active = doc.activeElement;\n restoreRef.current = active instanceof HTMLElement && active !== doc.body ? active : null;\n }, [state.isOpen]);\n useEffect(() => {\n const collapsed = collapsedRef.current;\n if (!isExiting || !collapsed) return;\n // Only from inside the leaving view: a press outside that moved focus elsewhere keeps it.\n if (!collapsed.ownerDocument.activeElement?.closest('[data-exiting]')) return;\n const previous = restoreRef.current;\n const target =\n previous?.isConnected && !previous.closest('[data-exiting]') ? previous : collapsed;\n target.focus({ preventScroll: true });\n }, [isExiting]);\n\n // The expanded view starts from the bar's bounds; measure them when it opens.\n useLayoutEffect(() => {\n const root = rootRef.current;\n if (!state.isOpen || !root) return;\n const box = root.getBoundingClientRect();\n setRect({\n top: box.top,\n left: box.left,\n right: box.right,\n bottom: box.bottom,\n width: box.width,\n height: box.height,\n rtl: getComputedStyle(root).direction === 'rtl',\n });\n }, [state.isOpen, rootRef]);\n\n const iconState: SearchBarIconState = { expanded: state.isOpen, collapse: state.close, view };\n const leading = typeof leadingIcon === 'function' ? leadingIcon(iconState) : leadingIcon;\n const trailing = typeof trailingIcon === 'function' ? trailingIcon(iconState) : trailingIcon;\n const styles = searchBarStyles({\n inAppBar: appBar != null,\n hasLeading: leading != null,\n hasTrailing: trailing != null,\n });\n const { root: data, rest: own } = splitRootProps(rest, { wrapper: true });\n // The naming attributes describe the inputs, as `aria-label` names them.\n const { 'aria-describedby': describedBy, 'aria-details': details } = own;\n const describedByInput =\n [describedBy, consumerInputProps?.['aria-describedby']].filter(Boolean).join(' ') || undefined;\n // In an app bar the collapsed field is `surface-container`; the expanded view's field\n // keeps the search view's own `surface-container-high`.\n const collapsedTone = appBar ? (appBar.scrolled ? 'scrolled' : 'appBar') : 'default';\n\n const field = (input: ReactNode, tone: 'default' | 'appBar' | 'scrolled') => (\n <SearchField\n className={searchBarStyles({ tone }).field({ class: classNames?.field })}\n leading={\n leading != null && (\n <span data-slot=\"leading\" className={styles.leading({ class: classNames?.leading })}>\n {leading}\n </span>\n )\n }\n trailing={\n trailing != null && (\n <span data-slot=\"trailing\" className={styles.trailing({ class: classNames?.trailing })}>\n {trailing}\n </span>\n )\n }\n >\n {input}\n </SearchField>\n );\n\n // The consumer's attributes replace the defaults; the bar's own keys always win, and\n // `mergeProps` chains event handlers (the consumer's first).\n const sharedInputProps = (handlers: ComponentProps<'input'>): ComponentProps<'input'> => ({\n ...mergeProps(\n {\n enterKeyHint: 'search',\n autoComplete: 'off',\n spellCheck: false,\n 'aria-details': details,\n ...pickDescriptionAria(own),\n },\n consumerInputProps,\n { 'aria-describedby': describedByInput },\n handlers,\n ),\n id: undefined,\n name: undefined,\n role: undefined,\n type: 'search',\n value: query,\n placeholder,\n 'aria-label': label,\n 'aria-labelledby': label == null ? labelledBy : undefined,\n });\n const inputClass = (align: 'start' | 'center') =>\n searchBarStyles({\n hasLeading: leading != null,\n hasTrailing: trailing != null,\n textAlign: align,\n }).input({ class: classNames?.input });\n\n const submitOnEnter = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Enter' && !event.nativeEvent.isComposing) {\n event.preventDefault();\n onSubmit?.(query);\n return true;\n }\n return false;\n };\n\n return (\n <div\n {...data}\n id={(own as { id?: string }).id}\n ref={rootRef}\n style={style}\n data-expanded={state.isOpen || undefined}\n data-view={view}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n {field(\n <input\n {...sharedInputProps({\n onChange: (event: ChangeEvent<HTMLInputElement>) => {\n const next = event.target.value;\n // Compose expands the bar as the user types into it.\n if (next.length > query.length) state.open();\n setQuery(next);\n },\n onClick: state.open,\n // Enter and ↓ open the search view, as a click does (Compose expands the bar\n // as its field takes focus); the query is submitted from the open view.\n onKeyDown: (event) => {\n const enter = event.key === 'Enter' && !event.nativeEvent.isComposing;\n if (enter || event.key === 'ArrowDown') {\n event.preventDefault();\n state.open();\n }\n },\n })}\n data-slot=\"input\"\n className={inputClass(textAlign)}\n // The consumer's `inputRef` follows whichever input is live.\n ref={collapsedInputRef}\n // Live again as soon as the view starts leaving: a press or typing on the bar\n // while the view leaves (its parts take no presses) opens it again from where it\n // is, instead of being swallowed for the ~450ms of the exit (2026-10-17).\n inert={state.isOpen}\n aria-hidden={state.isOpen || undefined}\n // A combobox whose popup is the search view's dialog (ARIA 1.2).\n role=\"combobox\"\n aria-expanded={state.isOpen}\n aria-haspopup=\"dialog\"\n />,\n collapsedTone,\n )}\n {isPresent && rect && (\n <Overlay exiting={isExiting}>\n <SearchView\n view={view}\n state={state}\n rect={rect}\n animateHeight={!reduceMotion}\n isExiting={isExiting}\n exitProps={exitProps}\n label={label}\n labelledBy={labelledBy}\n classNames={classNames}\n styles={styles}\n resultsAnnouncement={resultsAnnouncement}\n field={(contentRef) =>\n field(\n <ExpandedInput\n {...sharedInputProps({\n onChange: (event) => setQuery(event.target.value),\n onKeyDown: (event) => {\n // M3: once results are queried, the query stays visible but not\n // focused; focus moves to the results.\n if (submitOnEnter(event)) {\n contentRef.current?.focus();\n return;\n }\n if (event.key === 'ArrowDown') {\n const first = contentRef.current?.querySelector<HTMLElement>(FOCUSABLE);\n if (first) {\n event.preventDefault();\n first.focus();\n }\n }\n },\n })}\n data-slot=\"input\"\n className={inputClass('start')}\n inputRef={inputRef}\n open={state.isOpen}\n />,\n 'default',\n )\n }\n >\n {children}\n </SearchView>\n </Overlay>\n )}\n </div>\n );\n}\n\nfunction SearchField({\n className,\n leading,\n trailing,\n children,\n}: {\n className: string;\n leading: ReactNode;\n trailing: ReactNode;\n children: ReactNode;\n}) {\n // The pill shows the inset focus ring while its input has keyboard focus.\n const { isFocusVisible, focusProps } = useFocusRing({ within: true, isTextInput: true });\n return (\n <div\n {...focusProps}\n data-tokens=\"md-comp-search-bar\"\n data-focus-visible={isFocusVisible || undefined}\n className={className}\n >\n {/* Icon buttons in a search bar are standard (M3), not the filled default. */}\n <ButtonContext value={standardIconButtons}>\n {leading}\n {children}\n {trailing}\n </ButtonContext>\n </div>\n );\n}\n\nfunction ExpandedInput({\n inputRef,\n open,\n ...props\n}: Omit<ComponentProps<'input'>, 'ref'> & {\n inputRef?: Ref<HTMLInputElement>;\n /** The view is open (not leaving): reopened while leaving, the input takes focus again. */\n open: boolean;\n}) {\n const ref = useObjectRef(inputRef);\n // Take focus when the view opens, with the caret after the query.\n useEffect(() => {\n const input = ref.current;\n if (!input || !open) return;\n input.focus({ preventScroll: true });\n input.setSelectionRange(input.value.length, input.value.length);\n }, [ref, open]);\n return <input {...props} ref={ref} />;\n}\n\nfunction SearchView({\n view,\n state,\n rect,\n animateHeight,\n isExiting,\n exitProps,\n label,\n labelledBy,\n classNames,\n styles,\n field,\n resultsAnnouncement,\n children,\n}: {\n view: 'docked' | 'full-screen';\n state: OverlayTriggerState;\n rect: BarRect;\n /** Whether the dropdown's height follows its content on a spring (not under reduced motion). */\n animateHeight: boolean;\n isExiting: boolean;\n exitProps: ReturnType<typeof usePresence>['exitProps'];\n label?: string;\n labelledBy?: string;\n classNames?: SearchBarClassNames;\n styles: ReturnType<typeof searchBarStyles>;\n field: (contentRef: RefObject<HTMLDivElement | null>) => ReactNode;\n resultsAnnouncement?: string;\n children: ReactNode;\n}) {\n const panelRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const contentSizeRef = useRef<HTMLDivElement>(null);\n useContentHeight(\n contentRef,\n contentSizeRef,\n view === 'docked' && animateHeight && children != null,\n );\n const { modalProps, underlayProps } = useModalOverlay(\n { isDismissable: !isExiting },\n state,\n panelRef,\n );\n const { dialogProps } = useDialog(\n { 'aria-label': label, 'aria-labelledby': label == null ? labelledBy : undefined },\n panelRef,\n );\n const exiting = isExiting || undefined;\n const panelProps = mergeProps(modalProps, dialogProps, { tabIndex: -1 });\n\n if (view === 'full-screen') {\n const vars = {\n '--m3-search-top': `${rect.top}px`,\n '--m3-search-left': `${rect.left}px`,\n '--m3-search-right': `${window.innerWidth - rect.right}px`,\n '--m3-search-bottom': `${window.innerHeight - rect.bottom}px`,\n '--m3-search-start': `${rect.rtl ? window.innerWidth - rect.right : rect.left}px`,\n '--m3-search-width': `${rect.width}px`,\n '--m3-search-height': `${rect.height}px`,\n } as CSSProperties;\n return (\n <div\n {...panelProps}\n {...exitProps}\n ref={panelRef}\n style={vars}\n data-tokens=\"md-comp-search-view\"\n data-view=\"full-screen\"\n data-exiting={exiting}\n className={styles.fullScreenPanel({ class: classNames?.view })}\n >\n <div data-exiting={exiting} className={styles.fullScreenHeader()}>\n {field(contentRef)}\n </div>\n <div\n ref={contentRef}\n tabIndex={-1}\n data-exiting={exiting}\n className={styles.fullScreenContent({ class: classNames?.content })}\n >\n {children}\n </div>\n <LiveStatus message={resultsAnnouncement} />\n </div>\n );\n }\n // The docked field widens by 12px on each side (M3 Expressive: margins 24dp → 12dp),\n // staying within the window and, unless the bar is wider already, 720px.\n const width = Math.min(rect.width + 2 * FOCUSED_GROWTH, Math.max(rect.width, MAX_WIDTH));\n const grow = (width - rect.width) / 2;\n const left = Math.max(0, Math.min(rect.left - grow, window.innerWidth - width));\n // The focused field is 56px whatever the collapsed bar's height (a consumer `h-*`),\n // centred on the bar and kept within the window.\n const top = Math.max(0, rect.top + (rect.height - FIELD_HEIGHT) / 2);\n // The panel morphs from the bar's bounds by these deltas (`dockedPanel`).\n const dockedVars = {\n '--m3-search-top': `${rect.top}px`,\n '--m3-search-left': `${rect.left}px`,\n '--m3-search-width': `${rect.width}px`,\n '--m3-search-height': `${rect.height}px`,\n '--m3-search-dx': `${left - rect.left}px`,\n '--m3-search-dy': `${top - rect.top}px`,\n '--m3-search-dw': `${Math.min(width, window.innerWidth) - rect.width}px`,\n '--m3-search-dh': `${FIELD_HEIGHT - rect.height}px`,\n } as CSSProperties;\n\n return (\n <>\n {/* Presence ends on the scrim's fade under reduced motion, else on the panel's motion. */}\n <div\n {...exitProps}\n aria-hidden=\"true\"\n data-exiting={exiting}\n className={styles.scrim({ class: classNames?.scrim })}\n />\n <div {...underlayProps} data-exiting={exiting} className={styles.underlay()}>\n <div\n {...panelProps}\n {...exitProps}\n ref={panelRef}\n style={dockedVars}\n data-exiting={exiting}\n className={styles.dockedPanel({ class: classNames?.view })}\n >\n <div className={styles.dockedHeader()}>{field(contentRef)}</div>\n {children != null && (\n <div\n ref={contentRef}\n tabIndex={-1}\n data-tokens=\"md-comp-search-view\"\n data-view=\"docked\"\n data-exiting={exiting}\n className={styles.dropdown({ class: classNames?.content })}\n >\n {/* Measured, so the dropdown's height can follow it (`useContentHeight`). */}\n <div ref={contentSizeRef} className=\"flow-root\">\n {children}\n </div>\n </div>\n )}\n <LiveStatus message={resultsAnnouncement} />\n </div>\n </div>\n </>\n );\n}\n\n/**\n * Keeps `--m3-search-content-height` on the dropdown at its content's height (plus the\n * dropdown's own padding and borders), so the dropdown's height animates as the results\n * change instead of jumping. While that height animates the dropdown doesn't scroll\n * (`data-resizing`), so a scrollbar never flashes in and rewraps the content.\n */\nfunction useContentHeight(\n dropdownRef: RefObject<HTMLDivElement | null>,\n contentRef: RefObject<HTMLDivElement | null>,\n enabled: boolean,\n) {\n useLayoutEffect(() => {\n const dropdown = dropdownRef.current;\n const content = contentRef.current;\n if (!enabled || !dropdown || !content) return;\n let measured = Number.NaN;\n const measure = () => {\n const style = getComputedStyle(dropdown);\n const frame =\n parseFloat(style.paddingTop) +\n parseFloat(style.paddingBottom) +\n parseFloat(style.borderTopWidth) +\n parseFloat(style.borderBottomWidth);\n const height = content.getBoundingClientRect().height + frame;\n // The content's box shifts by a fraction of a pixel as the panel moves; only a real\n // change of size restarts the height's spring.\n if (Math.abs(height - measured) < 0.5) return;\n measured = height;\n dropdown.style.setProperty('--m3-search-content-height', `${height}px`);\n };\n const resizing = (on: boolean) => (event: TransitionEvent) => {\n if (event.target !== dropdown || event.propertyName !== 'height') return;\n if (on) dropdown.dataset.resizing = '';\n else delete dropdown.dataset.resizing;\n };\n const start = resizing(true);\n const end = resizing(false);\n measure();\n dropdown.addEventListener('transitionrun', start);\n dropdown.addEventListener('transitionend', end);\n dropdown.addEventListener('transitioncancel', end);\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure);\n observer?.observe(content);\n return () => {\n observer?.disconnect();\n dropdown.removeEventListener('transitionrun', start);\n dropdown.removeEventListener('transitionend', end);\n dropdown.removeEventListener('transitioncancel', end);\n dropdown.style.removeProperty('--m3-search-content-height');\n delete dropdown.dataset.resizing;\n };\n }, [dropdownRef, contentRef, enabled]);\n}\n\n/**\n * A polite live region inside the search view. It starts empty and takes the message a\n * moment later, so the first one is announced too, and again whenever it changes.\n */\nfunction LiveStatus({ message }: { message?: string }) {\n const [text, setText] = useState('');\n useEffect(() => {\n const timer = setTimeout(() => setText(message ?? ''), 150);\n return () => clearTimeout(timer);\n }, [message]);\n return (\n <div role=\"status\" className=\"sr-only\">\n {text}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SearchBar.js","names":["Overlay"],"sources":["../../../src/components/search/SearchBar.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type ChangeEvent,\n type ComponentProps,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n} from 'react';\nimport { useReducedMotion } from 'motion/react';\nimport { mergeProps, mergeRefs, useDialog, useModalOverlay, useObjectRef } from 'react-aria';\nimport { useOverlayTriggerState, type OverlayTriggerState } from 'react-stately';\nimport { useControlledState } from 'react-stately/useControlledState';\nimport { Overlay } from '../../primitives/Overlay';\nimport { usePresence } from '../../primitives/use-presence';\nimport { cn } from '../../utils/cn';\nimport {\n pickDescriptionAria,\n splitRootProps,\n type WrapperRootDOMProps,\n} from '../../utils/root-props';\nimport { COMPACT_WINDOW, useMediaQuery } from '../date-picker/use-media-query';\nimport { ButtonContext, standardIconButtons } from '../button/ButtonContext';\nimport { SearchFieldFrame } from './search-field-frame';\nimport { searchBarStyles } from './search-styles';\n\n/** Set by `SearchAppBar` for the search bar inside it. */\nexport const SearchAppBarContext = createContext<{ scrolled: boolean } | null>(null);\n\n/** What a search bar's icons can depend on. */\nexport interface SearchBarIconState {\n expanded: boolean;\n /** Collapses the search view, e.g. from a back button. */\n collapse: () => void;\n /**\n * The view in effect (`auto` resolved for the window), so a back button can show in the\n * full-screen view only.\n */\n view: 'docked' | 'full-screen';\n}\n\ntype SearchBarIcon = ReactNode | ((state: SearchBarIconState) => ReactNode);\n\nexport interface SearchBarClassNames {\n /** The outer container, merged with `className`. */\n root?: string;\n /** The pill around the icons and the input (collapsed and expanded). */\n field?: string;\n /** The text input (collapsed and expanded). */\n input?: string;\n /** The leading icon slot. */\n leading?: string;\n /** The trailing icon slot. */\n trailing?: string;\n /** The scrim behind the docked view. */\n scrim?: string;\n /** The expanded view: the docked panel or the full-screen surface. */\n view?: string;\n /** The expanded content: the docked dropdown or the full-screen content area. */\n content?: string;\n}\n\ntype Naming =\n | {\n /** Accessible name for the search bar and its input. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the search bar and its input. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\n/**\n * Attributes for the search inputs, e.g. `aria-keyshortcuts`, `aria-describedby`,\n * `enterKeyHint`, `autoComplete`, `spellCheck`, `inputMode` or event handlers. The bar owns\n * its value, name, role and popup state, so those keys are left out; an `id` or `name`\n * would be duplicated, since the bar renders a second input while expanded.\n */\nexport type SearchBarInputProps = Omit<\n ComponentProps<'input'>,\n | 'ref'\n | 'id'\n | 'name'\n | 'type'\n | 'role'\n | 'value'\n | 'defaultValue'\n | 'onChange'\n | 'placeholder'\n | 'className'\n | 'children'\n | 'dangerouslySetInnerHTML'\n | 'inert'\n | 'aria-label'\n | 'aria-labelledby'\n | 'aria-expanded'\n | 'aria-haspopup'\n | 'aria-controls'\n | 'aria-hidden'\n> & {\n [data: `data-${string}`]: string | number | boolean | undefined;\n};\n\ninterface SearchBarOwnProps extends WrapperRootDOMProps {\n /** The root element's id. */\n id?: string;\n /**\n * The ids of elements that describe the search, for both inputs (the bar's and the open\n * view's), before any `inputProps['aria-describedby']`.\n */\n 'aria-describedby'?: string;\n /** The id of an element with more details about the search, for both inputs. */\n 'aria-details'?: string;\n /** Controlled query. */\n value?: string;\n /** Initial query when uncontrolled. @default \"\" */\n defaultValue?: string;\n /** Called with the query as it changes. */\n onChange?: (value: string) => void;\n /**\n * Called with the query when the user presses Enter (the keyboard's search action) in the\n * open search view. Enter in the collapsed bar opens the view.\n */\n onSubmit?: (value: string) => void;\n /** Controlled expansion of the search view. */\n expanded?: boolean;\n /** Whether the search view starts expanded (uncontrolled). @default false */\n defaultExpanded?: boolean;\n /** Called when the search view expands or collapses. */\n onExpandedChange?: (expanded: boolean) => void;\n /**\n * How the bar expands: `docked`, a dropdown below it over a scrim; `full-screen`, a view\n * that fills the window; `auto`, full screen on compact windows (below 600px wide) and\n * docked on larger ones, as M3 recommends (\"Full-screen layout: Default for compact\n * breakpoints\"; docked is \"Best for medium and expanded windows\"). It follows the window\n * as it resizes.\n * @default \"auto\"\n */\n view?: SearchBarView;\n /** Hint text shown in the empty input, e.g. \"Search mail\". */\n placeholder?: string;\n /** Leading icon, e.g. a search icon, or a back button while expanded. */\n leadingIcon?: SearchBarIcon;\n /**\n * Trailing content: one or two icon buttons (e.g. voice search and a clear button), or an\n * avatar (an `img`, drawn at 30px) with up to one icon button (M3).\n */\n trailingIcon?: SearchBarIcon;\n /**\n * Centre the placeholder and the query while the bar is collapsed (M3's search app bar\n * supports centred text). @default \"start\"\n */\n textAlign?: 'start' | 'center';\n /**\n * What screen readers hear when the suggestions or results change while the view is open\n * (M3: \"the screen reader must announce the change\"), e.g. \"5 results\". Announced\n * politely whenever it changes.\n */\n resultsAnnouncement?: string;\n /**\n * The expanded content: suggestions or results, usually a list. ↓ from the input moves\n * focus into it.\n */\n children?: ReactNode;\n /** The input element (collapsed, or expanded while the view is open). */\n inputRef?: Ref<HTMLInputElement>;\n /**\n * Attributes for both inputs, the collapsed one and the expanded view's. They replace the\n * bar's defaults (`enterKeyHint=\"search\"`, `autoComplete=\"off\"`, `spellCheck={false}`);\n * event handlers run before the bar's own, and the bar's role, name, value and popup state\n * always win.\n */\n inputProps?: SearchBarInputProps;\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?: SearchBarClassNames;\n /** `data-*` attributes, like the other DOM attributes, go to the root. */\n [data: `data-${string}`]: string | number | boolean | undefined;\n}\n\nexport type SearchBarProps = SearchBarOwnProps & Naming;\n\n/** How a {@link SearchBar} expands. */\nexport type SearchBarView = 'auto' | 'docked' | 'full-screen';\n\ninterface BarRect {\n top: number;\n left: number;\n right: number;\n bottom: number;\n width: number;\n height: number;\n rtl: boolean;\n}\n\n/** How far the docked field widens on each side when focused (24dp → 12dp margins). */\nconst FOCUSED_GROWTH = 12;\n/** The search bar's maximum width (M3). */\nconst MAX_WIDTH = 720;\n/** The focused search bar's height (M3: \"Search bar container Height 56dp\"). */\nconst FIELD_HEIGHT = 56;\n\nconst FOCUSABLE =\n 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * M3 Expressive search bar: a 56px pill that expands into a search view, either a dropdown\n * below the bar (docked) or a full-screen view, holding suggestions or results. Pressing\n * the bar, typing into it or ↓ expands it; Escape, a press outside or `collapse()` closes it.\n *\n * @example\n * <SearchBar aria-label=\"Search mail\" placeholder=\"Search mail\" leadingIcon={<SearchIcon />}\n * value={query} onChange={setQuery} onSubmit={search}>\n * <SuggestionList … />\n * </SearchBar>\n */\nexport function SearchBar(props: SearchBarProps) {\n const {\n value,\n defaultValue,\n onChange,\n onSubmit,\n expanded,\n defaultExpanded,\n onExpandedChange,\n view: requestedView = 'auto',\n placeholder,\n leadingIcon,\n trailingIcon,\n textAlign = 'start',\n resultsAnnouncement,\n children,\n inputRef,\n inputProps: consumerInputProps,\n ref,\n className,\n style,\n classNames,\n ...rest\n } = props;\n const { 'aria-label': label, 'aria-labelledby': labelledBy } = props as {\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n };\n // The server and the first client render see a docked view, so hydration matches.\n const compact = useMediaQuery(COMPACT_WINDOW);\n const view = requestedView === 'auto' ? (compact ? 'full-screen' : 'docked') : requestedView;\n const [query, setQuery] = useControlledState(value, defaultValue ?? '', onChange);\n const state = useOverlayTriggerState({\n isOpen: expanded,\n defaultOpen: defaultExpanded,\n onOpenChange: onExpandedChange,\n });\n const appBar = useContext(SearchAppBarContext);\n const rootRef = useObjectRef(ref);\n const [rect, setRect] = useState<BarRect | null>(null);\n // The views grow from the bar as `--m3-search-progress` runs; under reduced motion they\n // only fade (the scrim, or the full-screen view).\n const reduceMotion = useReducedMotion();\n const { isPresent, isExiting, exitProps } = usePresence(\n state.isOpen,\n reduceMotion ? 'opacity' : '--m3-search-progress',\n );\n\n // As the view starts leaving, focus goes back at once to what had it when the view opened\n // (WAI-ARIA: a closing dialog returns focus to what opened it), not when the view unmounts\n // ~450ms later: typing meanwhile reaches the bar, which opens the view again (2026-10-17).\n // That's the bar's input when the bar opened it, and whatever had focus when an app's\n // shortcut did (a link stays focused after ⌘K and Escape); the bar's input otherwise.\n const collapsedRef = useRef<HTMLInputElement>(null);\n const collapsedInputRef = useMemo(\n () => mergeRefs(collapsedRef, isPresent ? undefined : inputRef),\n [isPresent, inputRef],\n );\n // Read before the view's input takes focus (a passive effect) and before React Aria's\n // focus scope would restore it on unmount, which this hand-over replaces.\n const restoreRef = useRef<HTMLElement | null>(null);\n useLayoutEffect(() => {\n const doc = collapsedRef.current?.ownerDocument;\n if (!state.isOpen || !doc) return;\n const active = doc.activeElement;\n restoreRef.current = active instanceof HTMLElement && active !== doc.body ? active : null;\n }, [state.isOpen]);\n useEffect(() => {\n const collapsed = collapsedRef.current;\n if (!isExiting || !collapsed) return;\n // Only from inside the leaving view: a press outside that moved focus elsewhere keeps it.\n if (!collapsed.ownerDocument.activeElement?.closest('[data-exiting]')) return;\n const previous = restoreRef.current;\n const target =\n previous?.isConnected && !previous.closest('[data-exiting]') ? previous : collapsed;\n target.focus({ preventScroll: true });\n }, [isExiting]);\n\n // The expanded view starts from the bar's bounds; measure them when it opens.\n useLayoutEffect(() => {\n const root = rootRef.current;\n if (!state.isOpen || !root) return;\n const box = root.getBoundingClientRect();\n setRect({\n top: box.top,\n left: box.left,\n right: box.right,\n bottom: box.bottom,\n width: box.width,\n height: box.height,\n rtl: getComputedStyle(root).direction === 'rtl',\n });\n }, [state.isOpen, rootRef]);\n\n const iconState: SearchBarIconState = { expanded: state.isOpen, collapse: state.close, view };\n const leading = typeof leadingIcon === 'function' ? leadingIcon(iconState) : leadingIcon;\n const trailing = typeof trailingIcon === 'function' ? trailingIcon(iconState) : trailingIcon;\n const styles = searchBarStyles({\n inAppBar: appBar != null,\n hasLeading: leading != null,\n hasTrailing: trailing != null,\n });\n const { root: data, rest: own } = splitRootProps(rest, { wrapper: true });\n // The naming attributes describe the inputs, as `aria-label` names them.\n const { 'aria-describedby': describedBy, 'aria-details': details } = own;\n const describedByInput =\n [describedBy, consumerInputProps?.['aria-describedby']].filter(Boolean).join(' ') || undefined;\n // In an app bar the collapsed field is `surface-container`; the expanded view's field\n // keeps the search view's own `surface-container-high`.\n const collapsedTone = appBar ? (appBar.scrolled ? 'scrolled' : 'appBar') : 'default';\n\n // Icon buttons in a search bar are standard (M3), not the filled default.\n const field = (input: ReactNode, tone: 'default' | 'appBar' | 'scrolled') => (\n <ButtonContext value={standardIconButtons}>\n <SearchFieldFrame\n tone={tone}\n className={classNames?.field}\n leading={\n leading != null && (\n <span data-slot=\"leading\" className={styles.leading({ class: classNames?.leading })}>\n {leading}\n </span>\n )\n }\n trailing={\n trailing != null && (\n <span data-slot=\"trailing\" className={styles.trailing({ class: classNames?.trailing })}>\n {trailing}\n </span>\n )\n }\n >\n {input}\n </SearchFieldFrame>\n </ButtonContext>\n );\n\n // The consumer's attributes replace the defaults; the bar's own keys always win, and\n // `mergeProps` chains event handlers (the consumer's first).\n const sharedInputProps = (handlers: ComponentProps<'input'>): ComponentProps<'input'> => ({\n ...mergeProps(\n {\n enterKeyHint: 'search',\n autoComplete: 'off',\n spellCheck: false,\n 'aria-details': details,\n ...pickDescriptionAria(own),\n },\n consumerInputProps,\n { 'aria-describedby': describedByInput },\n handlers,\n ),\n id: undefined,\n name: undefined,\n role: undefined,\n type: 'search',\n value: query,\n placeholder,\n 'aria-label': label,\n 'aria-labelledby': label == null ? labelledBy : undefined,\n });\n const inputClass = (align: 'start' | 'center') =>\n searchBarStyles({\n hasLeading: leading != null,\n hasTrailing: trailing != null,\n textAlign: align,\n }).input({ class: classNames?.input });\n\n const submitOnEnter = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Enter' && !event.nativeEvent.isComposing) {\n event.preventDefault();\n onSubmit?.(query);\n return true;\n }\n return false;\n };\n\n return (\n <div\n {...data}\n id={(own as { id?: string }).id}\n ref={rootRef}\n style={style}\n data-expanded={state.isOpen || undefined}\n data-view={view}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n {field(\n <input\n {...sharedInputProps({\n onChange: (event: ChangeEvent<HTMLInputElement>) => {\n const next = event.target.value;\n // Compose expands the bar as the user types into it.\n if (next.length > query.length) state.open();\n setQuery(next);\n },\n onClick: state.open,\n // Enter and ↓ open the search view, as a click does (Compose expands the bar\n // as its field takes focus); the query is submitted from the open view.\n onKeyDown: (event) => {\n const enter = event.key === 'Enter' && !event.nativeEvent.isComposing;\n if (enter || event.key === 'ArrowDown') {\n event.preventDefault();\n state.open();\n }\n },\n })}\n data-slot=\"input\"\n className={inputClass(textAlign)}\n // The consumer's `inputRef` follows whichever input is live.\n ref={collapsedInputRef}\n // Live again as soon as the view starts leaving: a press or typing on the bar\n // while the view leaves (its parts take no presses) opens it again from where it\n // is, instead of being swallowed for the ~450ms of the exit (2026-10-17).\n inert={state.isOpen}\n aria-hidden={state.isOpen || undefined}\n // A combobox whose popup is the search view's dialog (ARIA 1.2).\n role=\"combobox\"\n aria-expanded={state.isOpen}\n aria-haspopup=\"dialog\"\n />,\n collapsedTone,\n )}\n {isPresent && rect && (\n <Overlay exiting={isExiting}>\n <SearchView\n view={view}\n state={state}\n rect={rect}\n animateHeight={!reduceMotion}\n isExiting={isExiting}\n exitProps={exitProps}\n label={label}\n labelledBy={labelledBy}\n classNames={classNames}\n styles={styles}\n resultsAnnouncement={resultsAnnouncement}\n field={(contentRef) =>\n field(\n <ExpandedInput\n {...sharedInputProps({\n onChange: (event) => setQuery(event.target.value),\n onKeyDown: (event) => {\n // M3: once results are queried, the query stays visible but not\n // focused; focus moves to the results.\n if (submitOnEnter(event)) {\n contentRef.current?.focus();\n return;\n }\n if (event.key === 'ArrowDown') {\n const first = contentRef.current?.querySelector<HTMLElement>(FOCUSABLE);\n if (first) {\n event.preventDefault();\n first.focus();\n }\n }\n },\n })}\n data-slot=\"input\"\n className={inputClass('start')}\n inputRef={inputRef}\n open={state.isOpen}\n />,\n 'default',\n )\n }\n >\n {children}\n </SearchView>\n </Overlay>\n )}\n </div>\n );\n}\n\nfunction ExpandedInput({\n inputRef,\n open,\n ...props\n}: Omit<ComponentProps<'input'>, 'ref'> & {\n inputRef?: Ref<HTMLInputElement>;\n /** The view is open (not leaving): reopened while leaving, the input takes focus again. */\n open: boolean;\n}) {\n const ref = useObjectRef(inputRef);\n // Take focus when the view opens, with the caret after the query.\n useEffect(() => {\n const input = ref.current;\n if (!input || !open) return;\n input.focus({ preventScroll: true });\n input.setSelectionRange(input.value.length, input.value.length);\n }, [ref, open]);\n return <input {...props} ref={ref} />;\n}\n\nfunction SearchView({\n view,\n state,\n rect,\n animateHeight,\n isExiting,\n exitProps,\n label,\n labelledBy,\n classNames,\n styles,\n field,\n resultsAnnouncement,\n children,\n}: {\n view: 'docked' | 'full-screen';\n state: OverlayTriggerState;\n rect: BarRect;\n /** Whether the dropdown's height follows its content on a spring (not under reduced motion). */\n animateHeight: boolean;\n isExiting: boolean;\n exitProps: ReturnType<typeof usePresence>['exitProps'];\n label?: string;\n labelledBy?: string;\n classNames?: SearchBarClassNames;\n styles: ReturnType<typeof searchBarStyles>;\n field: (contentRef: RefObject<HTMLDivElement | null>) => ReactNode;\n resultsAnnouncement?: string;\n children: ReactNode;\n}) {\n const panelRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const contentSizeRef = useRef<HTMLDivElement>(null);\n useContentHeight(\n contentRef,\n contentSizeRef,\n view === 'docked' && animateHeight && children != null,\n );\n const { modalProps, underlayProps } = useModalOverlay(\n { isDismissable: !isExiting },\n state,\n panelRef,\n );\n const { dialogProps } = useDialog(\n { 'aria-label': label, 'aria-labelledby': label == null ? labelledBy : undefined },\n panelRef,\n );\n const exiting = isExiting || undefined;\n const panelProps = mergeProps(modalProps, dialogProps, { tabIndex: -1 });\n\n if (view === 'full-screen') {\n const vars = {\n '--m3-search-top': `${rect.top}px`,\n '--m3-search-left': `${rect.left}px`,\n '--m3-search-right': `${window.innerWidth - rect.right}px`,\n '--m3-search-bottom': `${window.innerHeight - rect.bottom}px`,\n '--m3-search-start': `${rect.rtl ? window.innerWidth - rect.right : rect.left}px`,\n '--m3-search-width': `${rect.width}px`,\n '--m3-search-height': `${rect.height}px`,\n } as CSSProperties;\n return (\n <div\n {...panelProps}\n {...exitProps}\n ref={panelRef}\n style={vars}\n data-tokens=\"md-comp-search-view\"\n data-view=\"full-screen\"\n data-exiting={exiting}\n className={styles.fullScreenPanel({ class: classNames?.view })}\n >\n <div data-exiting={exiting} className={styles.fullScreenHeader()}>\n {field(contentRef)}\n </div>\n <div\n ref={contentRef}\n tabIndex={-1}\n data-exiting={exiting}\n className={styles.fullScreenContent({ class: classNames?.content })}\n >\n {children}\n </div>\n <LiveStatus message={resultsAnnouncement} />\n </div>\n );\n }\n // The docked field widens by 12px on each side (M3 Expressive: margins 24dp → 12dp),\n // staying within the window and, unless the bar is wider already, 720px.\n const width = Math.min(rect.width + 2 * FOCUSED_GROWTH, Math.max(rect.width, MAX_WIDTH));\n const grow = (width - rect.width) / 2;\n const left = Math.max(0, Math.min(rect.left - grow, window.innerWidth - width));\n // The focused field is 56px whatever the collapsed bar's height (a consumer `h-*`),\n // centred on the bar and kept within the window.\n const top = Math.max(0, rect.top + (rect.height - FIELD_HEIGHT) / 2);\n // The panel morphs from the bar's bounds by these deltas (`dockedPanel`).\n const dockedVars = {\n '--m3-search-top': `${rect.top}px`,\n '--m3-search-left': `${rect.left}px`,\n '--m3-search-width': `${rect.width}px`,\n '--m3-search-height': `${rect.height}px`,\n '--m3-search-dx': `${left - rect.left}px`,\n '--m3-search-dy': `${top - rect.top}px`,\n '--m3-search-dw': `${Math.min(width, window.innerWidth) - rect.width}px`,\n '--m3-search-dh': `${FIELD_HEIGHT - rect.height}px`,\n } as CSSProperties;\n\n return (\n <>\n {/* Presence ends on the scrim's fade under reduced motion, else on the panel's motion. */}\n <div\n {...exitProps}\n aria-hidden=\"true\"\n data-exiting={exiting}\n className={styles.scrim({ class: classNames?.scrim })}\n />\n <div {...underlayProps} data-exiting={exiting} className={styles.underlay()}>\n <div\n {...panelProps}\n {...exitProps}\n ref={panelRef}\n style={dockedVars}\n data-exiting={exiting}\n className={styles.dockedPanel({ class: classNames?.view })}\n >\n <div className={styles.dockedHeader()}>{field(contentRef)}</div>\n {children != null && (\n <div\n ref={contentRef}\n tabIndex={-1}\n data-tokens=\"md-comp-search-view\"\n data-view=\"docked\"\n data-exiting={exiting}\n className={styles.dropdown({ class: classNames?.content })}\n >\n {/* Measured, so the dropdown's height can follow it (`useContentHeight`). */}\n <div ref={contentSizeRef} className=\"flow-root\">\n {children}\n </div>\n </div>\n )}\n <LiveStatus message={resultsAnnouncement} />\n </div>\n </div>\n </>\n );\n}\n\n/**\n * Keeps `--m3-search-content-height` on the dropdown at its content's height (plus the\n * dropdown's own padding and borders), so the dropdown's height animates as the results\n * change instead of jumping. While that height animates the dropdown doesn't scroll\n * (`data-resizing`), so a scrollbar never flashes in and rewraps the content.\n */\nfunction useContentHeight(\n dropdownRef: RefObject<HTMLDivElement | null>,\n contentRef: RefObject<HTMLDivElement | null>,\n enabled: boolean,\n) {\n useLayoutEffect(() => {\n const dropdown = dropdownRef.current;\n const content = contentRef.current;\n if (!enabled || !dropdown || !content) return;\n let measured = Number.NaN;\n const measure = () => {\n const style = getComputedStyle(dropdown);\n const frame =\n parseFloat(style.paddingTop) +\n parseFloat(style.paddingBottom) +\n parseFloat(style.borderTopWidth) +\n parseFloat(style.borderBottomWidth);\n const height = content.getBoundingClientRect().height + frame;\n // The content's box shifts by a fraction of a pixel as the panel moves; only a real\n // change of size restarts the height's spring.\n if (Math.abs(height - measured) < 0.5) return;\n measured = height;\n dropdown.style.setProperty('--m3-search-content-height', `${height}px`);\n };\n const resizing = (on: boolean) => (event: TransitionEvent) => {\n if (event.target !== dropdown || event.propertyName !== 'height') return;\n if (on) dropdown.dataset.resizing = '';\n else delete dropdown.dataset.resizing;\n };\n const start = resizing(true);\n const end = resizing(false);\n measure();\n dropdown.addEventListener('transitionrun', start);\n dropdown.addEventListener('transitionend', end);\n dropdown.addEventListener('transitioncancel', end);\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure);\n observer?.observe(content);\n return () => {\n observer?.disconnect();\n dropdown.removeEventListener('transitionrun', start);\n dropdown.removeEventListener('transitionend', end);\n dropdown.removeEventListener('transitioncancel', end);\n dropdown.style.removeProperty('--m3-search-content-height');\n delete dropdown.dataset.resizing;\n };\n }, [dropdownRef, contentRef, enabled]);\n}\n\n/**\n * A polite live region inside the search view. It starts empty and takes the message a\n * moment later, so the first one is announced too, and again whenever it changes.\n */\nfunction LiveStatus({ message }: { message?: string }) {\n const [text, setText] = useState('');\n useEffect(() => {\n const timer = setTimeout(() => setText(message ?? ''), 150);\n return () => clearTimeout(timer);\n }, [message]);\n return (\n <div role=\"status\" className=\"sr-only\">\n {text}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ import { Density } from "../../tokens/density.js";
2
+ import { WrapperRootDOMProps } from "../../utils/root-props.js";
3
+ import { SearchBarInputProps } from "./SearchBar.js";
4
+ import { CSSProperties, ReactElement, ReactNode, Ref } from "react";
5
+ //#region src/components/search/SearchField.d.ts
6
+ export interface SearchFieldClassNames {
7
+ /** The outer container, merged with `className`. */
8
+ root?: string;
9
+ /** The pill around the icons and the input. */
10
+ field?: string;
11
+ /** The text input. */
12
+ input?: string;
13
+ /** The leading icon slot. */
14
+ leading?: string;
15
+ /** The trailing slot, holding the clear button and `trailingIcon`. */
16
+ trailing?: string;
17
+ /** The clear button. */
18
+ clear?: string;
19
+ }
20
+ export interface SearchFieldLabels {
21
+ /** The clear button's name, also its tooltip. @default "Clear search" */
22
+ clear: string;
23
+ }
24
+ /**
25
+ * Attributes for the input, e.g. `aria-keyshortcuts`, `enterKeyHint`, `autoComplete`,
26
+ * `spellCheck`, `inputMode` or event handlers: those of {@link SearchBarInputProps}, less
27
+ * `disabled` and `autoFocus`, which are the field's own props.
28
+ */
29
+ export type SearchFieldInputProps = Omit<SearchBarInputProps, 'disabled' | 'autoFocus'>;
30
+ type Naming = {
31
+ /** The search box's accessible name. Required unless `aria-labelledby` is set. */
32
+ 'aria-label': string;
33
+ } | {
34
+ /** The id of the element that names the search box. Required unless `aria-label` is set. */
35
+ 'aria-labelledby': string;
36
+ };
37
+ interface SearchFieldOwnProps extends WrapperRootDOMProps {
38
+ /** The input's id. */
39
+ id?: string;
40
+ /** The ids of elements that describe the search, before any `inputProps['aria-describedby']`. */
41
+ 'aria-describedby'?: string;
42
+ /** The id of an element with more details about the search. */
43
+ 'aria-details'?: string;
44
+ /** The id of what the search filters, e.g. a list of results. */
45
+ 'aria-controls'?: string;
46
+ /** Controlled query. */
47
+ value?: string;
48
+ /** Initial query when uncontrolled. @default "" */
49
+ defaultValue?: string;
50
+ /** Called with the query as it changes, by typing, Escape or the clear button. */
51
+ onChange?: (value: string) => void;
52
+ /** Called with the query when the user presses Enter (the keyboard's search action). */
53
+ onSubmit?: (value: string) => void;
54
+ /** Hint text shown in the empty input, e.g. "Search settings". */
55
+ placeholder?: string;
56
+ /** Leading icon, usually a search icon, or a back button. */
57
+ leadingIcon?: ReactNode;
58
+ /**
59
+ * Trailing content after the clear button: an icon button (e.g. voice search), or an avatar
60
+ * (an `img`, drawn at 30px), as in the search bar.
61
+ */
62
+ trailingIcon?: ReactNode;
63
+ /** Replaces the clear button's icon (Material Symbols `close`). */
64
+ clearIcon?: ReactElement;
65
+ /**
66
+ * Web density, `0` to `-3`: each step takes 4px off the pill (56px at 0, 44px at -3), as on the
67
+ * other fields; icon buttons in it keep their 48px touch targets. Left out, the field takes the
68
+ * density of the nearest `ThemeProvider` or `ThemeScope`. M3 asks for density to be the
69
+ * reader's choice rather than a default, so prefer setting it there, from a setting.
70
+ * @default 0
71
+ */
72
+ density?: Density;
73
+ /** Whether the field is disabled: it takes no input and shows no clear button. */
74
+ disabled?: boolean;
75
+ /** Focus the input when the field mounts. */
76
+ autoFocus?: boolean;
77
+ /** The input element. */
78
+ inputRef?: Ref<HTMLInputElement>;
79
+ /**
80
+ * Attributes for the input. They replace the field's defaults (`enterKeyHint="search"`,
81
+ * `autoComplete="off"`, `spellCheck={false}`); event handlers run before the field's own, and
82
+ * its role, name, value and type always win.
83
+ */
84
+ inputProps?: SearchFieldInputProps;
85
+ /** Words the field shows or announces, in place of the English defaults. */
86
+ labels?: Partial<SearchFieldLabels>;
87
+ /** Ref to the root `<div>` element. */
88
+ ref?: Ref<HTMLDivElement>;
89
+ /** Classes for the root element, merged with `classNames.root`; consumer classes win. */
90
+ className?: string;
91
+ /** Inline styles for the root element. */
92
+ style?: CSSProperties;
93
+ /** Classes for each part, by slot. */
94
+ classNames?: SearchFieldClassNames;
95
+ /** `data-*` attributes, like the other DOM attributes, go to the root. */
96
+ [data: `data-${string}`]: string | number | boolean | undefined;
97
+ }
98
+ export type SearchFieldProps = SearchFieldOwnProps & Naming;
99
+ /**
100
+ * A search box that filters content in place (a settings page, a table, a list pane), drawn as
101
+ * the M3 search bar's 56px pill and filling its container. It never opens a search view: use
102
+ * `SearchBar` for suggestions or results. Escape clears the query, Enter calls `onSubmit`, and a
103
+ * clear button shows while there is text.
104
+ *
105
+ * @example
106
+ * <SearchField aria-label="Search settings" placeholder="Search settings"
107
+ * leadingIcon={<SearchIcon />} value={query} onChange={setQuery} />
108
+ */
109
+ export declare function SearchField(props: SearchFieldProps): import("react").JSX.Element;
110
+ //#endregion
111
+ //# sourceMappingURL=SearchField.d.ts.map
@@ -0,0 +1,128 @@
1
+ "use client";
2
+ import { densityAttribute } from "../../tokens/density.js";
3
+ import { cn } from "../../utils/cn.js";
4
+ import { ButtonContext, standardIconButtons } from "../button/ButtonContext.js";
5
+ import { IconButton } from "../icon-button/IconButton.js";
6
+ import { pickWidgetAria, splitRootProps } from "../../utils/root-props.js";
7
+ import { searchBarStyles, searchFieldStyles } from "./search-styles.js";
8
+ import { SearchFieldFrame } from "./search-field-frame.js";
9
+ import "react";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { mergeProps, useObjectRef, useSearchField } from "react-aria";
12
+ import { useControlledState } from "react-stately/useControlledState";
13
+ //#region src/components/search/SearchField.tsx
14
+ /** Material Symbols `close`. */
15
+ const ClearIcon = () => /* @__PURE__ */ jsx("svg", {
16
+ viewBox: "0 -960 960 960",
17
+ fill: "currentColor",
18
+ "aria-hidden": "true",
19
+ children: /* @__PURE__ */ jsx("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" })
20
+ });
21
+ const DEFAULT_LABELS = { clear: "Clear search" };
22
+ /** Presses on these keep their own behaviour; a press anywhere else on the pill focuses the input. */
23
+ const INTERACTIVE = "input, button, a, [role=\"button\"]";
24
+ /**
25
+ * A search box that filters content in place (a settings page, a table, a list pane), drawn as
26
+ * the M3 search bar's 56px pill and filling its container. It never opens a search view: use
27
+ * `SearchBar` for suggestions or results. Escape clears the query, Enter calls `onSubmit`, and a
28
+ * clear button shows while there is text.
29
+ *
30
+ * @example
31
+ * <SearchField aria-label="Search settings" placeholder="Search settings"
32
+ * leadingIcon={<SearchIcon />} value={query} onChange={setQuery} />
33
+ */
34
+ function SearchField(props) {
35
+ const { value, defaultValue, onChange, onSubmit, placeholder, leadingIcon, trailingIcon, clearIcon, density, disabled = false, autoFocus, inputRef, inputProps: consumerInputProps, labels: labelsProp, ref, className, style, classNames, ...rest } = props;
36
+ const labels = {
37
+ ...DEFAULT_LABELS,
38
+ ...labelsProp
39
+ };
40
+ const [query, setQuery] = useControlledState(value, defaultValue ?? "", onChange);
41
+ const objectInputRef = useObjectRef(inputRef);
42
+ const { root: data, rest: own } = splitRootProps(rest, { wrapper: true });
43
+ const { id, "aria-label": label, "aria-labelledby": labelledBy, "aria-describedby": describedBy, "aria-details": details } = own;
44
+ const describedByInput = [describedBy, consumerInputProps?.["aria-describedby"]].filter(Boolean).join(" ") || void 0;
45
+ const { inputProps } = useSearchField({
46
+ id,
47
+ "aria-label": label,
48
+ "aria-labelledby": label == null ? labelledBy : void 0,
49
+ "aria-describedby": describedByInput,
50
+ "aria-details": details,
51
+ placeholder,
52
+ isDisabled: disabled,
53
+ autoFocus
54
+ }, {
55
+ value: query,
56
+ setValue: setQuery
57
+ }, objectInputRef);
58
+ const submitOnEnter = (event) => {
59
+ if (!onSubmit || event.key !== "Enter" || event.nativeEvent.isComposing) return;
60
+ event.preventDefault();
61
+ onSubmit(query);
62
+ };
63
+ const clear = () => {
64
+ setQuery("");
65
+ objectInputRef.current?.focus({ preventScroll: true });
66
+ };
67
+ const focusInput = (event) => {
68
+ if (disabled || event.button !== 0) return;
69
+ if (event.target.closest(INTERACTIVE)) return;
70
+ event.preventDefault();
71
+ objectInputRef.current?.focus();
72
+ };
73
+ const showClear = query !== "" && !disabled;
74
+ const hasTrailing = showClear || trailingIcon != null;
75
+ const styles = searchBarStyles({
76
+ hasLeading: leadingIcon != null,
77
+ hasTrailing,
78
+ disabled
79
+ });
80
+ const rootStyles = searchFieldStyles();
81
+ return /* @__PURE__ */ jsx("div", {
82
+ ...data,
83
+ ref,
84
+ style,
85
+ ...densityAttribute(density),
86
+ "data-disabled": disabled || void 0,
87
+ className: rootStyles.root({ class: cn(classNames?.root, className) }),
88
+ children: /* @__PURE__ */ jsx(ButtonContext, {
89
+ value: standardIconButtons,
90
+ children: /* @__PURE__ */ jsx(SearchFieldFrame, {
91
+ disabled,
92
+ onPointerDown: focusInput,
93
+ className: classNames?.field,
94
+ leading: leadingIcon != null && /* @__PURE__ */ jsx("span", {
95
+ "data-slot": "leading",
96
+ className: styles.leading({ class: cn(rootStyles.slot(), classNames?.leading) }),
97
+ children: leadingIcon
98
+ }),
99
+ trailing: hasTrailing && /* @__PURE__ */ jsxs("span", {
100
+ "data-slot": "trailing",
101
+ className: styles.trailing({ class: cn(rootStyles.slot(), classNames?.trailing) }),
102
+ children: [showClear && /* @__PURE__ */ jsx(IconButton, {
103
+ "aria-label": labels.clear,
104
+ icon: clearIcon ?? /* @__PURE__ */ jsx(ClearIcon, {}),
105
+ "data-slot": "clear",
106
+ preventFocusOnPress: true,
107
+ onPress: clear,
108
+ className: classNames?.clear
109
+ }), trailingIcon]
110
+ }),
111
+ children: /* @__PURE__ */ jsx("input", {
112
+ ...mergeProps({
113
+ enterKeyHint: "search",
114
+ autoComplete: "off",
115
+ spellCheck: false
116
+ }, consumerInputProps, inputProps, pickWidgetAria(own), { onKeyDown: submitOnEnter }),
117
+ ref: objectInputRef,
118
+ "data-slot": "input",
119
+ className: styles.input({ class: classNames?.input })
120
+ })
121
+ })
122
+ })
123
+ });
124
+ }
125
+ //#endregion
126
+ export { SearchField };
127
+
128
+ //# sourceMappingURL=SearchField.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { searchBarStyles } from "./search-styles.js";
3
+ import { jsxs } from "react/jsx-runtime";
4
+ import { mergeProps, useFocusRing } from "react-aria";
5
+ //#region src/components/search/search-field-frame.tsx
6
+ /**
7
+ * The M3 search bar's pill: the `md-comp-search-bar` tokens and the inset focus ring while its
8
+ * input has keyboard focus. `SearchBar` (collapsed and in its views), `SearchField` and a
9
+ * searchable `Select` draw it, so they can't drift; the first two wrap it in standard icon
10
+ * buttons (`standardIconButtons`), which `Select`'s search, with none, doesn't load. Not
11
+ * exported from the package.
12
+ */
13
+ function SearchFieldFrame({ tone = "default", disabled = false, className, leading, trailing, children, ...props }) {
14
+ const { isFocusVisible, focusProps } = useFocusRing({
15
+ within: true,
16
+ isTextInput: true
17
+ });
18
+ return /* @__PURE__ */ jsxs("div", {
19
+ ...mergeProps(props, focusProps),
20
+ "data-tokens": "md-comp-search-bar",
21
+ "data-focus-visible": isFocusVisible || void 0,
22
+ className: searchBarStyles({
23
+ tone,
24
+ disabled
25
+ }).field({ class: className }),
26
+ children: [
27
+ leading,
28
+ children,
29
+ trailing
30
+ ]
31
+ });
32
+ }
33
+ //#endregion
34
+ export { SearchFieldFrame };
35
+
36
+ //# sourceMappingURL=search-field-frame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-field-frame.js","names":[],"sources":["../../../src/components/search/search-field-frame.tsx"],"sourcesContent":["'use client';\n\nimport type { HTMLAttributes, ReactNode } from 'react';\nimport { mergeProps, useFocusRing } from 'react-aria';\nimport { searchBarStyles } from './search-styles';\n\n/** What the pill draws around its input. Internal: the search bar, `SearchField` and `Select`. */\ninterface SearchFieldFrameProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n /** The container colour: the search bar's own, or a search app bar's. @default \"default\" */\n tone?: 'default' | 'appBar' | 'scrolled';\n /** M3's disabled colours. */\n disabled?: boolean;\n /** The leading slot, already wrapped in its `data-slot=\"leading\"` element. */\n leading?: ReactNode;\n /** The trailing slot, already wrapped in its `data-slot=\"trailing\"` element. */\n trailing?: ReactNode;\n /** The input. */\n children: ReactNode;\n}\n\n/**\n * The M3 search bar's pill: the `md-comp-search-bar` tokens and the inset focus ring while its\n * input has keyboard focus. `SearchBar` (collapsed and in its views), `SearchField` and a\n * searchable `Select` draw it, so they can't drift; the first two wrap it in standard icon\n * buttons (`standardIconButtons`), which `Select`'s search, with none, doesn't load. Not\n * exported from the package.\n */\nexport function SearchFieldFrame({\n tone = 'default',\n disabled = false,\n className,\n leading,\n trailing,\n children,\n ...props\n}: SearchFieldFrameProps) {\n // The pill shows the inset focus ring while its input has keyboard focus.\n const { isFocusVisible, focusProps } = useFocusRing({ within: true, isTextInput: true });\n return (\n <div\n {...mergeProps(props, focusProps)}\n data-tokens=\"md-comp-search-bar\"\n data-focus-visible={isFocusVisible || undefined}\n className={searchBarStyles({ tone, disabled }).field({ class: className })}\n >\n {leading}\n {children}\n {trailing}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AA2BA,SAAgB,iBAAiB,EAC/B,OAAO,WACP,WAAW,OACX,WACA,SACA,UACA,UACA,GAAG,SACqB;CAExB,MAAM,EAAE,gBAAgB,eAAe,aAAa;EAAE,QAAQ;EAAM,aAAa;CAAK,CAAC;CACvF,OACE,qBAAC,OAAD;EACE,GAAI,WAAW,OAAO,UAAU;EAChC,eAAY;EACZ,sBAAoB,kBAAkB,KAAA;EACtC,WAAW,gBAAgB;GAAE;GAAM;EAAS,CAAC,CAAC,CAAC,MAAM,EAAE,OAAO,UAAU,CAAC;EAJ3E,UAAA;GAMG;GACA;GACA;EACE;;AAET"}