@windmorph/react 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/autocomplete/Autocomplete.js +1 -1
- package/dist/components/autocomplete/Autocomplete.js.map +1 -1
- package/dist/components/carousel/carousel-styles.d.ts +6 -15
- package/dist/components/carousel/carousel-styles.js +4 -7
- package/dist/components/carousel/carousel-styles.js.map +1 -1
- package/dist/components/checkbox/checkbox-tokens.d.ts +1 -1
- package/dist/components/date-picker/DatePicker.js +11 -4
- package/dist/components/date-picker/DatePicker.js.map +1 -1
- package/dist/components/date-picker/date-picker-styles.d.ts +3 -3
- package/dist/components/fab-menu/FabMenu.js +2 -2
- package/dist/components/fab-menu/FabMenu.js.map +1 -1
- package/dist/components/list/List.d.ts +9 -0
- package/dist/components/list/List.js +7 -4
- package/dist/components/list/List.js.map +1 -1
- package/dist/components/list/list-styles.js +6 -6
- package/dist/components/list/list-styles.js.map +1 -1
- package/dist/components/list/list-tokens.d.ts +1 -1
- package/dist/components/list/list-tokens.js +50 -0
- package/dist/components/list/list-tokens.js.map +1 -1
- package/dist/components/menu/Menu.d.ts +13 -0
- package/dist/components/menu/Menu.js +14 -1
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/navigation/NavigationRail.js +52 -15
- package/dist/components/navigation/NavigationRail.js.map +1 -1
- package/dist/components/navigation/navigation-styles.js +1 -1
- package/dist/components/navigation/navigation-styles.js.map +1 -1
- package/dist/components/search/SearchBar.js +19 -34
- package/dist/components/search/SearchBar.js.map +1 -1
- package/dist/components/search/SearchField.d.ts +111 -0
- package/dist/components/search/SearchField.js +128 -0
- package/dist/components/search/SearchField.js.map +1 -0
- package/dist/components/search/search-field-frame.js +36 -0
- package/dist/components/search/search-field-frame.js.map +1 -0
- package/dist/components/search/search-styles.d.ts +58 -0
- package/dist/components/search/search-styles.js +29 -4
- package/dist/components/search/search-styles.js.map +1 -1
- package/dist/components/select/search-list.js +8 -13
- package/dist/components/select/search-list.js.map +1 -1
- package/dist/components/slider/Slider.js +5 -1
- package/dist/components/slider/Slider.js.map +1 -1
- package/dist/components/snackbar/snackbar-tokens.d.ts +1 -1
- package/dist/components/text-field/TextField.js +19 -5
- package/dist/components/text-field/TextField.js.map +1 -1
- package/dist/components/text-field/text-field-styles.js +2 -2
- package/dist/components/text-field/text-field-styles.js.map +1 -1
- package/dist/components/toolbar/toolbar-styles.js +2 -2
- package/dist/components/toolbar/toolbar-styles.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/primitives/use-m3-interaction.js +10 -6
- package/dist/primitives/use-m3-interaction.js.map +1 -1
- package/dist/styles/generated/components.css +5 -0
- package/dist/styles/utilities.css +22 -15
- package/dist/styles.compiled.css +1 -1
- package/package.json +1 -1
|
@@ -31,28 +31,65 @@ function expandedChrome(el) {
|
|
|
31
31
|
const RailContext = createContext(null);
|
|
32
32
|
/**
|
|
33
33
|
* Expanded width: the widest label on one line plus the item chrome, 220–360px (Compose).
|
|
34
|
-
* Past 360px labels wrap.
|
|
34
|
+
* Past 360px labels wrap. With `settle`, a width that comes from measuring rather than from
|
|
35
|
+
* expanding (the rail mounting expanded, its font arriving, the rail coming into sight) is
|
|
36
|
+
* taken at once: the rail never springs open from the 220px guess on a page's first paint.
|
|
35
37
|
*/
|
|
36
|
-
function useExpandedWidth(ref, active, content) {
|
|
38
|
+
function useExpandedWidth(ref, active, content, settle = false) {
|
|
37
39
|
const [width, setWidth] = useState(EXPANDED_MIN);
|
|
40
|
+
const mounted = useRef(false);
|
|
38
41
|
useLayoutEffect(() => {
|
|
39
42
|
const el = ref.current;
|
|
43
|
+
const first = !mounted.current;
|
|
44
|
+
mounted.current = true;
|
|
40
45
|
if (!active || !el) return;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
const measure = (instant) => {
|
|
47
|
+
const labels = [...el.querySelectorAll("[data-nav-label]")];
|
|
48
|
+
const range = el.ownerDocument.createRange();
|
|
49
|
+
const widest = Math.max(0, ...labels.map((label) => {
|
|
50
|
+
label.style.whiteSpace = "nowrap";
|
|
51
|
+
range.selectNodeContents(label);
|
|
52
|
+
const text = typeof range.getBoundingClientRect === "function" ? range.getBoundingClientRect().width : 0;
|
|
53
|
+
const natural = Math.ceil(Math.max(text, label.scrollWidth));
|
|
54
|
+
label.style.whiteSpace = "";
|
|
55
|
+
return natural;
|
|
56
|
+
}));
|
|
57
|
+
const header = el.querySelector("[data-rail-header]");
|
|
58
|
+
const headerWidth = header ? header.scrollWidth : 0;
|
|
59
|
+
const chrome = expandedChrome(el);
|
|
60
|
+
const next = Math.min(Math.max(widest + chrome, headerWidth, EXPANDED_MIN), EXPANDED_MAX);
|
|
61
|
+
if (instant && settle) {
|
|
62
|
+
el.style.transitionProperty = "none";
|
|
63
|
+
el.style.setProperty("--m3-rail-width", `${next}px`);
|
|
64
|
+
el.offsetWidth;
|
|
65
|
+
el.style.transitionProperty = "";
|
|
66
|
+
}
|
|
67
|
+
setWidth(next);
|
|
68
|
+
};
|
|
69
|
+
const fonts = document.fonts;
|
|
70
|
+
const remeasure = () => {
|
|
71
|
+
if (el.getClientRects().length > 0) measure(true);
|
|
72
|
+
};
|
|
73
|
+
fonts?.addEventListener("loadingdone", remeasure);
|
|
74
|
+
if (el.getClientRects().length > 0 || typeof ResizeObserver === "undefined") {
|
|
75
|
+
measure(first);
|
|
76
|
+
return () => fonts?.removeEventListener("loadingdone", remeasure);
|
|
77
|
+
}
|
|
78
|
+
const observer = new ResizeObserver(() => {
|
|
79
|
+
if (el.getClientRects().length === 0) return;
|
|
80
|
+
observer.disconnect();
|
|
81
|
+
measure(true);
|
|
82
|
+
});
|
|
83
|
+
observer.observe(el);
|
|
84
|
+
return () => {
|
|
85
|
+
fonts?.removeEventListener("loadingdone", remeasure);
|
|
86
|
+
observer.disconnect();
|
|
87
|
+
};
|
|
52
88
|
}, [
|
|
53
89
|
ref,
|
|
54
90
|
active,
|
|
55
|
-
content
|
|
91
|
+
content,
|
|
92
|
+
settle
|
|
56
93
|
]);
|
|
57
94
|
return width;
|
|
58
95
|
}
|
|
@@ -110,7 +147,7 @@ function NavigationRail({ expanded, defaultExpanded = false, onExpandedChange, m
|
|
|
110
147
|
const rootRef = useRef(null);
|
|
111
148
|
const lastIndicatorRef = useRef(null);
|
|
112
149
|
const slide = indicatorMotion === "slide";
|
|
113
|
-
const width = useExpandedWidth(rootRef, inlineExpanded, children);
|
|
150
|
+
const width = useExpandedWidth(rootRef, inlineExpanded, children, true);
|
|
114
151
|
const widening = useWidening(rootRef, inlineExpanded);
|
|
115
152
|
const [mode, setMode] = useState(inlineExpanded);
|
|
116
153
|
const [modeChanged, setModeChanged] = useState(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavigationRail.js","names":["Overlay"],"sources":["../../../src/components/navigation/NavigationRail.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type CSSProperties,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { mergeProps, useDialog, useModalOverlay } 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 type { NavItemLayout } from './nav-item-styles';\nimport { DensityZeroButtons } from '../button/DensityZeroButtons';\nimport { NavItem, type NavItemProps } from './NavItem';\nimport { navigationRailStyles } from './navigation-styles';\nimport { onItemArrowKeys } from './use-item-arrow-keys';\nimport { useRailItemMorph } from './use-rail-item-morph';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Registered in utilities.css (96px); every rail inset is derived from it. */\nconst COLLAPSED_WIDTH_VAR = '--m3-rail-collapsed-width';\nconst DEFAULT_COLLAPSED_WIDTH = 96;\nconst EXPANDED_MIN = 220;\nconst EXPANDED_MAX = 360;\n/** Elements that take focus with Tab, for the modal rail's initial focus. */\nconst FOCUSABLE =\n 'a[href]:not([aria-disabled=\"true\"]), button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * The item chrome beside an expanded label: an item inset each side, (collapsed − 56) / 2,\n * plus 16px pill padding each side, the 24px icon and the 8px gap. 104px at 96px.\n */\nfunction expandedChrome(el: HTMLElement) {\n const collapsed =\n Number.parseFloat(getComputedStyle(el).getPropertyValue(COLLAPSED_WIDTH_VAR)) ||\n DEFAULT_COLLAPSED_WIDTH;\n return collapsed - 56 + 64;\n}\n\ninterface RailContextValue {\n layout: NavItemLayout;\n enterLabel: boolean;\n /** The expanded list's last current pill, which the next current item's pill slides from. */\n lastIndicatorRef?: RefObject<HTMLElement | null>;\n /** The modal rail collapses after an item is chosen. */\n onNavigate?: () => void;\n}\n\nconst RailContext = createContext<RailContextValue | null>(null);\n\ntype Naming =\n | {\n /** Accessible name for the navigation rail. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the navigation rail. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\nexport interface NavigationRailClassNames {\n /** The `<nav>` container, merged with `className`. */\n root?: string;\n /** The in-flow rail's scrolling column, which holds the 44px top inset. */\n body?: string;\n /** The content above the items (`header`). */\n header?: string;\n /** The list of items. */\n items?: string;\n /** The expanded modal rail's sheet. */\n sheet?: string;\n /** The scrim behind the expanded modal rail. */\n scrim?: string;\n}\n\ninterface NavigationRailOwnProps {\n /** Whether the rail is expanded (controlled). */\n expanded?: boolean;\n /** Whether the rail starts expanded (uncontrolled). @default false */\n defaultExpanded?: boolean;\n /** Called when the rail expands or collapses. */\n onExpandedChange?: (expanded: boolean) => void;\n /**\n * Expand over the page as a modal sheet with a scrim instead of pushing content.\n * Pressing outside, Escape or choosing an item collapses it.\n * @default false\n */\n modal?: boolean;\n /**\n * With `modal`, show nothing while collapsed (the expanded rail slides in). `compact` does so\n * only below 600px wide, in CSS so the server's HTML is right: one rail then serves as the\n * rail on larger windows and as the drawer on a phone.\n * @default false\n */\n hideOnCollapse?: boolean | 'compact';\n /** Content above the items, e.g. a menu button and a FAB; a function receives the state. */\n header?: ReactNode | ((state: { expanded: boolean; toggle: () => void }) => ReactNode);\n /**\n * Align the items as a group to the top, or to the middle of the space below the header\n * (M3 recommends centre on tablets, within easier reach). The header stays at the top.\n * @default \"top\"\n */\n itemAlignment?: 'top' | 'center';\n /** Draw a divider on the edge next to the content. @default false */\n divider?: boolean;\n /**\n * How the current item's pill moves to a newly chosen item. `expand` (M3: \"the active\n * indicator expands from the center of the icon\") grows it in place. `slide` moves it from\n * the previously chosen item on the fast spatial spring while the rail is expanded (in\n * place or modal), as in Google's Expressive showcase; an extension beyond the M3 spec.\n * The collapsed rail always expands, and reduced motion fades the pill in place.\n * @default \"expand\"\n */\n indicatorMotion?: 'expand' | 'slide';\n /** `NavigationRailItem` elements. */\n children: ReactNode;\n /** Classes for each part, by slot. */\n classNames?: NavigationRailClassNames;\n}\n\nexport type NavigationRailProps = NavigationRailOwnProps &\n Naming &\n Omit<\n ComponentPropsWithRef<'nav'>,\n keyof NavigationRailOwnProps | 'aria-label' | 'aria-labelledby'\n >;\n\n/**\n * Expanded width: the widest label on one line plus the item chrome, 220–360px (Compose).\n * Past 360px labels wrap.\n */\nfunction useExpandedWidth(ref: RefObject<HTMLElement | null>, active: boolean, content: ReactNode) {\n const [width, setWidth] = useState(EXPANDED_MIN);\n useLayoutEffect(() => {\n const el = ref.current;\n if (!active || !el) return;\n const labels = [...el.querySelectorAll<HTMLElement>('[data-nav-label]')];\n // Labels wrap in a narrow rail; measure each on one line.\n const widest = Math.max(\n 0,\n ...labels.map((label) => {\n label.style.whiteSpace = 'nowrap';\n const natural = label.scrollWidth;\n label.style.whiteSpace = '';\n return natural;\n }),\n );\n const header = el.querySelector<HTMLElement>('[data-rail-header]');\n const headerWidth = header ? header.scrollWidth : 0;\n const chrome = expandedChrome(el);\n setWidth(Math.min(Math.max(widest + chrome, headerWidth, EXPANDED_MIN), EXPANDED_MAX));\n }, [ref, active, content]);\n return width;\n}\n\n/**\n * Whether `ref` is still widening after it became `expanding` (or mounted expanding, as the\n * modal sheet grows from the collapsed width): from that render until its width transition\n * ends, and false at once when none runs (reduced motion, or a consumer width). Meanwhile the\n * rail keeps its labels on one line, so none wraps letter by letter in a rail that is still\n * narrow and no icon jumps; the rail's clipping hides what doesn't fit yet.\n */\nfunction useWidening(ref: RefObject<HTMLElement | null>, expanding: boolean) {\n const [widening, setWidening] = useState(expanding);\n const [wasExpanding, setWasExpanding] = useState(expanding);\n if (expanding !== wasExpanding) {\n setWasExpanding(expanding);\n setWidening(expanding);\n }\n useLayoutEffect(() => {\n const el = ref.current;\n if (!widening || !el) return;\n // Reading the running transitions resolves this render's styles, starting the spring.\n const running =\n typeof el.getAnimations === 'function' &&\n el\n .getAnimations()\n .some(\n (animation) =>\n 'transitionProperty' in animation && animation.transitionProperty === 'width',\n );\n if (!running) {\n setWidening(false);\n return;\n }\n // A later width (the measured one replacing the minimum) retargets the same spring.\n const done = (event: TransitionEvent) => {\n if (event.target === el && event.propertyName === 'width') setWidening(false);\n };\n el.addEventListener('transitionend', done);\n return () => el.removeEventListener('transitionend', done);\n }, [ref, widening]);\n return widening;\n}\n\n/**\n * M3 Expressive navigation rail: destinations along the start edge. Collapsed (96px) items\n * show the icon above the label; expanded (220–360px) they show it beside the label and\n * the rail's width springs open. With `modal`, the expanded rail opens over the page as a\n * sheet with a scrim, focusing its first interactive element. Put a menu button in\n * `header` to toggle `expanded`. Tab or the Up / Down arrows move between items, and\n * labels wrap rather than truncate.\n *\n * Icons stay on one line through the middle of the collapsed rail in both states. For a\n * different collapsed width, set `--m3-rail-collapsed-width` (e.g. the class\n * `[--m3-rail-collapsed-width:80px]`) rather than a `w-*` class; M3's widths are 96px\n * (Expressive) and 80px (baseline).\n *\n * @example\n * <NavigationRail aria-label=\"Main\" header={({ toggle }) => <IconButton icon={<MenuIcon />} aria-label=\"Menu\" onPress={toggle} />}>\n * <NavigationRailItem href=\"/inbox\" icon={<InboxIcon />} selected>Inbox</NavigationRailItem>\n * </NavigationRail>\n */\nexport function NavigationRail({\n expanded,\n defaultExpanded = false,\n onExpandedChange,\n modal = false,\n hideOnCollapse = false,\n header,\n itemAlignment = 'top',\n divider = false,\n indicatorMotion = 'expand',\n children,\n classNames,\n className,\n style,\n ...rest\n}: NavigationRailProps) {\n const [isExpanded, setExpanded] = useControlledState(expanded, defaultExpanded, onExpandedChange);\n const toggle = () => setExpanded(!isExpanded);\n const inlineExpanded = isExpanded && !modal;\n const rootRef = useRef<HTMLElement>(null);\n const lastIndicatorRef = useRef<HTMLElement>(null);\n const slide = indicatorMotion === 'slide';\n const width = useExpandedWidth(rootRef, inlineExpanded, children);\n const widening = useWidening(rootRef, inlineExpanded);\n // Labels fade in at their new place once the rail has changed mode (not on first render).\n const [mode, setMode] = useState(inlineExpanded);\n const [modeChanged, setModeChanged] = useState(false);\n const captureItems = useRailItemMorph(rootRef, mode);\n if (mode !== inlineExpanded) {\n // The DOM still shows the old layout: note where each item's icon is.\n captureItems();\n setMode(inlineExpanded);\n setModeChanged(true);\n }\n\n const styles = navigationRailStyles({\n expanded: inlineExpanded,\n itemAlignment,\n divider,\n slidingIndicator: slide && inlineExpanded,\n hiddenOnCompact: modal && hideOnCollapse === 'compact',\n });\n const headerContent =\n typeof header === 'function' ? header({ expanded: isExpanded, toggle }) : header;\n const showInline = !(modal && hideOnCollapse === true);\n\n return (\n <>\n {showInline && (\n <nav\n {...rest}\n ref={rootRef}\n style={\n {\n '--m3-rail-width': inlineExpanded ? `${width}px` : `var(${COLLAPSED_WIDTH_VAR})`,\n ...style,\n } as CSSProperties\n }\n data-tokens={mergeDataTokens('md-comp-nav-rail', rest)}\n data-expanded={inlineExpanded || undefined}\n data-widening={widening || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div className={styles.body({ class: classNames?.body })}>\n {headerContent != null && (\n <div data-rail-header=\"\" className={styles.header({ class: classNames?.header })}>\n <DensityZeroButtons>{headerContent}</DensityZeroButtons>\n </div>\n )}\n <div\n data-item-alignment={itemAlignment}\n onKeyDown={onItemArrowKeys('vertical')}\n className={styles.items({ class: classNames?.items })}\n >\n <RailContext\n value={{\n layout: inlineExpanded ? 'rail-start' : 'rail-top',\n enterLabel: modeChanged,\n ...(slide && inlineExpanded && { lastIndicatorRef }),\n }}\n >\n {children}\n </RailContext>\n </div>\n </div>\n </nav>\n )}\n {modal && (\n <ModalRail\n open={isExpanded}\n onClose={() => setExpanded(false)}\n header={headerContent}\n naming={rest as Naming}\n slide={hideOnCollapse !== false}\n itemAlignment={itemAlignment}\n slidingIndicator={slide}\n railRef={rootRef}\n classNames={classNames}\n >\n {children}\n </ModalRail>\n )}\n </>\n );\n}\n\n/** The sheet's exit transitions; whichever runs marks the end of its exit. */\nconst SHEET_EXIT_PROPERTIES = ['width', 'translate', 'opacity'] as const;\n\nfunction ModalRail({\n open,\n onClose,\n header,\n naming,\n slide,\n itemAlignment,\n slidingIndicator,\n railRef,\n classNames,\n children,\n}: {\n open: boolean;\n onClose: () => void;\n header: ReactNode;\n naming: Naming;\n slide: boolean;\n itemAlignment: 'top' | 'center';\n /** `indicatorMotion=\"slide\"`: the pill slides between items. */\n slidingIndicator: boolean;\n railRef: RefObject<HTMLElement | null>;\n classNames?: NavigationRailClassNames;\n children: ReactNode;\n}) {\n const state = useOverlayTriggerState({\n isOpen: open,\n onOpenChange: (isOpen) => {\n if (!isOpen) onClose();\n },\n });\n // Presence ends with the sheet's own motion, not the scrim's shorter fade, which removed\n // the sheet mid-spring inside the collapsed rail (2026-10-15). Width when it shrinks back,\n // translate when it slides out, opacity under reduced motion.\n const { isPresent, isExiting, exitProps } = usePresence(open, SHEET_EXIT_PROPERTIES);\n if (!isPresent) return null;\n return (\n <Overlay exiting={isExiting}>\n <ModalSheet\n state={state}\n isExiting={isExiting}\n exitProps={exitProps}\n header={header}\n naming={naming}\n slide={slide}\n itemAlignment={itemAlignment}\n slidingIndicator={slidingIndicator}\n railRef={railRef}\n classNames={classNames}\n >\n {children}\n </ModalSheet>\n </Overlay>\n );\n}\n\nfunction ModalSheet({\n state,\n isExiting,\n exitProps,\n header,\n naming,\n slide,\n itemAlignment,\n slidingIndicator,\n railRef,\n classNames,\n children,\n}: {\n state: OverlayTriggerState;\n isExiting: boolean;\n exitProps: ReturnType<typeof usePresence>['exitProps'];\n header: ReactNode;\n naming: Naming;\n slide: boolean;\n itemAlignment: 'top' | 'center';\n /** `indicatorMotion=\"slide\"`: the pill slides between items. */\n slidingIndicator: boolean;\n railRef: RefObject<HTMLElement | null>;\n classNames?: NavigationRailClassNames;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const lastIndicatorRef = useRef<HTMLElement>(null);\n const { modalProps, underlayProps } = useModalOverlay({ isDismissable: !isExiting }, state, ref);\n const { dialogProps } = useDialog(naming, ref);\n // The sheet is portalled out of the rail, so it can't inherit a collapsed width set on the\n // rail itself. Copy it before the width is measured and the sheet first paints.\n useLayoutEffect(() => {\n const rail = railRef.current;\n if (!rail || !ref.current) return;\n ref.current.style.setProperty(\n COLLAPSED_WIDTH_VAR,\n getComputedStyle(rail).getPropertyValue(COLLAPSED_WIDTH_VAR),\n );\n }, [railRef]);\n // M3: initial focus lands on the first interactive item, whether it's the menu button,\n // the FAB or the first destination (useDialog would otherwise focus the sheet itself).\n useLayoutEffect(() => {\n const first = ref.current?.querySelector<HTMLElement>(FOCUSABLE);\n first?.focus();\n }, []);\n const width = useExpandedWidth(ref, true, children);\n const widening = useWidening(ref, true);\n const styles = navigationRailStyles({\n expanded: true,\n sheetEntry: slide ? 'slide' : 'grow',\n itemAlignment,\n slidingIndicator,\n });\n const exiting = isExiting || undefined;\n return (\n <>\n <div\n {...underlayProps}\n aria-hidden=\"true\"\n data-exiting={exiting}\n className={styles.scrim({ class: classNames?.scrim })}\n />\n <div\n {...mergeProps(modalProps, dialogProps, exitProps)}\n ref={ref}\n data-tokens=\"md-comp-nav-rail\"\n data-modal=\"\"\n data-exiting={exiting}\n data-widening={widening || undefined}\n style={{ '--m3-rail-width': `${width}px` } as CSSProperties}\n className={styles.sheet({ class: classNames?.sheet })}\n >\n {header != null && (\n <div data-rail-header=\"\" className={styles.header({ class: classNames?.header })}>\n <DensityZeroButtons>{header}</DensityZeroButtons>\n </div>\n )}\n <nav\n aria-label={'aria-label' in naming ? naming['aria-label'] : undefined}\n aria-labelledby={'aria-labelledby' in naming ? naming['aria-labelledby'] : undefined}\n data-item-alignment={itemAlignment}\n onKeyDown={onItemArrowKeys('vertical')}\n className={styles.items({ class: classNames?.items })}\n >\n <RailContext\n value={{\n layout: 'rail-start',\n enterLabel: false,\n ...(slidingIndicator && { lastIndicatorRef }),\n onNavigate: state.close,\n }}\n >\n {children}\n </RailContext>\n </nav>\n </div>\n </>\n );\n}\n\nexport type NavigationRailItemProps = NavItemProps;\n\n/** What {@link useNavigationRail} reports about the rail around a component. */\nexport interface NavigationRailState {\n /** Whether the rail shows its labels beside the icons (expanded, or the modal sheet). */\n expanded: boolean;\n /** Whether this is the rail's modal sheet. */\n modal: boolean;\n /** Collapses the modal sheet; does nothing in the rail itself. */\n close: () => void;\n}\n\n/**\n * The rail around a component, for a custom destination that sits in a {@link NavigationRail}\n * beside its items and changes with it. Throws outside a rail.\n */\nexport function useNavigationRail(): NavigationRailState {\n const rail = useContext(RailContext);\n if (!rail) throw new Error('useNavigationRail must be used inside a NavigationRail');\n return {\n expanded: rail.layout === 'rail-start',\n modal: rail.onNavigate !== undefined,\n close: () => rail.onNavigate?.(),\n };\n}\n\n/** A destination in a {@link NavigationRail}: a link (`href`) or a button. */\nexport function NavigationRailItem({ onPress, ...props }: NavigationRailItemProps) {\n const rail = useContext(RailContext);\n if (!rail) throw new Error('NavigationRailItem must be rendered inside a NavigationRail');\n return (\n <NavItem\n {...(props as NavItemProps)}\n layout={rail.layout}\n enterLabel={rail.enterLabel}\n {...(rail.lastIndicatorRef && { lastIndicatorRef: rail.lastIndicatorRef })}\n onPress={(event) => {\n onPress?.(event);\n rail.onNavigate?.();\n }}\n />\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"NavigationRail.js","names":["Overlay"],"sources":["../../../src/components/navigation/NavigationRail.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type CSSProperties,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport { mergeProps, useDialog, useModalOverlay } 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 type { NavItemLayout } from './nav-item-styles';\nimport { DensityZeroButtons } from '../button/DensityZeroButtons';\nimport { NavItem, type NavItemProps } from './NavItem';\nimport { navigationRailStyles } from './navigation-styles';\nimport { onItemArrowKeys } from './use-item-arrow-keys';\nimport { useRailItemMorph } from './use-rail-item-morph';\nimport { mergeDataTokens } from '../../utils/merge-data-tokens';\n\n/** Registered in utilities.css (96px); every rail inset is derived from it. */\nconst COLLAPSED_WIDTH_VAR = '--m3-rail-collapsed-width';\nconst DEFAULT_COLLAPSED_WIDTH = 96;\nconst EXPANDED_MIN = 220;\nconst EXPANDED_MAX = 360;\n/** Elements that take focus with Tab, for the modal rail's initial focus. */\nconst FOCUSABLE =\n 'a[href]:not([aria-disabled=\"true\"]), button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * The item chrome beside an expanded label: an item inset each side, (collapsed − 56) / 2,\n * plus 16px pill padding each side, the 24px icon and the 8px gap. 104px at 96px.\n */\nfunction expandedChrome(el: HTMLElement) {\n const collapsed =\n Number.parseFloat(getComputedStyle(el).getPropertyValue(COLLAPSED_WIDTH_VAR)) ||\n DEFAULT_COLLAPSED_WIDTH;\n return collapsed - 56 + 64;\n}\n\ninterface RailContextValue {\n layout: NavItemLayout;\n enterLabel: boolean;\n /** The expanded list's last current pill, which the next current item's pill slides from. */\n lastIndicatorRef?: RefObject<HTMLElement | null>;\n /** The modal rail collapses after an item is chosen. */\n onNavigate?: () => void;\n}\n\nconst RailContext = createContext<RailContextValue | null>(null);\n\ntype Naming =\n | {\n /** Accessible name for the navigation rail. Required unless `aria-labelledby` is set. */\n 'aria-label': string;\n }\n | {\n /** The id of the element that names the navigation rail. Required unless `aria-label` is set. */\n 'aria-labelledby': string;\n };\n\nexport interface NavigationRailClassNames {\n /** The `<nav>` container, merged with `className`. */\n root?: string;\n /** The in-flow rail's scrolling column, which holds the 44px top inset. */\n body?: string;\n /** The content above the items (`header`). */\n header?: string;\n /** The list of items. */\n items?: string;\n /** The expanded modal rail's sheet. */\n sheet?: string;\n /** The scrim behind the expanded modal rail. */\n scrim?: string;\n}\n\ninterface NavigationRailOwnProps {\n /** Whether the rail is expanded (controlled). */\n expanded?: boolean;\n /** Whether the rail starts expanded (uncontrolled). @default false */\n defaultExpanded?: boolean;\n /** Called when the rail expands or collapses. */\n onExpandedChange?: (expanded: boolean) => void;\n /**\n * Expand over the page as a modal sheet with a scrim instead of pushing content.\n * Pressing outside, Escape or choosing an item collapses it.\n * @default false\n */\n modal?: boolean;\n /**\n * With `modal`, show nothing while collapsed (the expanded rail slides in). `compact` does so\n * only below 600px wide, in CSS so the server's HTML is right: one rail then serves as the\n * rail on larger windows and as the drawer on a phone.\n * @default false\n */\n hideOnCollapse?: boolean | 'compact';\n /** Content above the items, e.g. a menu button and a FAB; a function receives the state. */\n header?: ReactNode | ((state: { expanded: boolean; toggle: () => void }) => ReactNode);\n /**\n * Align the items as a group to the top, or to the middle of the space below the header\n * (M3 recommends centre on tablets, within easier reach). The header stays at the top.\n * @default \"top\"\n */\n itemAlignment?: 'top' | 'center';\n /** Draw a divider on the edge next to the content. @default false */\n divider?: boolean;\n /**\n * How the current item's pill moves to a newly chosen item. `expand` (M3: \"the active\n * indicator expands from the center of the icon\") grows it in place. `slide` moves it from\n * the previously chosen item on the fast spatial spring while the rail is expanded (in\n * place or modal), as in Google's Expressive showcase; an extension beyond the M3 spec.\n * The collapsed rail always expands, and reduced motion fades the pill in place.\n * @default \"expand\"\n */\n indicatorMotion?: 'expand' | 'slide';\n /** `NavigationRailItem` elements. */\n children: ReactNode;\n /** Classes for each part, by slot. */\n classNames?: NavigationRailClassNames;\n}\n\nexport type NavigationRailProps = NavigationRailOwnProps &\n Naming &\n Omit<\n ComponentPropsWithRef<'nav'>,\n keyof NavigationRailOwnProps | 'aria-label' | 'aria-labelledby'\n >;\n\n/**\n * Expanded width: the widest label on one line plus the item chrome, 220–360px (Compose).\n * Past 360px labels wrap. With `settle`, a width that comes from measuring rather than from\n * expanding (the rail mounting expanded, its font arriving, the rail coming into sight) is\n * taken at once: the rail never springs open from the 220px guess on a page's first paint.\n */\nfunction useExpandedWidth(\n ref: RefObject<HTMLElement | null>,\n active: boolean,\n content: ReactNode,\n settle = false,\n) {\n const [width, setWidth] = useState(EXPANDED_MIN);\n const mounted = useRef(false);\n useLayoutEffect(() => {\n const el = ref.current;\n const first = !mounted.current;\n mounted.current = true;\n if (!active || !el) return;\n const measure = (instant: boolean) => {\n const labels = [...el.querySelectorAll<HTMLElement>('[data-nav-label]')];\n // Labels wrap in a narrow rail; measure each one's text on one line, to the fraction of a\n // pixel (`scrollWidth` rounds, and a label 145.3px wide wrapped in 145px), rounded up.\n const range = el.ownerDocument.createRange();\n const widest = Math.max(\n 0,\n ...labels.map((label) => {\n label.style.whiteSpace = 'nowrap';\n range.selectNodeContents(label);\n // A test environment's Range may have no layout (jsdom): `scrollWidth` alone there.\n const text =\n typeof range.getBoundingClientRect === 'function'\n ? range.getBoundingClientRect().width\n : 0;\n const natural = Math.ceil(Math.max(text, label.scrollWidth));\n label.style.whiteSpace = '';\n return natural;\n }),\n );\n const header = el.querySelector<HTMLElement>('[data-rail-header]');\n const headerWidth = header ? header.scrollWidth : 0;\n const chrome = expandedChrome(el);\n const next = Math.min(Math.max(widest + chrome, headerWidth, EXPANDED_MIN), EXPANDED_MAX);\n if (instant && settle) {\n // Applied with no transition, then the spring is back for the next expand or collapse.\n el.style.transitionProperty = 'none';\n el.style.setProperty('--m3-rail-width', `${next}px`);\n void el.offsetWidth;\n el.style.transitionProperty = '';\n }\n setWidth(next);\n };\n // Measured again each time the page's fonts finish loading: the rail usually mounts before\n // its font arrives, and the fallback font's labels are narrower, so they wrapped once it did.\n const fonts = document.fonts as FontFaceSet | undefined;\n const remeasure = () => {\n if (el.getClientRects().length > 0) measure(true);\n };\n fonts?.addEventListener('loadingdone', remeasure);\n // A rail out of sight (`display: none` above it, as a rail shown only on wider windows)\n // lays nothing out, so it's measured once it shows.\n if (el.getClientRects().length > 0 || typeof ResizeObserver === 'undefined') {\n measure(first);\n return () => fonts?.removeEventListener('loadingdone', remeasure);\n }\n const observer = new ResizeObserver(() => {\n if (el.getClientRects().length === 0) return;\n observer.disconnect();\n measure(true);\n });\n observer.observe(el);\n return () => {\n fonts?.removeEventListener('loadingdone', remeasure);\n observer.disconnect();\n };\n }, [ref, active, content, settle]);\n return width;\n}\n\n/**\n * Whether `ref` is still widening after it became `expanding` (or mounted expanding, as the\n * modal sheet grows from the collapsed width): from that render until its width transition\n * ends, and false at once when none runs (reduced motion, or a consumer width). Meanwhile the\n * rail keeps its labels on one line, so none wraps letter by letter in a rail that is still\n * narrow and no icon jumps; the rail's clipping hides what doesn't fit yet.\n */\nfunction useWidening(ref: RefObject<HTMLElement | null>, expanding: boolean) {\n const [widening, setWidening] = useState(expanding);\n const [wasExpanding, setWasExpanding] = useState(expanding);\n if (expanding !== wasExpanding) {\n setWasExpanding(expanding);\n setWidening(expanding);\n }\n useLayoutEffect(() => {\n const el = ref.current;\n if (!widening || !el) return;\n // Reading the running transitions resolves this render's styles, starting the spring.\n const running =\n typeof el.getAnimations === 'function' &&\n el\n .getAnimations()\n .some(\n (animation) =>\n 'transitionProperty' in animation && animation.transitionProperty === 'width',\n );\n if (!running) {\n setWidening(false);\n return;\n }\n // A later width (the measured one replacing the minimum) retargets the same spring.\n const done = (event: TransitionEvent) => {\n if (event.target === el && event.propertyName === 'width') setWidening(false);\n };\n el.addEventListener('transitionend', done);\n return () => el.removeEventListener('transitionend', done);\n }, [ref, widening]);\n return widening;\n}\n\n/**\n * M3 Expressive navigation rail: destinations along the start edge. Collapsed (96px) items\n * show the icon above the label; expanded (220–360px) they show it beside the label and\n * the rail's width springs open. With `modal`, the expanded rail opens over the page as a\n * sheet with a scrim, focusing its first interactive element. Put a menu button in\n * `header` to toggle `expanded`. Tab or the Up / Down arrows move between items, and\n * labels wrap rather than truncate.\n *\n * Icons stay on one line through the middle of the collapsed rail in both states. For a\n * different collapsed width, set `--m3-rail-collapsed-width` (e.g. the class\n * `[--m3-rail-collapsed-width:80px]`) rather than a `w-*` class; M3's widths are 96px\n * (Expressive) and 80px (baseline).\n *\n * @example\n * <NavigationRail aria-label=\"Main\" header={({ toggle }) => <IconButton icon={<MenuIcon />} aria-label=\"Menu\" onPress={toggle} />}>\n * <NavigationRailItem href=\"/inbox\" icon={<InboxIcon />} selected>Inbox</NavigationRailItem>\n * </NavigationRail>\n */\nexport function NavigationRail({\n expanded,\n defaultExpanded = false,\n onExpandedChange,\n modal = false,\n hideOnCollapse = false,\n header,\n itemAlignment = 'top',\n divider = false,\n indicatorMotion = 'expand',\n children,\n classNames,\n className,\n style,\n ...rest\n}: NavigationRailProps) {\n const [isExpanded, setExpanded] = useControlledState(expanded, defaultExpanded, onExpandedChange);\n const toggle = () => setExpanded(!isExpanded);\n const inlineExpanded = isExpanded && !modal;\n const rootRef = useRef<HTMLElement>(null);\n const lastIndicatorRef = useRef<HTMLElement>(null);\n const slide = indicatorMotion === 'slide';\n const width = useExpandedWidth(rootRef, inlineExpanded, children, true);\n const widening = useWidening(rootRef, inlineExpanded);\n // Labels fade in at their new place once the rail has changed mode (not on first render).\n const [mode, setMode] = useState(inlineExpanded);\n const [modeChanged, setModeChanged] = useState(false);\n const captureItems = useRailItemMorph(rootRef, mode);\n if (mode !== inlineExpanded) {\n // The DOM still shows the old layout: note where each item's icon is.\n captureItems();\n setMode(inlineExpanded);\n setModeChanged(true);\n }\n\n const styles = navigationRailStyles({\n expanded: inlineExpanded,\n itemAlignment,\n divider,\n slidingIndicator: slide && inlineExpanded,\n hiddenOnCompact: modal && hideOnCollapse === 'compact',\n });\n const headerContent =\n typeof header === 'function' ? header({ expanded: isExpanded, toggle }) : header;\n const showInline = !(modal && hideOnCollapse === true);\n\n return (\n <>\n {showInline && (\n <nav\n {...rest}\n ref={rootRef}\n style={\n {\n '--m3-rail-width': inlineExpanded ? `${width}px` : `var(${COLLAPSED_WIDTH_VAR})`,\n ...style,\n } as CSSProperties\n }\n data-tokens={mergeDataTokens('md-comp-nav-rail', rest)}\n data-expanded={inlineExpanded || undefined}\n data-widening={widening || undefined}\n className={styles.root({ class: cn(classNames?.root, className) })}\n >\n <div className={styles.body({ class: classNames?.body })}>\n {headerContent != null && (\n <div data-rail-header=\"\" className={styles.header({ class: classNames?.header })}>\n <DensityZeroButtons>{headerContent}</DensityZeroButtons>\n </div>\n )}\n <div\n data-item-alignment={itemAlignment}\n onKeyDown={onItemArrowKeys('vertical')}\n className={styles.items({ class: classNames?.items })}\n >\n <RailContext\n value={{\n layout: inlineExpanded ? 'rail-start' : 'rail-top',\n enterLabel: modeChanged,\n ...(slide && inlineExpanded && { lastIndicatorRef }),\n }}\n >\n {children}\n </RailContext>\n </div>\n </div>\n </nav>\n )}\n {modal && (\n <ModalRail\n open={isExpanded}\n onClose={() => setExpanded(false)}\n header={headerContent}\n naming={rest as Naming}\n slide={hideOnCollapse !== false}\n itemAlignment={itemAlignment}\n slidingIndicator={slide}\n railRef={rootRef}\n classNames={classNames}\n >\n {children}\n </ModalRail>\n )}\n </>\n );\n}\n\n/** The sheet's exit transitions; whichever runs marks the end of its exit. */\nconst SHEET_EXIT_PROPERTIES = ['width', 'translate', 'opacity'] as const;\n\nfunction ModalRail({\n open,\n onClose,\n header,\n naming,\n slide,\n itemAlignment,\n slidingIndicator,\n railRef,\n classNames,\n children,\n}: {\n open: boolean;\n onClose: () => void;\n header: ReactNode;\n naming: Naming;\n slide: boolean;\n itemAlignment: 'top' | 'center';\n /** `indicatorMotion=\"slide\"`: the pill slides between items. */\n slidingIndicator: boolean;\n railRef: RefObject<HTMLElement | null>;\n classNames?: NavigationRailClassNames;\n children: ReactNode;\n}) {\n const state = useOverlayTriggerState({\n isOpen: open,\n onOpenChange: (isOpen) => {\n if (!isOpen) onClose();\n },\n });\n // Presence ends with the sheet's own motion, not the scrim's shorter fade, which removed\n // the sheet mid-spring inside the collapsed rail (2026-10-15). Width when it shrinks back,\n // translate when it slides out, opacity under reduced motion.\n const { isPresent, isExiting, exitProps } = usePresence(open, SHEET_EXIT_PROPERTIES);\n if (!isPresent) return null;\n return (\n <Overlay exiting={isExiting}>\n <ModalSheet\n state={state}\n isExiting={isExiting}\n exitProps={exitProps}\n header={header}\n naming={naming}\n slide={slide}\n itemAlignment={itemAlignment}\n slidingIndicator={slidingIndicator}\n railRef={railRef}\n classNames={classNames}\n >\n {children}\n </ModalSheet>\n </Overlay>\n );\n}\n\nfunction ModalSheet({\n state,\n isExiting,\n exitProps,\n header,\n naming,\n slide,\n itemAlignment,\n slidingIndicator,\n railRef,\n classNames,\n children,\n}: {\n state: OverlayTriggerState;\n isExiting: boolean;\n exitProps: ReturnType<typeof usePresence>['exitProps'];\n header: ReactNode;\n naming: Naming;\n slide: boolean;\n itemAlignment: 'top' | 'center';\n /** `indicatorMotion=\"slide\"`: the pill slides between items. */\n slidingIndicator: boolean;\n railRef: RefObject<HTMLElement | null>;\n classNames?: NavigationRailClassNames;\n children: ReactNode;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const lastIndicatorRef = useRef<HTMLElement>(null);\n const { modalProps, underlayProps } = useModalOverlay({ isDismissable: !isExiting }, state, ref);\n const { dialogProps } = useDialog(naming, ref);\n // The sheet is portalled out of the rail, so it can't inherit a collapsed width set on the\n // rail itself. Copy it before the width is measured and the sheet first paints.\n useLayoutEffect(() => {\n const rail = railRef.current;\n if (!rail || !ref.current) return;\n ref.current.style.setProperty(\n COLLAPSED_WIDTH_VAR,\n getComputedStyle(rail).getPropertyValue(COLLAPSED_WIDTH_VAR),\n );\n }, [railRef]);\n // M3: initial focus lands on the first interactive item, whether it's the menu button,\n // the FAB or the first destination (useDialog would otherwise focus the sheet itself).\n useLayoutEffect(() => {\n const first = ref.current?.querySelector<HTMLElement>(FOCUSABLE);\n first?.focus();\n }, []);\n const width = useExpandedWidth(ref, true, children);\n const widening = useWidening(ref, true);\n const styles = navigationRailStyles({\n expanded: true,\n sheetEntry: slide ? 'slide' : 'grow',\n itemAlignment,\n slidingIndicator,\n });\n const exiting = isExiting || undefined;\n return (\n <>\n <div\n {...underlayProps}\n aria-hidden=\"true\"\n data-exiting={exiting}\n className={styles.scrim({ class: classNames?.scrim })}\n />\n <div\n {...mergeProps(modalProps, dialogProps, exitProps)}\n ref={ref}\n data-tokens=\"md-comp-nav-rail\"\n data-modal=\"\"\n data-exiting={exiting}\n data-widening={widening || undefined}\n style={{ '--m3-rail-width': `${width}px` } as CSSProperties}\n className={styles.sheet({ class: classNames?.sheet })}\n >\n {header != null && (\n <div data-rail-header=\"\" className={styles.header({ class: classNames?.header })}>\n <DensityZeroButtons>{header}</DensityZeroButtons>\n </div>\n )}\n <nav\n aria-label={'aria-label' in naming ? naming['aria-label'] : undefined}\n aria-labelledby={'aria-labelledby' in naming ? naming['aria-labelledby'] : undefined}\n data-item-alignment={itemAlignment}\n onKeyDown={onItemArrowKeys('vertical')}\n className={styles.items({ class: classNames?.items })}\n >\n <RailContext\n value={{\n layout: 'rail-start',\n enterLabel: false,\n ...(slidingIndicator && { lastIndicatorRef }),\n onNavigate: state.close,\n }}\n >\n {children}\n </RailContext>\n </nav>\n </div>\n </>\n );\n}\n\nexport type NavigationRailItemProps = NavItemProps;\n\n/** What {@link useNavigationRail} reports about the rail around a component. */\nexport interface NavigationRailState {\n /** Whether the rail shows its labels beside the icons (expanded, or the modal sheet). */\n expanded: boolean;\n /** Whether this is the rail's modal sheet. */\n modal: boolean;\n /** Collapses the modal sheet; does nothing in the rail itself. */\n close: () => void;\n}\n\n/**\n * The rail around a component, for a custom destination that sits in a {@link NavigationRail}\n * beside its items and changes with it. Throws outside a rail.\n */\nexport function useNavigationRail(): NavigationRailState {\n const rail = useContext(RailContext);\n if (!rail) throw new Error('useNavigationRail must be used inside a NavigationRail');\n return {\n expanded: rail.layout === 'rail-start',\n modal: rail.onNavigate !== undefined,\n close: () => rail.onNavigate?.(),\n };\n}\n\n/** A destination in a {@link NavigationRail}: a link (`href`) or a button. */\nexport function NavigationRailItem({ onPress, ...props }: NavigationRailItemProps) {\n const rail = useContext(RailContext);\n if (!rail) throw new Error('NavigationRailItem must be rendered inside a NavigationRail');\n return (\n <NavItem\n {...(props as NavItemProps)}\n layout={rail.layout}\n enterLabel={rail.enterLabel}\n {...(rail.lastIndicatorRef && { lastIndicatorRef: rail.lastIndicatorRef })}\n onPress={(event) => {\n onPress?.(event);\n rail.onNavigate?.();\n }}\n />\n );\n}\n"],"mappings":"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"}
|
|
@@ -23,7 +23,7 @@ const navigationRailStyles = tv({
|
|
|
23
23
|
]
|
|
24
24
|
],
|
|
25
25
|
body: "flex min-h-0 flex-1 flex-col overflow-x-clip overflow-y-auto pt-[44px]",
|
|
26
|
-
header: "grid grid-cols-[56px] justify-items-center-safe ps-(--m3-rail-item-inset) pb-[40px] [&>*]:
|
|
26
|
+
header: "grid grid-cols-[56px] justify-items-center-safe ps-(--m3-rail-item-inset) pb-[40px] [&>*]:whitespace-nowrap",
|
|
27
27
|
items: ["flex flex-col", "[transition-property:gap] [transition-duration:var(--md-sys-motion-spring-spatial-default-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]"],
|
|
28
28
|
scrim: [
|
|
29
29
|
"fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-styles.js","names":[],"sources":["../../../src/components/navigation/navigation-styles.ts"],"sourcesContent":["import { reducedMotionFade } from '../../motion/reduced-motion';\nimport { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (WideNavigationRail.kt, ShortNavigationBar.kt and the\n * NavigationRail* / NavigationBar* token files).\n *\n * Rail: `surface`, 96px collapsed; expanded it is as wide as its widest item plus the 20px\n * insets, 220–360px, and the width springs on the default spatial spring. Content starts\n * 44px down; a header sits 40px above the items, which are 4px apart collapsed and flush\n * expanded. The modal expanded rail is `surface-container` at level 2 with 16px end\n * corners over a 32% scrim; it grows from the collapsed width on the fast spatial spring, or\n * slides in on the default one when the collapsed rail is hidden. It stays mounted until its\n * own width or slide (or, under reduced motion, its fade) has ended.\n *\n * Icons stay on one line in both states, through the middle of the collapsed rail, as in\n * M3's rail motion: only the labels and the rail's edge move. While the width springs open\n * (and while the modal sheet shrinks away) labels stay on one line, clipped by the rail, so\n * none wraps letter by letter in a rail that is still narrow and no item grows taller. So nothing is centred in\n * the animated width. The collapsed width is `--m3-rail-collapsed-width` (96px), and the\n * item inset `--m3-rail-item-inset` is half of what's left beside a 56px pill (20px). The\n * header is a 56px column at that inset: anything up to 56px (the menu button, a FAB) is\n * centred on the icon line, and anything wider (an extended FAB) keeps its width and starts\n * at the column's edge.\n *\n * Items align as a group to the top (default) or, with `itemAlignment=\"center\"`, to the\n * middle of the space below the header (M3: centre alignment on tablets); the header stays\n * at the top. An optional divider (`outline-variant`) marks the edge next to the content.\n *\n * Bar: `surface-container`, at least 64px (it grows when labels wrap), no elevation.\n * Stacked items share the width equally, or, when centred, sit within side padding of\n * (100% − 10% × (items + 3)) / 2 (up to 6 items). Items beside their icons\n * (`iconPlacement=\"start\"`) are centred by default and sized to their content, so the extra\n * space goes to the bar's ends (M3: \"Horizontal navigation items have a fixed width\").\n * With `scrollBehavior=\"hide-on-scroll\"` the bar is sticky at the bottom and its offset\n * (`--m3-nav-bar-offset`, 0 down to minus its height) follows the page scroll.\n *\n * The rail's width is the `--m3-rail-width` variable, so a consumer `w-*` class still wins;\n * a narrower or wider collapsed rail sets `--m3-rail-collapsed-width` instead.\n *\n * Container colours and the modal rail's corners come from the component tokens\n * (`navigation-tokens.ts`, `--md-comp-nav-bar-*` and `--md-comp-nav-rail-*`): the bar, the\n * rail and the modal rail's sheet (`data-modal`) carry `data-tokens`. A consumer class still\n * wins in `cn()`.\n * Every class is written out in full so Tailwind can find it.\n */\n\n/** Half of what's left beside a 56px pill in the collapsed rail. */\nconst itemInset = '[--m3-rail-item-inset:calc((var(--m3-rail-collapsed-width)_-_56px)/2)]';\n\n/**\n * While the expanded rail is still widening (`data-widening`), labels stay on one line and\n * the rail clips them, so the items keep their height and the icons their places.\n */\nconst oneLineWhileResizing = '[&[data-widening]_[data-nav-label]]:text-nowrap';\n\nconst widthMotion = [\n '[transition-property:width]',\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\n/** Variant definitions for {@link NavigationRail}; extend them to add variants. */\nexport const navigationRailStyles = tv({\n slots: {\n root: [\n // Positioned, so a secondary column placed against it escapes the body's scrolling.\n 'relative flex w-(--m3-rail-width) shrink-0 flex-col text-on-surface',\n 'bg-[var(--_md-comp-nav-rail-collapsed-container-color)] data-expanded:bg-[var(--_md-comp-nav-rail-expanded-container-color)]',\n itemInset,\n oneLineWhileResizing,\n ...widthMotion,\n ],\n // Library-owned wrapper: clips labels while the width animates.\n body: 'flex min-h-0 flex-1 flex-col overflow-x-clip overflow-y-auto pt-[44px]',\n // Children keep their own width (a centred grid item would shrink to the 56px column).\n header:\n 'grid grid-cols-[56px] justify-items-center-safe ps-(--m3-rail-item-inset) pb-[40px] [&>*]:min-w-max',\n // The gap between items springs 4 → 0px as the rail expands (default spatial, as\n // Compose's itemVerticalSpacedBy; 2026-10-17), instant under reduced motion.\n items: [\n 'flex flex-col',\n '[transition-property:gap] [transition-duration:var(--md-sys-motion-spring-spatial-default-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n ],\n scrim: [\n 'fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100',\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 // In and out on the default effects spring, as Compose's modal rail scrim (2026-10-15).\n 'starting:opacity-0 data-exiting:opacity-0',\n ],\n sheet: [\n 'fixed inset-y-0 start-0 z-(--md-sys-z-sheet) flex w-(--m3-rail-width) flex-col overflow-x-clip overflow-y-auto pt-[44px]',\n 'rounded-e-[var(--_md-comp-nav-rail-expanded-modal-container-shape)] bg-[var(--_md-comp-nav-rail-expanded-modal-container-color)]',\n 'text-on-surface shadow-elevation-2 outline-none',\n itemInset,\n oneLineWhileResizing,\n '[&[data-exiting]_[data-nav-label]]:text-nowrap',\n '[transition-property:width,translate]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n // Under reduced motion it fades in and out at its full width.\n ...reducedMotionFade,\n ],\n },\n variants: {\n expanded: {\n true: { items: 'gap-0' },\n false: { items: 'gap-[4px]' },\n },\n /** Auto margins centre the items without hiding any when the rail overflows. */\n itemAlignment: {\n top: {},\n center: { items: 'my-auto' },\n },\n /** `hideOnCollapse=\"compact\"`: the collapsed rail shows from 600px wide. */\n hiddenOnCompact: {\n true: { root: 'max-medium:hidden' },\n false: {},\n },\n /** A divider on the edge next to the content. */\n divider: {\n true: { root: 'border-e border-outline-variant' },\n false: {},\n },\n /**\n * `indicatorMotion=\"slide\"` in the expanded rail: the list is isolated, so the items'\n * raised content stays above a sliding pill and no higher.\n */\n slidingIndicator: {\n true: { items: 'isolate' },\n false: {},\n },\n /**\n * Modal sheet entry: from the collapsed width, or sliding in from the start edge. Under\n * reduced motion it fades instead.\n */\n sheetEntry: {\n grow: {\n sheet:\n 'motion-safe:starting:w-(--m3-rail-collapsed-width) motion-safe:data-exiting:w-(--m3-rail-collapsed-width)',\n },\n // Sliding in, the sheet takes the default spatial spring, as Compose's modal rail\n // (`WideNavigationRail.kt`): the fast one overshot 21px off the window edge (2026-10-15).\n slide: {\n sheet: [\n 'motion-safe:starting:-translate-x-full motion-safe:data-exiting:-translate-x-full motion-safe:rtl:starting:translate-x-full motion-safe:rtl:data-exiting:translate-x-full',\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 },\n },\n },\n defaultVariants: {\n expanded: false,\n sheetEntry: 'grow',\n itemAlignment: 'top',\n divider: false,\n slidingIndicator: false,\n },\n});\n\n/** Variant definitions for {@link NavigationBar}; extend them to add variants. */\nexport const navigationBarStyles = tv({\n slots: {\n root: 'flex min-h-[64px] w-full bg-[var(--_md-comp-nav-bar-container-color)] text-on-surface',\n item: 'min-w-0',\n },\n variants: {\n arrangement: {\n equal: { item: 'flex-1 basis-0' },\n centered: { root: 'justify-center', item: 'flex-1 basis-0' },\n },\n iconPlacement: { top: {}, start: {} },\n hideOnScroll: {\n // Settling after a scroll ends (`data-settling`), the offset eases to shown or hidden on\n // the default effects spring; while scrolling it follows the scroll directly.\n true: {\n root: [\n 'sticky bottom-(--m3-nav-bar-offset) z-(--md-sys-z-sticky)',\n 'data-settling:[transition-property:bottom]',\n 'data-settling:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'data-settling:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n ],\n },\n false: {},\n },\n },\n compoundVariants: [\n // Items beside their icons keep their own width; the extra space goes to the ends.\n { arrangement: 'centered', iconPlacement: 'start', class: { item: 'flex-initial basis-auto' } },\n ],\n defaultVariants: { arrangement: 'equal', iconPlacement: 'top', hideOnScroll: false },\n});\n\n/**\n * The variants of {@link navigationBarStyles}, 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 NavigationBarStyleProps = VariantProps<typeof navigationBarStyles>;\nexport type NavigationBarArrangement = NonNullable<NavigationBarStyleProps['arrangement']>;\n"],"mappings":";;;;AAgDA,MAAM,YAAY;;;;;AAMlB,MAAM,uBAAuB;;AAS7B,MAAa,uBAAuB,GAAG;CACrC,OAAO;EACL,MAAM;GAEJ;GACA;GACA;GACA;GACA,GAAG;IAdP;IACA;IACA;GAYO;EACL;EAEA,MAAM;EAEN,QACE;EAGF,OAAO,CACL,iBACA,sLACF;EACA,OAAO;GACL;GACA;GACA;GACA;GAEA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA,GAAG;EACL;CACF;CACA,UAAU;EACR,UAAU;GACR,MAAM,EAAE,OAAO,QAAQ;GACvB,OAAO,EAAE,OAAO,YAAY;EAC9B;;EAEA,eAAe;GACb,KAAK,CAAC;GACN,QAAQ,EAAE,OAAO,UAAU;EAC7B;;EAEA,iBAAiB;GACf,MAAM,EAAE,MAAM,oBAAoB;GAClC,OAAO,CAAC;EACV;;EAEA,SAAS;GACP,MAAM,EAAE,MAAM,kCAAkC;GAChD,OAAO,CAAC;EACV;;;;;EAKA,kBAAkB;GAChB,MAAM,EAAE,OAAO,UAAU;GACzB,OAAO,CAAC;EACV;;;;;EAKA,YAAY;GACV,MAAM,EACJ,OACE,4GACJ;GAGA,OAAO,EACL,OAAO;IACL;IACA;IACA;GACF,EACF;EACF;CACF;CACA,iBAAiB;EACf,UAAU;EACV,YAAY;EACZ,eAAe;EACf,SAAS;EACT,kBAAkB;CACpB;AACF,CAAC;;AAGD,MAAa,sBAAsB,GAAG;CACpC,OAAO;EACL,MAAM;EACN,MAAM;CACR;CACA,UAAU;EACR,aAAa;GACX,OAAO,EAAE,MAAM,iBAAiB;GAChC,UAAU;IAAE,MAAM;IAAkB,MAAM;GAAiB;EAC7D;EACA,eAAe;GAAE,KAAK,CAAC;GAAG,OAAO,CAAC;EAAE;EACpC,cAAc;GAGZ,MAAM,EACJ,MAAM;IACJ;IACA;IACA;IACA;GACF,EACF;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB,CAEhB;EAAE,aAAa;EAAY,eAAe;EAAS,OAAO,EAAE,MAAM,0BAA0B;CAAE,CAChG;CACA,iBAAiB;EAAE,aAAa;EAAS,eAAe;EAAO,cAAc;CAAM;AACrF,CAAC"}
|
|
1
|
+
{"version":3,"file":"navigation-styles.js","names":[],"sources":["../../../src/components/navigation/navigation-styles.ts"],"sourcesContent":["import { reducedMotionFade } from '../../motion/reduced-motion';\nimport { tv, type VariantProps } from '../../utils/tv';\n\n/*\n * Values follow Compose Material 3 (WideNavigationRail.kt, ShortNavigationBar.kt and the\n * NavigationRail* / NavigationBar* token files).\n *\n * Rail: `surface`, 96px collapsed; expanded it is as wide as its widest item plus the 20px\n * insets, 220–360px, and the width springs on the default spatial spring. Content starts\n * 44px down; a header sits 40px above the items, which are 4px apart collapsed and flush\n * expanded. The modal expanded rail is `surface-container` at level 2 with 16px end\n * corners over a 32% scrim; it grows from the collapsed width on the fast spatial spring, or\n * slides in on the default one when the collapsed rail is hidden. It stays mounted until its\n * own width or slide (or, under reduced motion, its fade) has ended.\n *\n * Icons stay on one line in both states, through the middle of the collapsed rail, as in\n * M3's rail motion: only the labels and the rail's edge move. While the width springs open\n * (and while the modal sheet shrinks away) labels stay on one line, clipped by the rail, so\n * none wraps letter by letter in a rail that is still narrow and no item grows taller. So nothing is centred in\n * the animated width. The collapsed width is `--m3-rail-collapsed-width` (96px), and the\n * item inset `--m3-rail-item-inset` is half of what's left beside a 56px pill (20px). The\n * header is a 56px column at that inset: anything up to 56px (the menu button, a FAB) is\n * centred on the icon line, and anything wider (an extended FAB) keeps its width and starts\n * at the column's edge.\n *\n * Items align as a group to the top (default) or, with `itemAlignment=\"center\"`, to the\n * middle of the space below the header (M3: centre alignment on tablets); the header stays\n * at the top. An optional divider (`outline-variant`) marks the edge next to the content.\n *\n * Bar: `surface-container`, at least 64px (it grows when labels wrap), no elevation.\n * Stacked items share the width equally, or, when centred, sit within side padding of\n * (100% − 10% × (items + 3)) / 2 (up to 6 items). Items beside their icons\n * (`iconPlacement=\"start\"`) are centred by default and sized to their content, so the extra\n * space goes to the bar's ends (M3: \"Horizontal navigation items have a fixed width\").\n * With `scrollBehavior=\"hide-on-scroll\"` the bar is sticky at the bottom and its offset\n * (`--m3-nav-bar-offset`, 0 down to minus its height) follows the page scroll.\n *\n * The rail's width is the `--m3-rail-width` variable, so a consumer `w-*` class still wins;\n * a narrower or wider collapsed rail sets `--m3-rail-collapsed-width` instead.\n *\n * Container colours and the modal rail's corners come from the component tokens\n * (`navigation-tokens.ts`, `--md-comp-nav-bar-*` and `--md-comp-nav-rail-*`): the bar, the\n * rail and the modal rail's sheet (`data-modal`) carry `data-tokens`. A consumer class still\n * wins in `cn()`.\n * Every class is written out in full so Tailwind can find it.\n */\n\n/** Half of what's left beside a 56px pill in the collapsed rail. */\nconst itemInset = '[--m3-rail-item-inset:calc((var(--m3-rail-collapsed-width)_-_56px)/2)]';\n\n/**\n * While the expanded rail is still widening (`data-widening`), labels stay on one line and\n * the rail clips them, so the items keep their height and the icons their places.\n */\nconst oneLineWhileResizing = '[&[data-widening]_[data-nav-label]]:text-nowrap';\n\nconst widthMotion = [\n '[transition-property:width]',\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\n/** Variant definitions for {@link NavigationRail}; extend them to add variants. */\nexport const navigationRailStyles = tv({\n slots: {\n root: [\n // Positioned, so a secondary column placed against it escapes the body's scrolling.\n 'relative flex w-(--m3-rail-width) shrink-0 flex-col text-on-surface',\n 'bg-[var(--_md-comp-nav-rail-collapsed-container-color)] data-expanded:bg-[var(--_md-comp-nav-rail-expanded-container-color)]',\n itemInset,\n oneLineWhileResizing,\n ...widthMotion,\n ],\n // Library-owned wrapper: clips labels while the width animates.\n body: 'flex min-h-0 flex-1 flex-col overflow-x-clip overflow-y-auto pt-[44px]',\n // A 56px column at the item inset: children up to 56px centre on the icon line, a wider\n // one (an extended FAB, an app's name) keeps its width on one line and starts at the\n // column's edge. Text doesn't wrap (2026-11-11) instead of every child taking\n // `min-width: max-content`, which grew a box holding an image at its full size (an image\n // avatar) to the image's natural width (2026-11-10).\n header:\n 'grid grid-cols-[56px] justify-items-center-safe ps-(--m3-rail-item-inset) pb-[40px] [&>*]:whitespace-nowrap',\n // The gap between items springs 4 → 0px as the rail expands (default spatial, as\n // Compose's itemVerticalSpacedBy; 2026-10-17), instant under reduced motion.\n items: [\n 'flex flex-col',\n '[transition-property:gap] [transition-duration:var(--md-sys-motion-spring-spatial-default-duration)] [transition-timing-function:var(--md-sys-motion-spring-spatial-default-easing)]',\n ],\n scrim: [\n 'fixed inset-0 z-(--md-sys-z-scrim) bg-scrim/32 opacity-100',\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 // In and out on the default effects spring, as Compose's modal rail scrim (2026-10-15).\n 'starting:opacity-0 data-exiting:opacity-0',\n ],\n sheet: [\n 'fixed inset-y-0 start-0 z-(--md-sys-z-sheet) flex w-(--m3-rail-width) flex-col overflow-x-clip overflow-y-auto pt-[44px]',\n 'rounded-e-[var(--_md-comp-nav-rail-expanded-modal-container-shape)] bg-[var(--_md-comp-nav-rail-expanded-modal-container-color)]',\n 'text-on-surface shadow-elevation-2 outline-none',\n itemInset,\n oneLineWhileResizing,\n '[&[data-exiting]_[data-nav-label]]:text-nowrap',\n '[transition-property:width,translate]',\n '[transition-duration:var(--md-sys-motion-spring-spatial-fast-duration)]',\n '[transition-timing-function:var(--md-sys-motion-spring-spatial-fast-easing)]',\n // Under reduced motion it fades in and out at its full width.\n ...reducedMotionFade,\n ],\n },\n variants: {\n expanded: {\n true: { items: 'gap-0' },\n false: { items: 'gap-[4px]' },\n },\n /** Auto margins centre the items without hiding any when the rail overflows. */\n itemAlignment: {\n top: {},\n center: { items: 'my-auto' },\n },\n /** `hideOnCollapse=\"compact\"`: the collapsed rail shows from 600px wide. */\n hiddenOnCompact: {\n true: { root: 'max-medium:hidden' },\n false: {},\n },\n /** A divider on the edge next to the content. */\n divider: {\n true: { root: 'border-e border-outline-variant' },\n false: {},\n },\n /**\n * `indicatorMotion=\"slide\"` in the expanded rail: the list is isolated, so the items'\n * raised content stays above a sliding pill and no higher.\n */\n slidingIndicator: {\n true: { items: 'isolate' },\n false: {},\n },\n /**\n * Modal sheet entry: from the collapsed width, or sliding in from the start edge. Under\n * reduced motion it fades instead.\n */\n sheetEntry: {\n grow: {\n sheet:\n 'motion-safe:starting:w-(--m3-rail-collapsed-width) motion-safe:data-exiting:w-(--m3-rail-collapsed-width)',\n },\n // Sliding in, the sheet takes the default spatial spring, as Compose's modal rail\n // (`WideNavigationRail.kt`): the fast one overshot 21px off the window edge (2026-10-15).\n slide: {\n sheet: [\n 'motion-safe:starting:-translate-x-full motion-safe:data-exiting:-translate-x-full motion-safe:rtl:starting:translate-x-full motion-safe:rtl:data-exiting:translate-x-full',\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 },\n },\n },\n defaultVariants: {\n expanded: false,\n sheetEntry: 'grow',\n itemAlignment: 'top',\n divider: false,\n slidingIndicator: false,\n },\n});\n\n/** Variant definitions for {@link NavigationBar}; extend them to add variants. */\nexport const navigationBarStyles = tv({\n slots: {\n root: 'flex min-h-[64px] w-full bg-[var(--_md-comp-nav-bar-container-color)] text-on-surface',\n item: 'min-w-0',\n },\n variants: {\n arrangement: {\n equal: { item: 'flex-1 basis-0' },\n centered: { root: 'justify-center', item: 'flex-1 basis-0' },\n },\n iconPlacement: { top: {}, start: {} },\n hideOnScroll: {\n // Settling after a scroll ends (`data-settling`), the offset eases to shown or hidden on\n // the default effects spring; while scrolling it follows the scroll directly.\n true: {\n root: [\n 'sticky bottom-(--m3-nav-bar-offset) z-(--md-sys-z-sticky)',\n 'data-settling:[transition-property:bottom]',\n 'data-settling:[transition-duration:var(--md-sys-motion-spring-effects-default-duration)]',\n 'data-settling:[transition-timing-function:var(--md-sys-motion-spring-effects-default-easing)]',\n ],\n },\n false: {},\n },\n },\n compoundVariants: [\n // Items beside their icons keep their own width; the extra space goes to the ends.\n { arrangement: 'centered', iconPlacement: 'start', class: { item: 'flex-initial basis-auto' } },\n ],\n defaultVariants: { arrangement: 'equal', iconPlacement: 'top', hideOnScroll: false },\n});\n\n/**\n * The variants of {@link navigationBarStyles}, 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 NavigationBarStyleProps = VariantProps<typeof navigationBarStyles>;\nexport type NavigationBarArrangement = NonNullable<NavigationBarStyleProps['arrangement']>;\n"],"mappings":";;;;AAgDA,MAAM,YAAY;;;;;AAMlB,MAAM,uBAAuB;;AAS7B,MAAa,uBAAuB,GAAG;CACrC,OAAO;EACL,MAAM;GAEJ;GACA;GACA;GACA;GACA,GAAG;IAdP;IACA;IACA;GAYO;EACL;EAEA,MAAM;EAMN,QACE;EAGF,OAAO,CACL,iBACA,sLACF;EACA,OAAO;GACL;GACA;GACA;GACA;GAEA;EACF;EACA,OAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAEA,GAAG;EACL;CACF;CACA,UAAU;EACR,UAAU;GACR,MAAM,EAAE,OAAO,QAAQ;GACvB,OAAO,EAAE,OAAO,YAAY;EAC9B;;EAEA,eAAe;GACb,KAAK,CAAC;GACN,QAAQ,EAAE,OAAO,UAAU;EAC7B;;EAEA,iBAAiB;GACf,MAAM,EAAE,MAAM,oBAAoB;GAClC,OAAO,CAAC;EACV;;EAEA,SAAS;GACP,MAAM,EAAE,MAAM,kCAAkC;GAChD,OAAO,CAAC;EACV;;;;;EAKA,kBAAkB;GAChB,MAAM,EAAE,OAAO,UAAU;GACzB,OAAO,CAAC;EACV;;;;;EAKA,YAAY;GACV,MAAM,EACJ,OACE,4GACJ;GAGA,OAAO,EACL,OAAO;IACL;IACA;IACA;GACF,EACF;EACF;CACF;CACA,iBAAiB;EACf,UAAU;EACV,YAAY;EACZ,eAAe;EACf,SAAS;EACT,kBAAkB;CACpB;AACF,CAAC;;AAGD,MAAa,sBAAsB,GAAG;CACpC,OAAO;EACL,MAAM;EACN,MAAM;CACR;CACA,UAAU;EACR,aAAa;GACX,OAAO,EAAE,MAAM,iBAAiB;GAChC,UAAU;IAAE,MAAM;IAAkB,MAAM;GAAiB;EAC7D;EACA,eAAe;GAAE,KAAK,CAAC;GAAG,OAAO,CAAC;EAAE;EACpC,cAAc;GAGZ,MAAM,EACJ,MAAM;IACJ;IACA;IACA;IACA;GACF,EACF;GACA,OAAO,CAAC;EACV;CACF;CACA,kBAAkB,CAEhB;EAAE,aAAa;EAAY,eAAe;EAAS,OAAO,EAAE,MAAM,0BAA0B;CAAE,CAChG;CACA,iBAAiB;EAAE,aAAa;EAAS,eAAe;EAAO,cAAc;CAAM;AACrF,CAAC"}
|
|
@@ -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,
|
|
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(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
className:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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":";;;;;;;;;;;;;;;;AA0CA,MAAa,sBAAsB,cAA4C,IAAI;;AAgLnF,MAAM,YAAY;;AAElB,MAAM,eAAe;AAErB,MAAM,YACJ;;;;;;;;;;;;AAaF,SAAgB,UAAU,OAAuB;CAC/C,MAAM,EACJ,OACA,cACA,UACA,UACA,UACA,iBACA,kBACA,MAAM,gBAAgB,QACtB,aACA,aACA,cACA,YAAY,SACZ,qBACA,UACA,UACA,YAAY,oBACZ,KACA,WACA,OACA,YACA,GAAG,SACD;CACJ,MAAM,EAAE,cAAc,OAAO,mBAAmB,eAAe;CAK/D,MAAM,UAAU,cAAc,cAAc;CAC5C,MAAM,OAAO,kBAAkB,SAAU,UAAU,gBAAgB,WAAY;CAC/E,MAAM,CAAC,OAAO,YAAY,mBAAmB,OAAO,gBAAgB,IAAI,QAAQ;CAChF,MAAM,QAAQ,uBAAuB;EACnC,QAAQ;EACR,aAAa;EACb,cAAc;CAChB,CAAC;CACD,MAAM,SAAS,WAAW,mBAAmB;CAC7C,MAAM,UAAU,aAAa,GAAG;CAChC,MAAM,CAAC,MAAM,WAAW,SAAyB,IAAI;CAGrD,MAAM,eAAe,iBAAiB;CACtC,MAAM,EAAE,WAAW,WAAW,cAAc,YAC1C,MAAM,QACN,eAAe,YAAY,sBAC7B;CAOA,MAAM,eAAe,OAAyB,IAAI;CAClD,MAAM,oBAAoB,cAClB,UAAU,cAAc,YAAY,KAAA,IAAY,QAAQ,GAC9D,CAAC,WAAW,QAAQ,CACtB;CAGA,MAAM,aAAa,OAA2B,IAAI;CAClD,sBAAsB;EACpB,MAAM,MAAM,aAAa,SAAS;EAClC,IAAI,CAAC,MAAM,UAAU,CAAC,KAAK;EAC3B,MAAM,SAAS,IAAI;EACnB,WAAW,UAAU,kBAAkB,eAAe,WAAW,IAAI,OAAO,SAAS;CACvF,GAAG,CAAC,MAAM,MAAM,CAAC;CACjB,gBAAgB;EACd,MAAM,YAAY,aAAa;EAC/B,IAAI,CAAC,aAAa,CAAC,WAAW;EAE9B,IAAI,CAAC,UAAU,cAAc,eAAe,QAAQ,gBAAgB,GAAG;EACvE,MAAM,WAAW,WAAW;EAG5B,CADE,UAAU,eAAe,CAAC,SAAS,QAAQ,gBAAgB,IAAI,WAAW,UAAA,CACrE,MAAM,EAAE,eAAe,KAAK,CAAC;CACtC,GAAG,CAAC,SAAS,CAAC;CAGd,sBAAsB;EACpB,MAAM,OAAO,QAAQ;EACrB,IAAI,CAAC,MAAM,UAAU,CAAC,MAAM;EAC5B,MAAM,MAAM,KAAK,sBAAsB;EACvC,QAAQ;GACN,KAAK,IAAI;GACT,MAAM,IAAI;GACV,OAAO,IAAI;GACX,QAAQ,IAAI;GACZ,OAAO,IAAI;GACX,QAAQ,IAAI;GACZ,KAAK,iBAAiB,IAAI,CAAC,CAAC,cAAc;EAC5C,CAAC;CACH,GAAG,CAAC,MAAM,QAAQ,OAAO,CAAC;CAE1B,MAAM,YAAgC;EAAE,UAAU,MAAM;EAAQ,UAAU,MAAM;EAAO;CAAK;CAC5F,MAAM,UAAU,OAAO,gBAAgB,aAAa,YAAY,SAAS,IAAI;CAC7E,MAAM,WAAW,OAAO,iBAAiB,aAAa,aAAa,SAAS,IAAI;CAChF,MAAM,SAAS,gBAAgB;EAC7B,UAAU,UAAU;EACpB,YAAY,WAAW;EACvB,aAAa,YAAY;CAC3B,CAAC;CACD,MAAM,EAAE,MAAM,MAAM,MAAM,QAAQ,eAAe,MAAM,EAAE,SAAS,KAAK,CAAC;CAExE,MAAM,EAAE,oBAAoB,aAAa,gBAAgB,YAAY;CACrE,MAAM,mBACJ,CAAC,aAAa,qBAAqB,mBAAmB,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,KAAK,KAAA;CAGvF,MAAM,gBAAgB,SAAU,OAAO,WAAW,aAAa,WAAY;CAE3E,MAAM,SAAS,OAAkB,SAC/B,oBAAC,aAAD;EACE,WAAW,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;EACvE,SACE,WAAW,QACT,oBAAC,QAAD;GAAM,aAAU;GAAU,WAAW,OAAO,QAAQ,EAAE,OAAO,YAAY,QAAQ,CAAC;GAC/E,UAAA;EACG,CAAA;EAGV,UACE,YAAY,QACV,oBAAC,QAAD;GAAM,aAAU;GAAW,WAAW,OAAO,SAAS,EAAE,OAAO,YAAY,SAAS,CAAC;GAClF,UAAA;EACG,CAAA;EAIT,UAAA;CACU,CAAA;CAKf,MAAM,oBAAoB,cAAgE;EACxF,GAAG,WACD;GACE,cAAc;GACd,cAAc;GACd,YAAY;GACZ,gBAAgB;GAChB,GAAG,oBAAoB,GAAG;EAC5B,GACA,oBACA,EAAE,oBAAoB,iBAAiB,GACvC,QACF;EACA,IAAI,KAAA;EACJ,MAAM,KAAA;EACN,MAAM,KAAA;EACN,MAAM;EACN,OAAO;EACP;EACA,cAAc;EACd,mBAAmB,SAAS,OAAO,aAAa,KAAA;CAClD;CACA,MAAM,cAAc,UAClB,gBAAgB;EACd,YAAY,WAAW;EACvB,aAAa,YAAY;EACzB,WAAW;CACb,CAAC,CAAC,CAAC,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;CAEvC,MAAM,iBAAiB,UAA2C;EAChE,IAAI,MAAM,QAAQ,WAAW,CAAC,MAAM,YAAY,aAAa;GAC3D,MAAM,eAAe;GACrB,WAAW,KAAK;GAChB,OAAO;EACT;EACA,OAAO;CACT;CAEA,OACE,qBAAC,OAAD;EACE,GAAI;EACJ,IAAK,IAAwB;EAC7B,KAAK;EACE;EACP,iBAAe,MAAM,UAAU,KAAA;EAC/B,aAAW;EACX,WAAW,OAAO,KAAK,EAAE,OAAO,GAAG,YAAY,MAAM,SAAS,EAAE,CAAC;EAPnE,UAAA,CASG,MACC,oBAAC,SAAD;GACE,GAAI,iBAAiB;IACnB,WAAW,UAAyC;KAClD,MAAM,OAAO,MAAM,OAAO;KAE1B,IAAI,KAAK,SAAS,MAAM,QAAQ,MAAM,KAAK;KAC3C,SAAS,IAAI;IACf;IACA,SAAS,MAAM;IAGf,YAAY,UAAU;KAEpB,IADc,MAAM,QAAQ,WAAW,CAAC,MAAM,YAAY,eAC7C,MAAM,QAAQ,aAAa;MACtC,MAAM,eAAe;MACrB,MAAM,KAAK;KACb;IACF;GACF,CAAC;GACD,aAAU;GACV,WAAW,WAAW,SAAS;GAE/B,KAAK;GAIL,OAAO,MAAM;GACb,eAAa,MAAM,UAAU,KAAA;GAE7B,MAAK;GACL,iBAAe,MAAM;GACrB,iBAAc;EACf,CAAA,GACD,aACF,GACC,aAAa,QACZ,oBAACA,WAAD;GAAS,SAAS;GAChB,UAAA,oBAAC,YAAD;IACQ;IACC;IACD;IACN,eAAe,CAAC;IACL;IACA;IACJ;IACK;IACA;IACJ;IACa;IACrB,QAAQ,eACN,MACE,oBAAC,eAAD;KACE,GAAI,iBAAiB;MACnB,WAAW,UAAU,SAAS,MAAM,OAAO,KAAK;MAChD,YAAY,UAAU;OAGpB,IAAI,cAAc,KAAK,GAAG;QACxB,WAAW,SAAS,MAAM;QAC1B;OACF;OACA,IAAI,MAAM,QAAQ,aAAa;QAC7B,MAAM,QAAQ,WAAW,SAAS,cAA2B,SAAS;QACtE,IAAI,OAAO;SACT,MAAM,eAAe;SACrB,MAAM,MAAM;QACd;OACF;MACF;KACF,CAAC;KACD,aAAU;KACV,WAAW,WAAW,OAAO;KACnB;KACV,MAAM,MAAM;IACb,CAAA,GACD,SACF;IAGD;GACS,CAAA;EACL,CAAA,CAER;;AAET;AAEA,SAAS,YAAY,EACnB,WACA,SACA,UACA,YAMC;CAED,MAAM,EAAE,gBAAgB,eAAe,aAAa;EAAE,QAAQ;EAAM,aAAa;CAAK,CAAC;CACvF,OACE,oBAAC,OAAD;EACE,GAAI;EACJ,eAAY;EACZ,sBAAoB,kBAAkB,KAAA;EAC3B;EAGX,UAAA,qBAAC,eAAD;GAAe,OAAO;GAAtB,UAAA;IACG;IACA;IACA;GACY;;CACZ,CAAA;AAET;AAEA,SAAS,cAAc,EACrB,UACA,MACA,GAAG,SAKF;CACD,MAAM,MAAM,aAAa,QAAQ;CAEjC,gBAAgB;EACd,MAAM,QAAQ,IAAI;EAClB,IAAI,CAAC,SAAS,CAAC,MAAM;EACrB,MAAM,MAAM,EAAE,eAAe,KAAK,CAAC;EACnC,MAAM,kBAAkB,MAAM,MAAM,QAAQ,MAAM,MAAM,MAAM;CAChE,GAAG,CAAC,KAAK,IAAI,CAAC;CACd,OAAO,oBAAC,SAAD;EAAO,GAAI;EAAY;CAAM,CAAA;AACtC;AAEA,SAAS,WAAW,EAClB,MACA,OACA,MACA,eACA,WACA,WACA,OACA,YACA,YACA,QACA,OACA,qBACA,YAgBC;CACD,MAAM,WAAW,OAAuB,IAAI;CAC5C,MAAM,aAAa,OAAuB,IAAI;CAC9C,MAAM,iBAAiB,OAAuB,IAAI;CAClD,iBACE,YACA,gBACA,SAAS,YAAY,iBAAiB,YAAY,IACpD;CACA,MAAM,EAAE,YAAY,kBAAkB,gBACpC,EAAE,eAAe,CAAC,UAAU,GAC5B,OACA,QACF;CACA,MAAM,EAAE,gBAAgB,UACtB;EAAE,cAAc;EAAO,mBAAmB,SAAS,OAAO,aAAa,KAAA;CAAU,GACjF,QACF;CACA,MAAM,UAAU,aAAa,KAAA;CAC7B,MAAM,aAAa,WAAW,YAAY,aAAa,EAAE,UAAU,GAAG,CAAC;CAEvE,IAAI,SAAS,eAAe;EAC1B,MAAM,OAAO;GACX,mBAAmB,GAAG,KAAK,IAAI;GAC/B,oBAAoB,GAAG,KAAK,KAAK;GACjC,qBAAqB,GAAG,OAAO,aAAa,KAAK,MAAM;GACvD,sBAAsB,GAAG,OAAO,cAAc,KAAK,OAAO;GAC1D,qBAAqB,GAAG,KAAK,MAAM,OAAO,aAAa,KAAK,QAAQ,KAAK,KAAK;GAC9E,qBAAqB,GAAG,KAAK,MAAM;GACnC,sBAAsB,GAAG,KAAK,OAAO;EACvC;EACA,OACE,qBAAC,OAAD;GACE,GAAI;GACJ,GAAI;GACJ,KAAK;GACL,OAAO;GACP,eAAY;GACZ,aAAU;GACV,gBAAc;GACd,WAAW,OAAO,gBAAgB,EAAE,OAAO,YAAY,KAAK,CAAC;GAR/D,UAAA;IAUE,oBAAC,OAAD;KAAK,gBAAc;KAAS,WAAW,OAAO,iBAAiB;KAC5D,UAAA,MAAM,UAAU;IACd,CAAA;IACL,oBAAC,OAAD;KACE,KAAK;KACL,UAAU;KACV,gBAAc;KACd,WAAW,OAAO,kBAAkB,EAAE,OAAO,YAAY,QAAQ,CAAC;KAEjE;IACE,CAAA;IACL,oBAAC,YAAD,EAAY,SAAS,oBAAsB,CAAA;GACxC;;CAET;CAGA,MAAM,QAAQ,KAAK,IAAI,KAAK,QAAQ,IAAoB,KAAK,IAAI,KAAK,OAAO,SAAS,CAAC;CACvF,MAAM,QAAQ,QAAQ,KAAK,SAAS;CACpC,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,OAAO,MAAM,OAAO,aAAa,KAAK,CAAC;CAG9E,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,SAAS,gBAAgB,CAAC;CAEnE,MAAM,aAAa;EACjB,mBAAmB,GAAG,KAAK,IAAI;EAC/B,oBAAoB,GAAG,KAAK,KAAK;EACjC,qBAAqB,GAAG,KAAK,MAAM;EACnC,sBAAsB,GAAG,KAAK,OAAO;EACrC,kBAAkB,GAAG,OAAO,KAAK,KAAK;EACtC,kBAAkB,GAAG,MAAM,KAAK,IAAI;EACpC,kBAAkB,GAAG,KAAK,IAAI,OAAO,OAAO,UAAU,IAAI,KAAK,MAAM;EACrE,kBAAkB,GAAG,eAAe,KAAK,OAAO;CAClD;CAEA,OACE,qBAAA,YAAA,EAAA,UAAA,CAEE,oBAAC,OAAD;EACE,GAAI;EACJ,eAAY;EACZ,gBAAc;EACd,WAAW,OAAO,MAAM,EAAE,OAAO,YAAY,MAAM,CAAC;CACrD,CAAA,GACD,oBAAC,OAAD;EAAK,GAAI;EAAe,gBAAc;EAAS,WAAW,OAAO,SAAS;EACxE,UAAA,qBAAC,OAAD;GACE,GAAI;GACJ,GAAI;GACJ,KAAK;GACL,OAAO;GACP,gBAAc;GACd,WAAW,OAAO,YAAY,EAAE,OAAO,YAAY,KAAK,CAAC;GAN3D,UAAA;IAQE,oBAAC,OAAD;KAAK,WAAW,OAAO,aAAa;KAAI,UAAA,MAAM,UAAU;IAAO,CAAA;IAC9D,YAAY,QACX,oBAAC,OAAD;KACE,KAAK;KACL,UAAU;KACV,eAAY;KACZ,aAAU;KACV,gBAAc;KACd,WAAW,OAAO,SAAS,EAAE,OAAO,YAAY,QAAQ,CAAC;KAGzD,UAAA,oBAAC,OAAD;MAAK,KAAK;MAAgB,WAAU;MACjC;KACE,CAAA;IACF,CAAA;IAEP,oBAAC,YAAD,EAAY,SAAS,oBAAsB,CAAA;GACxC;;CACF,CAAA,CACL,EAAA,CAAA;AAEN;;;;;;;AAQA,SAAS,iBACP,aACA,YACA,SACA;CACA,sBAAsB;EACpB,MAAM,WAAW,YAAY;EAC7B,MAAM,UAAU,WAAW;EAC3B,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,SAAS;EACvC,IAAI,WAAW;EACf,MAAM,gBAAgB;GACpB,MAAM,QAAQ,iBAAiB,QAAQ;GACvC,MAAM,QACJ,WAAW,MAAM,UAAU,IAC3B,WAAW,MAAM,aAAa,IAC9B,WAAW,MAAM,cAAc,IAC/B,WAAW,MAAM,iBAAiB;GACpC,MAAM,SAAS,QAAQ,sBAAsB,CAAC,CAAC,SAAS;GAGxD,IAAI,KAAK,IAAI,SAAS,QAAQ,IAAI,IAAK;GACvC,WAAW;GACX,SAAS,MAAM,YAAY,8BAA8B,GAAG,OAAO,GAAG;EACxE;EACA,MAAM,YAAY,QAAiB,UAA2B;GAC5D,IAAI,MAAM,WAAW,YAAY,MAAM,iBAAiB,UAAU;GAClE,IAAI,IAAI,SAAS,QAAQ,WAAW;QAC/B,OAAO,SAAS,QAAQ;EAC/B;EACA,MAAM,QAAQ,SAAS,IAAI;EAC3B,MAAM,MAAM,SAAS,KAAK;EAC1B,QAAQ;EACR,SAAS,iBAAiB,iBAAiB,KAAK;EAChD,SAAS,iBAAiB,iBAAiB,GAAG;EAC9C,SAAS,iBAAiB,oBAAoB,GAAG;EACjD,MAAM,WAAW,OAAO,mBAAmB,cAAc,OAAO,IAAI,eAAe,OAAO;EAC1F,UAAU,QAAQ,OAAO;EACzB,aAAa;GACX,UAAU,WAAW;GACrB,SAAS,oBAAoB,iBAAiB,KAAK;GACnD,SAAS,oBAAoB,iBAAiB,GAAG;GACjD,SAAS,oBAAoB,oBAAoB,GAAG;GACpD,SAAS,MAAM,eAAe,4BAA4B;GAC1D,OAAO,SAAS,QAAQ;EAC1B;CACF,GAAG;EAAC;EAAa;EAAY;CAAO,CAAC;AACvC;;;;;AAMA,SAAS,WAAW,EAAE,WAAiC;CACrD,MAAM,CAAC,MAAM,WAAW,SAAS,EAAE;CACnC,gBAAgB;EACd,MAAM,QAAQ,iBAAiB,QAAQ,WAAW,EAAE,GAAG,GAAG;EAC1D,aAAa,aAAa,KAAK;CACjC,GAAG,CAAC,OAAO,CAAC;CACZ,OACE,oBAAC,OAAD;EAAK,MAAK;EAAS,WAAU;EAC1B,UAAA;CACE,CAAA;AAET"}
|
|
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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"}
|