@edgeandnode/eds-react 1.0.2 → 1.1.1
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/README.md +32 -7
- package/dist/EDSContext.d.ts +2 -2
- package/dist/EDSContext.d.ts.map +1 -1
- package/dist/EDSContext.js +1 -1
- package/dist/EDSContext.js.map +1 -1
- package/dist/EDSProvider.d.ts +13 -6
- package/dist/EDSProvider.d.ts.map +1 -1
- package/dist/EDSProvider.js +48 -63
- package/dist/EDSProvider.js.map +1 -1
- package/dist/components/Avatar.d.ts +3 -3
- package/dist/components/Avatar.d.ts.map +1 -1
- package/dist/components/Avatar.js.map +1 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +1 -1
- package/dist/components/Button.js.map +1 -1
- package/dist/components/Card.d.ts +8 -4
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/Card.js +2 -1
- package/dist/components/Card.js.map +1 -1
- package/dist/components/Keyboard.js +1 -1
- package/dist/components/LocalTheme.d.ts +21 -8
- package/dist/components/LocalTheme.d.ts.map +1 -1
- package/dist/components/LocalTheme.js +10 -4
- package/dist/components/LocalTheme.js.map +1 -1
- package/dist/components/Menu.parts.d.ts +43 -9
- package/dist/components/Menu.parts.d.ts.map +1 -1
- package/dist/components/Menu.parts.js +118 -114
- package/dist/components/Menu.parts.js.map +1 -1
- package/dist/components/Modal.parts.d.ts +1 -1
- package/dist/components/Modal.parts.d.ts.map +1 -1
- package/dist/components/Modal.parts.js +2 -2
- package/dist/components/Modal.parts.js.map +1 -1
- package/dist/components/Pane.parts.d.ts +23 -22
- package/dist/components/Pane.parts.d.ts.map +1 -1
- package/dist/components/Pane.parts.js +3 -3
- package/dist/components/Pane.parts.js.map +1 -1
- package/dist/components/Prose.d.ts +1 -1
- package/dist/components/Prose.d.ts.map +1 -1
- package/dist/components/Prose.js +3 -3
- package/dist/components/Prose.js.map +1 -1
- package/dist/components/Prose.meta.d.ts +5 -0
- package/dist/components/Prose.meta.d.ts.map +1 -1
- package/dist/components/Prose.meta.js +10 -0
- package/dist/components/Prose.meta.js.map +1 -1
- package/dist/components/Select.parts.d.ts +8 -3
- package/dist/components/Select.parts.d.ts.map +1 -1
- package/dist/components/Select.parts.js +26 -17
- package/dist/components/Select.parts.js.map +1 -1
- package/dist/components/ThemeSwitcher.d.ts +5 -5
- package/dist/components/ThemeSwitcher.d.ts.map +1 -1
- package/dist/components/ThemeSwitcher.js +5 -4
- package/dist/components/ThemeSwitcher.js.map +1 -1
- package/dist/components/base/MaybeButtonOrLink.d.ts +7 -5
- package/dist/components/base/MaybeButtonOrLink.d.ts.map +1 -1
- package/dist/components/base/MaybeButtonOrLink.js +7 -2
- package/dist/components/base/MaybeButtonOrLink.js.map +1 -1
- package/dist/components/base/Render.d.ts +25 -6
- package/dist/components/base/Render.d.ts.map +1 -1
- package/dist/components/base/Render.js +4 -3
- package/dist/components/base/Render.js.map +1 -1
- package/dist/components/base/index.d.ts +1 -1
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/base/index.js.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/hooks/useEDS.d.ts +16 -8
- package/dist/hooks/useEDS.d.ts.map +1 -1
- package/dist/hooks/useEDS.js +16 -8
- package/dist/hooks/useEDS.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/theme-persistence.d.ts +18 -0
- package/dist/theme-persistence.d.ts.map +1 -0
- package/dist/theme-persistence.js +25 -0
- package/dist/theme-persistence.js.map +1 -0
- package/package.json +20 -20
- package/skill/SKILL.md +91 -57
- package/skill/references/tokens.md +1 -1
- package/src/EDSContext.ts +2 -2
- package/src/EDSProvider.tsx +80 -74
- package/src/components/Avatar.tsx +5 -3
- package/src/components/Button.tsx +4 -0
- package/src/components/Card.tsx +14 -8
- package/src/components/Keyboard.tsx +1 -1
- package/src/components/LocalTheme.tsx +22 -14
- package/src/components/Menu.parts.tsx +365 -244
- package/src/components/Modal.parts.tsx +8 -1
- package/src/components/Pane.parts.tsx +31 -26
- package/src/components/Prose.meta.ts +10 -0
- package/src/components/Prose.tsx +4 -3
- package/src/components/Select.parts.tsx +37 -29
- package/src/components/ThemeSwitcher.tsx +11 -13
- package/src/components/base/MaybeButtonOrLink.tsx +11 -6
- package/src/components/base/Render.tsx +28 -4
- package/src/components/base/index.ts +2 -0
- package/src/components/index.ts +8 -1
- package/src/hooks/useEDS.ts +16 -8
- package/src/index.ts +2 -1
- package/src/theme-persistence.ts +31 -0
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { DistributedOmit } from 'type-fest';
|
|
2
|
+
import { type ResolvedTheme } from '../EDSContext.ts';
|
|
3
|
+
import { type AsOrRenderProps, type HTMLElementProps } from './base/Render.tsx';
|
|
4
|
+
export declare namespace LocalThemeProps {
|
|
5
|
+
interface CommonProps extends HTMLElementProps {
|
|
6
|
+
/** The theme to render the content in. */
|
|
7
|
+
theme: ResolvedTheme;
|
|
8
|
+
}
|
|
5
9
|
}
|
|
10
|
+
export type LocalThemeProps = LocalThemeProps.CommonProps & AsOrRenderProps;
|
|
11
|
+
export type DarkProps = DistributedOmit<LocalThemeProps, 'theme'>;
|
|
12
|
+
export type LightProps = DistributedOmit<LocalThemeProps, 'theme'>;
|
|
13
|
+
/**
|
|
14
|
+
* Renders its content in the given theme, whatever the page's theme is. Unlike switching between
|
|
15
|
+
* `Dark` and `Light`, changing `theme` doesn't remount the content. Components that need to know
|
|
16
|
+
* the theme in JavaScript can read it with `useEDS().activeTheme`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function LocalTheme({ theme, ...props }: LocalThemeProps): import("react").JSX.Element;
|
|
6
19
|
/**
|
|
7
20
|
* Renders its content in the dark theme, whatever the page's theme is. Components that need to know
|
|
8
|
-
* the theme in JavaScript can read it with `useEDS().
|
|
21
|
+
* the theme in JavaScript can read it with `useEDS().activeTheme`.
|
|
9
22
|
*/
|
|
10
|
-
export declare function Dark(props:
|
|
23
|
+
export declare function Dark(props: DarkProps): import("react").JSX.Element;
|
|
11
24
|
/**
|
|
12
25
|
* Renders its content in the light theme, whatever the page's theme is. Components that need to
|
|
13
|
-
* know the theme in JavaScript can read it with `useEDS().
|
|
26
|
+
* know the theme in JavaScript can read it with `useEDS().activeTheme`.
|
|
14
27
|
*/
|
|
15
|
-
export declare function Light(props:
|
|
28
|
+
export declare function Light(props: LightProps): import("react").JSX.Element;
|
|
16
29
|
//# sourceMappingURL=LocalTheme.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalTheme.d.ts","sourceRoot":"","sources":["../../src/components/LocalTheme.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LocalTheme.d.ts","sourceRoot":"","sources":["../../src/components/LocalTheme.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhD,OAAO,EAAoC,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACvF,OAAO,EAAU,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEvF,MAAM,CAAC,OAAO,WAAW,eAAe,CAAC;IACvC,UAAU,WAAY,SAAQ,gBAAgB;QAC5C,0CAA0C;QAC1C,KAAK,EAAE,aAAa,CAAA;KACrB;CACF;AAED,MAAM,MAAM,eAAe,GAAG,eAAe,CAAC,WAAW,GAAG,eAAe,CAAA;AAC3E,MAAM,MAAM,SAAS,GAAG,eAAe,CAAC,eAAe,EAAE,OAAO,CAAC,CAAA;AACjE,MAAM,MAAM,UAAU,GAAG,eAAe,CAAC,eAAe,EAAE,OAAO,CAAC,CAAA;AAElE;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,EAAE,eAAe,+BAM9D;AAED;;;GAGG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,+BAEpC;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,+BAEtC"}
|
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { mergeProps } from '@base-ui/react';
|
|
4
4
|
import { LocalThemeContext, THEME_CLASSES } from '../EDSContext.js';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { Render } from './base/Render.js';
|
|
6
|
+
/**
|
|
7
|
+
* Renders its content in the given theme, whatever the page's theme is. Unlike switching between
|
|
8
|
+
* `Dark` and `Light`, changing `theme` doesn't remount the content. Components that need to know
|
|
9
|
+
* the theme in JavaScript can read it with `useEDS().activeTheme`.
|
|
10
|
+
*/
|
|
11
|
+
export function LocalTheme({ theme, ...props }) {
|
|
12
|
+
return (_jsx(LocalThemeContext.Provider, { value: theme, children: _jsx(Render, { ...mergeProps({ className: THEME_CLASSES[theme] }, props) }) }));
|
|
7
13
|
}
|
|
8
14
|
/**
|
|
9
15
|
* Renders its content in the dark theme, whatever the page's theme is. Components that need to know
|
|
10
|
-
* the theme in JavaScript can read it with `useEDS().
|
|
16
|
+
* the theme in JavaScript can read it with `useEDS().activeTheme`.
|
|
11
17
|
*/
|
|
12
18
|
export function Dark(props) {
|
|
13
19
|
return _jsx(LocalTheme, { theme: "dark", ...props });
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
* Renders its content in the light theme, whatever the page's theme is. Components that need to
|
|
17
|
-
* know the theme in JavaScript can read it with `useEDS().
|
|
23
|
+
* know the theme in JavaScript can read it with `useEDS().activeTheme`.
|
|
18
24
|
*/
|
|
19
25
|
export function Light(props) {
|
|
20
26
|
return _jsx(LocalTheme, { theme: "light", ...props });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalTheme.js","sourceRoot":"","sources":["../../src/components/LocalTheme.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;
|
|
1
|
+
{"version":3,"file":"LocalTheme.js","sourceRoot":"","sources":["../../src/components/LocalTheme.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3C,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAsB,MAAM,kBAAkB,CAAA;AACvF,OAAO,EAAE,MAAM,EAA+C,MAAM,kBAAmB,CAAA;AAavF;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,KAAK,EAAE,GAAG,KAAK,EAAmB;IAC7D,OAAO,CACL,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACtC,KAAC,MAAM,OAAK,UAAU,CAAC,EAAE,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,GAAI,GAC3C,CAC9B,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,IAAI,CAAC,KAAgB;IACnC,OAAO,KAAC,UAAU,IAAC,KAAK,EAAC,MAAM,KAAK,KAAK,GAAI,CAAA;AAC/C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,OAAO,KAAC,UAAU,IAAC,KAAK,EAAC,OAAO,KAAK,KAAK,GAAI,CAAA;AAChD,CAAC"}
|
|
@@ -71,6 +71,36 @@ export declare namespace MenuProps {
|
|
|
71
71
|
* @default false
|
|
72
72
|
*/
|
|
73
73
|
highlightCheckedOnOpen?: boolean | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Adds `Menu.Search` to the header and filters items by their text (or `label`) as the user
|
|
76
|
+
* types. A custom `header` must include `Menu.Search`, which handles the keyboard navigation.
|
|
77
|
+
* Pass a function to customize matching, or `'manual'` to filter the items yourself (e.g.
|
|
78
|
+
* results from an API). Defaults to `true` if `searchValue` or `onSearchValueChange` is set.
|
|
79
|
+
*/
|
|
80
|
+
searchable?: boolean | ((item: {
|
|
81
|
+
label: string;
|
|
82
|
+
searchValue: string;
|
|
83
|
+
}) => boolean) | 'manual' | undefined;
|
|
84
|
+
/** The search query, to control it along with `onSearchValueChange`. */
|
|
85
|
+
searchValue?: string | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Called when the search query changes, including when it's reset after the menu closes (then
|
|
88
|
+
* `eventDetails.reason` is `'popup-close'`).
|
|
89
|
+
*/
|
|
90
|
+
onSearchValueChange?: ((searchValue: string, eventDetails: Menu.FilterProvider.ChangeEventDetails) => void) | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Shown in a searchable menu when no item matches. A string is rendered in a `Menu.Empty`, and
|
|
93
|
+
* `null` renders nothing.
|
|
94
|
+
*
|
|
95
|
+
* @default <Menu.Empty />
|
|
96
|
+
*/
|
|
97
|
+
empty?: ReactNode | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Marks the items as loading (e.g. search results from an API). In a searchable menu, a
|
|
100
|
+
* `LoadingIcon` also shows in the search input, or in place of the empty state until a search
|
|
101
|
+
* finishes.
|
|
102
|
+
*/
|
|
103
|
+
loading?: boolean | undefined;
|
|
74
104
|
}
|
|
75
105
|
interface CommonProps extends HTMLProps, OwnProps, EDSComponentProps<typeof MenuMeta> {
|
|
76
106
|
}
|
|
@@ -80,7 +110,7 @@ export declare namespace MenuProps {
|
|
|
80
110
|
}
|
|
81
111
|
}
|
|
82
112
|
export type MenuProps<T extends OptionValue = OptionValue> = MenuProps.NonRadioProps | MenuProps.RadioProps<T>;
|
|
83
|
-
export declare function MenuRoot<T extends OptionValue>({ ref: passedRef, trigger, triggerMode, anchor: passedAnchor, side, gap, align, alignOffset, matchTriggerWidth, matchTriggerHeight, open: controlledOpen, defaultOpen, onOpenChange, header, footer, highlightCheckedOnOpen, label, type, value, defaultValue, onValueChange, children, ...props }: MenuProps<T>): import("react").JSX.Element;
|
|
113
|
+
export declare function MenuRoot<T extends OptionValue>({ ref: passedRef, trigger, triggerMode, anchor: passedAnchor, side, gap, align, alignOffset, matchTriggerWidth, matchTriggerHeight, open: controlledOpen, defaultOpen, onOpenChange, header: passedHeader, footer, highlightCheckedOnOpen, searchable: passedSearchable, searchValue, onSearchValueChange, empty, loading, label, type, value, defaultValue, onValueChange, children, ...props }: MenuProps<T>): import("react").JSX.Element;
|
|
84
114
|
export declare namespace MenuRoot {
|
|
85
115
|
var displayName: string;
|
|
86
116
|
}
|
|
@@ -101,6 +131,11 @@ export declare namespace MenuItemProps {
|
|
|
101
131
|
interface HTMLProps extends Omit<ComponentProps<'button'>, 'defaultChecked' | 'defaultValue' | 'onChange' | 'type' | 'value'> {
|
|
102
132
|
}
|
|
103
133
|
interface OwnProps {
|
|
134
|
+
/**
|
|
135
|
+
* Text that search and typeahead match against. Defaults to the item's rendered text, addons
|
|
136
|
+
* included.
|
|
137
|
+
*/
|
|
138
|
+
label?: string | undefined;
|
|
104
139
|
addonBefore?: AddonValue | undefined;
|
|
105
140
|
addonAfter?: AddonValue | undefined;
|
|
106
141
|
disabled?: boolean | undefined;
|
|
@@ -142,26 +177,25 @@ export declare namespace MenuItemProps {
|
|
|
142
177
|
}
|
|
143
178
|
}
|
|
144
179
|
export type MenuItemProps<T extends OptionValue = OptionValue> = MenuItemProps.PlainProps | MenuItemProps.CheckboxProps | MenuItemProps.RadioProps<T>;
|
|
145
|
-
export declare function MenuItem<T extends OptionValue>({ ref: passedRef, type: passedType, addonBefore: passedAddonBefore, addonAfter: passedAddonAfter, variant, loading, href, target, checked, defaultChecked, onCheckedChange, value: passedValue, disabled: passedDisabled, children, ...props }: MenuItemProps<T>): import("react").JSX.Element;
|
|
180
|
+
export declare function MenuItem<T extends OptionValue>({ ref: passedRef, type: passedType, label, addonBefore: passedAddonBefore, addonAfter: passedAddonAfter, variant, loading, href, target, checked, defaultChecked, onCheckedChange, value: passedValue, disabled: passedDisabled, children, ...props }: MenuItemProps<T>): import("react").JSX.Element;
|
|
146
181
|
export declare namespace MenuItem {
|
|
147
182
|
var displayName: string;
|
|
148
183
|
}
|
|
149
|
-
export interface MenuSearchProps extends Omit<ComponentProps<'input'>, 'children'> {
|
|
184
|
+
export interface MenuSearchProps extends Omit<ComponentProps<'input'>, 'children' | 'value' | 'defaultValue'> {
|
|
150
185
|
/** @default 'Search' */
|
|
151
186
|
placeholder?: string | undefined;
|
|
152
|
-
value?: string | undefined;
|
|
153
|
-
defaultValue?: string | undefined;
|
|
154
|
-
onValueChange?: ((value: string) => void) | undefined;
|
|
155
187
|
}
|
|
156
|
-
|
|
188
|
+
/** Search input for the `header` of a `searchable` menu, which filters its items. */
|
|
189
|
+
export declare function MenuSearch({ placeholder, ...props }: MenuSearchProps): import("react").JSX.Element;
|
|
157
190
|
export declare namespace MenuSearch {
|
|
158
191
|
var displayName: string;
|
|
159
192
|
}
|
|
160
193
|
export interface MenuEmptyProps extends ComponentProps<'div'> {
|
|
161
194
|
}
|
|
162
195
|
/**
|
|
163
|
-
* Renders the standard empty-state treatment for a `Menu
|
|
164
|
-
*
|
|
196
|
+
* Renders the standard empty-state treatment for a `Menu`, e.g. when it has no items. Searchable
|
|
197
|
+
* menus render it when no item matches (see `empty`). Its text wraps instead of widening the menu.
|
|
198
|
+
* Defaults to `"No results"`.
|
|
165
199
|
*/
|
|
166
200
|
export declare function MenuEmpty({ children, ...props }: MenuEmptyProps): import("react").JSX.Element;
|
|
167
201
|
export declare namespace MenuEmpty {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Menu.parts.d.ts","sourceRoot":"","sources":["../../src/components/Menu.parts.tsx"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"Menu.parts.d.ts","sourceRoot":"","sources":["../../src/components/Menu.parts.tsx"],"names":[],"mappings":"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"}
|
|
@@ -8,7 +8,7 @@ import { useMergedRefs } from '@base-ui/utils/useMergedRefs';
|
|
|
8
8
|
import { twToPx } from '@edgeandnode/eds-css';
|
|
9
9
|
import { ArrowRightInteractiveIcon, ArrowUpRightInteractiveIcon, CaretRightIcon, CheckIcon, LoadingIcon, MagnifyingGlassIcon, XIcon, } from '@edgeandnode/eds-react/icons';
|
|
10
10
|
import { useAutoValue, useControlled, useCSSPropsPolyfill, useCSSState, useEDS, } from '../hooks/index.js';
|
|
11
|
-
import { cn, getCSSPropsAttributes,
|
|
11
|
+
import { cn, getCSSPropsAttributes, splitProps } from '../utils/index.js';
|
|
12
12
|
import { renderAddon } from './base/Addon.js';
|
|
13
13
|
import { ButtonOrLink } from './base/ButtonOrLink.js';
|
|
14
14
|
import { Portal } from './base/Portal.js';
|
|
@@ -17,7 +17,9 @@ import { Button } from './Button.js';
|
|
|
17
17
|
import { Checkbox } from './Checkbox.js';
|
|
18
18
|
import { MenuMeta } from './Menu.meta.js';
|
|
19
19
|
const MenuTriggerContext = createContext(false);
|
|
20
|
-
|
|
20
|
+
const MenuSearchableContext = createContext(false);
|
|
21
|
+
const MenuSearchLoadingContext = createContext(false);
|
|
22
|
+
export function MenuRoot({ ref: passedRef, trigger, triggerMode, anchor: passedAnchor, side, gap, align, alignOffset, matchTriggerWidth, matchTriggerHeight, open: controlledOpen, defaultOpen, onOpenChange, header: passedHeader, footer, highlightCheckedOnOpen, searchable: passedSearchable, searchValue, onSearchValueChange, empty, loading, label, type, value, defaultValue, onValueChange, children, ...props }) {
|
|
21
23
|
const { dirProps, localThemeClass } = useEDS();
|
|
22
24
|
const isSubmenu = useContext(MenuGroupContext) !== null;
|
|
23
25
|
const isContextMenu = triggerMode === 'right-click' && !isSubmenu;
|
|
@@ -80,35 +82,43 @@ export function MenuRoot({ ref: passedRef, trigger, triggerMode, anchor: passedA
|
|
|
80
82
|
capturePointerCoords,
|
|
81
83
|
});
|
|
82
84
|
useHighlightCheckedOnOpen(popupRef, open, highlightCheckedOnOpen);
|
|
85
|
+
const searchable = passedSearchable ?? (searchValue !== undefined || onSearchValueChange !== undefined);
|
|
86
|
+
const [onSearchValueChangeAfterClose, reportPendingSearchReset] = useSearchResetAfterClose(onSearchValueChange);
|
|
87
|
+
const header = passedHeader !== undefined ? passedHeader : searchable ? _jsx(MenuSearch, {}) : undefined;
|
|
83
88
|
return (_jsx("span", { "data-has-trigger": hasTrigger || undefined, className: cn(`root-contents [anchor-scope:all]
|
|
84
89
|
not-data-has-trigger:**:data-base-ui-focus-guard:hidden`,
|
|
85
90
|
// Very imperfect workaround for https://bugs.webkit.org/show_bug.cgi?id=301871
|
|
86
91
|
`safari:root-inline
|
|
87
|
-
i:safari:not-data-is-submenu:size-max`), children: _jsxs(MenuRoot, { actionsRef: actionsRef, open: open, onOpenChange: (newOpen) => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
92
|
+
i:safari:not-data-is-submenu:size-max`), children: _jsx(MenuFilter, { searchable: searchable, searchValue: searchValue, onSearchValueChange: onSearchValueChangeAfterClose, children: _jsxs(MenuRoot, { actionsRef: actionsRef, open: open, onOpenChange: (newOpen) => {
|
|
93
|
+
if (newOpen)
|
|
94
|
+
reportPendingSearchReset();
|
|
95
|
+
// A menu with no trigger can still be opened/closed by the consumer, but it should not send events
|
|
96
|
+
if (hasTrigger)
|
|
97
|
+
setOpen(newOpen);
|
|
98
|
+
}, onOpenChangeComplete: (newOpen) => {
|
|
99
|
+
if (!newOpen)
|
|
100
|
+
reportPendingSearchReset();
|
|
101
|
+
}, ...(!isSubmenu && !isContextMenu && { modal: hasTrigger }), closeParentOnEsc: true, children: [_jsx(MenuTriggerContext.Provider, { value: true, children: renderedTriggerElement ?? (_jsx(MenuInlineFallback, { label: label, portalRef: portalRef, rootProps: props })) }), _jsx(Portal, { children: _jsx("span", { ref: cssPropsPolyfillRef, ...mergeProps({ className: cn('eds-menu invisible! block!', cssPropsClassName) }, cssPropsAttributes, cssPropsPolyfillAttributes, dirProps, props), hidden: undefined }) }), _jsx(Menu.Portal, { container: !hasTrigger
|
|
102
|
+
? portalRef
|
|
103
|
+
: /**
|
|
104
|
+
* Specifying `document.body` because while it is the default for top-level menus, a submenu
|
|
105
|
+
* defaults to using the same portal as its parent, which is not desirable when the parent is
|
|
106
|
+
* rendered inline (it can cause the submenu to appear behind other elements).
|
|
107
|
+
*/
|
|
108
|
+
typeof document !== 'undefined'
|
|
109
|
+
? document.body
|
|
110
|
+
: undefined,
|
|
111
|
+
// Inline menus are portalled in place, so they already inherit the local theme
|
|
112
|
+
className: hasTrigger ? localThemeClass : undefined, children: _jsx(Menu.Positioner, { ref: passedRef, anchor: anchor, side: cssProps.side === 'start'
|
|
113
|
+
? 'inline-start'
|
|
114
|
+
: cssProps.side === 'end'
|
|
115
|
+
? 'inline-end'
|
|
116
|
+
: cssProps.side, sideOffset: twToPx(cssProps.gap), align: cssProps.align, alignOffset: twToPx(cssProps.alignOffset), collisionPadding: 8, collisionAvoidance: {
|
|
117
|
+
side: 'flip',
|
|
118
|
+
align: 'flip',
|
|
119
|
+
fallbackAxisSide: cssProps.side === 'start' || cssProps.side === 'end' ? 'end' : 'none',
|
|
120
|
+
}, "data-has-trigger": hasTrigger || undefined, "data-match-trigger-width": cssProps.matchTriggerWidth || undefined, "data-match-trigger-height": cssProps.matchTriggerHeight || undefined, ...mergeProps({
|
|
121
|
+
className: cn(`
|
|
112
122
|
eds-menu root-flex
|
|
113
123
|
[--available-height-but-not-too-small:max(var(--available-height),--spacing(16))]
|
|
114
124
|
[anchor-name:--eds-menu]
|
|
@@ -127,15 +137,49 @@ export function MenuRoot({ ref: passedRef, trigger, triggerMode, anchor: passedA
|
|
|
127
137
|
u:i:data-[match-trigger-width=at-most]:max-w-[min(var(--anchor-width),var(--available-width))]
|
|
128
138
|
u:i:data-[match-trigger-width=true]:w-(--anchor-width)
|
|
129
139
|
`, cssPropsClassName),
|
|
130
|
-
|
|
140
|
+
}, cssPropsAttributes, cssPropsPolyfillAttributes, dirProps, props), render: (renderProps, state) => (_jsxs(_Fragment, { children: [_jsx("div", { ...renderProps, style: hasTrigger ? renderProps.style : undefined }), hasTrigger && state.open && state.anchorHidden && actionsRef.current ? (_jsx(LayoutEffect, { effect: actionsRef.current.close })) : null] })), children: _jsx(Menu.Popup, { ref: popupRef, "data-initial-open": initialOpen.current || undefined, className: "\n flex grow origin-(--transform-origin) flex-col overflow-clip rounded-inherit bg-muted outline-0 transition\n data-ending-style:scale-95\n data-ending-style:opacity-0\n data-starting-style:not-data-initial-open:scale-95\n data-starting-style:not-data-initial-open:opacity-0\n ", onKeyDown: handleMenuPopupKeyDown, children: _jsx(MenuPopupContent, { header: header, footer: footer, empty: empty, loading: loading, children: label !== undefined || type !== undefined ? (type === 'radio' ? (_jsx(MenuGroup, { label: label, type: type, value: value, defaultValue: defaultValue, onValueChange: onValueChange, children: children })) : (_jsx(MenuGroup, { label: label, type: type, children: children }))) : (children) }) }) }) })] }, `${hasTrigger}-${triggerMode}`) }) }));
|
|
131
141
|
}
|
|
132
142
|
MenuRoot.displayName = 'Menu';
|
|
143
|
+
/** Makes the menu filterable when it's `searchable` (see `MenuProps.OwnProps['searchable']`). */
|
|
144
|
+
function MenuFilter({ searchable, searchValue, onSearchValueChange, children, }) {
|
|
145
|
+
const filter = useMemo(() => searchable === 'manual'
|
|
146
|
+
? null
|
|
147
|
+
: typeof searchable === 'function'
|
|
148
|
+
? (label, query) => searchable({ label, searchValue: query })
|
|
149
|
+
: undefined, [searchable]);
|
|
150
|
+
return (_jsx(MenuSearchableContext.Provider, { value: Boolean(searchable), children: searchable ? (_jsx(Menu.FilterProvider, { filter: filter, autoHighlight: true, value: searchValue, onValueChange: onSearchValueChange, children: children })) : (children) }));
|
|
151
|
+
}
|
|
133
152
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
153
|
+
* Holds back the search reset that Base UI reports as the menu starts closing until it has finished
|
|
154
|
+
* closing (or reopens), so items filtered outside of Base UI (e.g. with `searchable="manual"`)
|
|
155
|
+
* don't change while it animates out. Base UI keeps filtering its own items with the previous query
|
|
156
|
+
* until then too.
|
|
136
157
|
*/
|
|
137
|
-
function
|
|
138
|
-
|
|
158
|
+
function useSearchResetAfterClose(onSearchValueChange) {
|
|
159
|
+
const pendingResetRef = useRef(null);
|
|
160
|
+
const onSearchValueChangeAfterClose = onSearchValueChange &&
|
|
161
|
+
((searchValue, eventDetails) => {
|
|
162
|
+
if (eventDetails.reason === 'popup-close') {
|
|
163
|
+
pendingResetRef.current = () => onSearchValueChange(searchValue, eventDetails);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
onSearchValueChange(searchValue, eventDetails);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const reportPendingReset = () => {
|
|
170
|
+
pendingResetRef.current?.();
|
|
171
|
+
pendingResetRef.current = null;
|
|
172
|
+
};
|
|
173
|
+
return [onSearchValueChangeAfterClose, reportPendingReset];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Popup chrome: minimum-width spacer, optional header, scrollable items area (with a searchable
|
|
177
|
+
* menu's empty state), optional footer, border overlay.
|
|
178
|
+
*/
|
|
179
|
+
function MenuPopupContent({ header, footer, empty, loading = false, children, }) {
|
|
180
|
+
const searchable = useContext(MenuSearchableContext);
|
|
181
|
+
const [emptyShowsLoading, setEmptyShowsLoading] = useState(false);
|
|
182
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "w-58" }), header ? (_jsx("div", { className: "nested/menu-header u:shrink-0 u:border-b u:border-muted u:p-4", children: _jsx(MenuSearchLoadingContext.Provider, { value: loading && !emptyShowsLoading, children: header }) })) : null, _jsx("div", { className: "grow scroll-p-8 scrollbar-thin overflow-x-clip overflow-y-auto gradient-mask-y", children: _jsxs("div", { className: "overflow-clip p-2", children: [searchable ? (_jsx(Menu.Empty, { children: _jsx(MenuEmptyContent, { empty: empty, loading: loading, onLoadingShownChange: setEmptyShowsLoading }) })) : null, _jsx(Menu.List, { "aria-busy": loading || undefined, className: "outline-0", children: _jsx(MenuGroupContext.Provider, { value: "root", children: children }) })] }) }), footer ? (_jsx("div", { className: "nested/menu-footer u:shrink-0 u:border-t u:border-muted u:p-4", children: footer })) : null, _jsx("div", { className: "pointer-events-none absolute inset-0 rounded-inherit border border-muted" })] }));
|
|
139
183
|
}
|
|
140
184
|
/**
|
|
141
185
|
* Resolves the anchor `Menu.Positioner` should point at, and returns a pointer-capture handler for
|
|
@@ -232,9 +276,28 @@ function renderMenuTriggerElement({ trigger, triggerMode, triggerRef, isSubmenu,
|
|
|
232
276
|
return (_jsx(Menu.Trigger, { ref: triggerRef, openOnHover: triggerMode === 'hover', "data-open-on-hover": triggerMode === 'hover' || undefined, className: "\n u:inherit-position\n u:i:open:data-open-on-hover:state-hover\n u:iii:open:not-data-open-on-hover:state-active\n ", onPointerDown: capturePointerCoords, onPointerEnter: capturePointerCoords, render: renderTrigger }));
|
|
233
277
|
}
|
|
234
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* The content of a searchable menu's `Menu.Empty`, which Base UI only renders while no item
|
|
281
|
+
* matches. Until a search finishes, `empty` would be premature, so `loading` shows a `LoadingIcon`
|
|
282
|
+
* instead (rather than in the search input).
|
|
283
|
+
*/
|
|
284
|
+
function MenuEmptyContent({ empty, loading, onLoadingShownChange, }) {
|
|
285
|
+
const [searchFinished, setSearchFinished] = useState(!loading);
|
|
286
|
+
if (!loading && !searchFinished)
|
|
287
|
+
setSearchFinished(true);
|
|
288
|
+
useLayoutEffect(() => {
|
|
289
|
+
onLoadingShownChange(!searchFinished);
|
|
290
|
+
return () => onLoadingShownChange(false);
|
|
291
|
+
}, [searchFinished, onLoadingShownChange]);
|
|
292
|
+
if (!searchFinished) {
|
|
293
|
+
return (_jsx(MenuEmpty, { children: _jsx("span", { className: "flex h-lh items-center justify-center", children: _jsx(LoadingIcon, {}) }) }));
|
|
294
|
+
}
|
|
295
|
+
return empty === undefined || typeof empty === 'string' ? _jsx(MenuEmpty, { children: empty }) : empty;
|
|
296
|
+
}
|
|
235
297
|
/**
|
|
236
298
|
* Focus the `[data-checked]` item on open so Base UI's composite syncs its active index to it (Base
|
|
237
|
-
* UI has no `initialActiveIndex` API).
|
|
299
|
+
* UI has no `initialActiveIndex` API). In a searchable menu, typing then moves focus back to the
|
|
300
|
+
* search input.
|
|
238
301
|
*/
|
|
239
302
|
function useHighlightCheckedOnOpen(popupRef, open, enabled) {
|
|
240
303
|
useEffect(() => {
|
|
@@ -252,19 +315,12 @@ function useHighlightCheckedOnOpen(popupRef, open, enabled) {
|
|
|
252
315
|
highlightedItem === checkedItem) {
|
|
253
316
|
return;
|
|
254
317
|
}
|
|
255
|
-
const searchInput = popup.querySelector('input[type="search"]');
|
|
256
|
-
const searchWasFocused = searchInput !== null && document.activeElement === searchInput;
|
|
257
318
|
checkedItem.focus({ preventScroll: false });
|
|
258
|
-
if (searchWasFocused)
|
|
259
|
-
searchInput.focus({ preventScroll: true });
|
|
260
319
|
});
|
|
261
320
|
return () => cancelAnimationFrame(frameId);
|
|
262
321
|
}, [popupRef, open, enabled]);
|
|
263
322
|
}
|
|
264
|
-
/**
|
|
265
|
-
* Keydown handler for `Menu.Popup`. Handles Shift + Tab escaping the popup and inline-search-input
|
|
266
|
-
* / composite-item coordination.
|
|
267
|
-
*/
|
|
323
|
+
/** Keydown handler for `Menu.Popup`. Handles Shift + Tab escaping the popup. */
|
|
268
324
|
function handleMenuPopupKeyDown(event) {
|
|
269
325
|
/**
|
|
270
326
|
* Ensure Shift + Tab doesn't close the menu if there is a previous focusable element that is not
|
|
@@ -292,54 +348,6 @@ function handleMenuPopupKeyDown(event) {
|
|
|
292
348
|
if ((previousElement && !previousElementIsMenuItem) ||
|
|
293
349
|
(!activeElementIsMenuItem && previousElementIsMenuItem)) {
|
|
294
350
|
event.preventBaseUIHandler();
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// If there is a search input in the menu...
|
|
299
|
-
const searchInput = event.currentTarget.querySelector('input[type="search"]');
|
|
300
|
-
if (searchInput instanceof HTMLInputElement) {
|
|
301
|
-
// If the user typed some text, ensure it goes in the input instead of Base UI focusing the item that matches
|
|
302
|
-
if ((event.key.length === 1 && event.key !== ' ') ||
|
|
303
|
-
event.key === 'Backspace' ||
|
|
304
|
-
event.key === 'ArrowLeft' ||
|
|
305
|
-
event.key === 'ArrowRight') {
|
|
306
|
-
event.preventBaseUIHandler();
|
|
307
|
-
searchInput.focus();
|
|
308
|
-
}
|
|
309
|
-
else {
|
|
310
|
-
const searchInputIsFocused = document.activeElement === searchInput;
|
|
311
|
-
const menuItems = [...event.currentTarget.querySelectorAll('.eds-menu-item')];
|
|
312
|
-
const highlightedMenuItem = menuItems.find((item) => item.matches('[data-highlighted]'));
|
|
313
|
-
/**
|
|
314
|
-
* If the user pressed the down arrow while the search input is focused and there is a single
|
|
315
|
-
* menu item in the results, move the focus to it because it may already be highlighted, which
|
|
316
|
-
* would prevent Base UI from focusing it.
|
|
317
|
-
*/
|
|
318
|
-
if (event.key === 'ArrowDown' && searchInputIsFocused && menuItems.length === 1) {
|
|
319
|
-
event.preventBaseUIHandler();
|
|
320
|
-
menuItems[0].focus();
|
|
321
|
-
}
|
|
322
|
-
// Inversely, if the user pressed the up arrow and the focus is on the only menu item, move it back to the search input
|
|
323
|
-
else if (event.key === 'ArrowUp' &&
|
|
324
|
-
menuItems.length === 1 &&
|
|
325
|
-
document.activeElement === menuItems[0]) {
|
|
326
|
-
event.preventDefault();
|
|
327
|
-
searchInput.focus();
|
|
328
|
-
}
|
|
329
|
-
// If the user pressed Enter while the search input is focused and a menu item is highlighted, select it
|
|
330
|
-
else if (event.key === 'Enter' && searchInputIsFocused && highlightedMenuItem) {
|
|
331
|
-
highlightedMenuItem.focus();
|
|
332
|
-
window.setTimeout(() => {
|
|
333
|
-
highlightedMenuItem.dispatchEvent(new KeyboardEvent('keydown', {
|
|
334
|
-
key: 'Enter',
|
|
335
|
-
code: 'Enter',
|
|
336
|
-
keyCode: 13,
|
|
337
|
-
which: 13,
|
|
338
|
-
bubbles: true,
|
|
339
|
-
cancelable: true,
|
|
340
|
-
}));
|
|
341
|
-
}, 0);
|
|
342
|
-
}
|
|
343
351
|
}
|
|
344
352
|
}
|
|
345
353
|
}
|
|
@@ -355,10 +363,12 @@ export function MenuGroup({ label, type = 'plain', value, defaultValue, onValueC
|
|
|
355
363
|
if (isNested) {
|
|
356
364
|
throw new Error(`[Menu] Groups cannot be nested, make sure you're not using \`Menu.Group\` in a \`Menu\` with a \`label\` and/or \`type\` prop (implicit group)`);
|
|
357
365
|
}
|
|
358
|
-
return (_jsx(MenuGroupContext.Provider, { value: menuGroupContextValue, children: _jsxs(Menu.Group, { ...mergeProps({
|
|
366
|
+
return (_jsx(MenuGroupContext.Provider, { value: menuGroupContextValue, children: _jsxs(Menu.Group, { ...mergeProps({
|
|
367
|
+
className: cn('u:-m-2 u:border-muted u:p-2 u:has-[~:not([hidden]):not(:empty)]:mb-2 u:has-[~:not([hidden]):not(:empty)]:border-b'),
|
|
368
|
+
}, props), render: type === 'radio' ? (_jsx(Menu.RadioGroup, { value: value, defaultValue: defaultValue, onValueChange: onValueChange })) : (_jsx("div", {})), children: [label ? (_jsx(Menu.GroupLabel, { className: "truncate p-2 text-12 text-muted", children: label })) : null, children] }) }));
|
|
359
369
|
}
|
|
360
370
|
MenuGroup.displayName = 'Menu.Group';
|
|
361
|
-
export function MenuItem({ ref: passedRef, type: passedType, addonBefore: passedAddonBefore, addonAfter: passedAddonAfter, variant, loading, href, target, checked, defaultChecked, onCheckedChange, value: passedValue, disabled: passedDisabled, children, ...props }) {
|
|
371
|
+
export function MenuItem({ ref: passedRef, type: passedType, label, addonBefore: passedAddonBefore, addonAfter: passedAddonAfter, variant, loading, href, target, checked, defaultChecked, onCheckedChange, value: passedValue, disabled: passedDisabled, children, ...props }) {
|
|
362
372
|
const elementRef = useRef(null);
|
|
363
373
|
const elementPassedRef = useMergedRefs(elementRef, passedRef);
|
|
364
374
|
const autoValue = useAutoValue(children);
|
|
@@ -427,18 +437,18 @@ export function MenuItem({ ref: passedRef, type: passedType, addonBefore: passed
|
|
|
427
437
|
...props,
|
|
428
438
|
};
|
|
429
439
|
if (type === 'submenu-trigger') {
|
|
430
|
-
return _jsx(Menu.SubmenuTrigger, { nativeButton: true, disabled: disabled, render: renderItem(baseProps) });
|
|
440
|
+
return (_jsx(Menu.SubmenuTrigger, { nativeButton: true, label: label, disabled: disabled, render: renderItem(baseProps) }));
|
|
431
441
|
}
|
|
432
442
|
if (type === 'checkbox') {
|
|
433
|
-
return (_jsx(CheckboxItem, { ...baseProps, nativeButton: true, checked: checked, defaultChecked: defaultChecked, onCheckedChange: onCheckedChange, disabled: disabled, render: renderItem }));
|
|
443
|
+
return (_jsx(CheckboxItem, { ...baseProps, nativeButton: true, label: label, checked: checked, defaultChecked: defaultChecked, onCheckedChange: onCheckedChange, disabled: disabled, render: renderItem }));
|
|
434
444
|
}
|
|
435
445
|
if (type === 'radio') {
|
|
436
|
-
return (_jsx(Menu.RadioItem, { ...baseProps, nativeButton: true, closeOnClick: true, value: passedValue !== undefined ? passedValue : autoValue, disabled: disabled, render: renderItem }));
|
|
446
|
+
return (_jsx(Menu.RadioItem, { ...baseProps, nativeButton: true, closeOnClick: true, label: label, value: passedValue !== undefined ? passedValue : autoValue, disabled: disabled, render: renderItem }));
|
|
437
447
|
}
|
|
438
448
|
if (href !== undefined) {
|
|
439
|
-
return (_jsx(Menu.LinkItem, { ...baseProps, closeOnClick: true, href: href, target: target, render: renderItem, children: children }));
|
|
449
|
+
return (_jsx(Menu.LinkItem, { ...baseProps, closeOnClick: true, label: label, href: href, target: target, render: renderItem, children: children }));
|
|
440
450
|
}
|
|
441
|
-
return _jsx(Menu.Item, { ...baseProps, nativeButton: true, disabled: disabled, render: renderItem });
|
|
451
|
+
return (_jsx(Menu.Item, { ...baseProps, nativeButton: true, label: label, disabled: disabled, render: renderItem }));
|
|
442
452
|
}
|
|
443
453
|
MenuItem.displayName = 'Menu.Item';
|
|
444
454
|
function MenuItemAddon({ children, ...props }) {
|
|
@@ -446,30 +456,24 @@ function MenuItemAddon({ children, ...props }) {
|
|
|
446
456
|
className: cn('eds-addon root-flex items-center justify-center u:h-(--eds-addon-size) u:min-w-(--eds-addon-size) u:shrink-0 u:addon-medium'),
|
|
447
457
|
}, props), children: children }));
|
|
448
458
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
459
|
+
/** Search input for the `header` of a `searchable` menu, which filters its items. */
|
|
460
|
+
export function MenuSearch({ placeholder = 'Search', ...props }) {
|
|
461
|
+
if (!useContext(MenuSearchableContext)) {
|
|
462
|
+
throw new Error('[Menu] `Menu.Search` must be used in a `searchable` menu');
|
|
463
|
+
}
|
|
464
|
+
const loading = useContext(MenuSearchLoadingContext);
|
|
454
465
|
const { rootProps, nestedProps } = splitProps(props);
|
|
455
|
-
return (_jsxs("div", { ...mergeProps({ className: cn('root-flex items-center u:-m-4 u:text-14 u:text-default') }, rootProps), children: [_jsx(MagnifyingGlassIcon, { alt: "", className: "absolute inset-y-0 inset-s-3.5 my-auto size-3.5" }), _jsx("
|
|
456
|
-
className: cn('peer -mb-px h-10 w-0 grow ps-8.5 outline-0'),
|
|
457
|
-
onChange: (event) => setValue(event.target.value),
|
|
458
|
-
}, nestedProps) }), _jsx("div", { className: "me-1.5 flex size-7 shrink-0 items-center justify-center peer-placeholder-shown:hidden", children: _jsx(Button, { variant: "naked", size: "xsmall", tabIndex: -1, onClick: () => {
|
|
459
|
-
const input = inputRef.current;
|
|
460
|
-
if (!input)
|
|
461
|
-
return;
|
|
462
|
-
setInputValue(input, '');
|
|
463
|
-
input.focus();
|
|
464
|
-
}, className: "h-full", children: _jsx(XIcon, { alt: "Clear" }) }) })] }));
|
|
466
|
+
return (_jsxs("div", { ...mergeProps({ className: cn('root-flex items-center u:-m-4 u:text-14 u:text-default') }, rootProps), children: [loading ? (_jsx(LoadingIcon, { className: "absolute inset-y-0 inset-s-3.5 my-auto size-3.5" })) : (_jsx(MagnifyingGlassIcon, { alt: "", className: "absolute inset-y-0 inset-s-3.5 my-auto size-3.5" })), _jsx(Menu.Input, { placeholder: placeholder, "aria-label": placeholder, ...mergeProps({ className: cn('peer -mb-px h-10 w-0 grow ps-8.5 outline-0') }, nestedProps) }), _jsx("div", { className: "me-1.5 flex size-7 shrink-0 items-center justify-center peer-placeholder-shown:hidden", children: _jsx(Menu.Clear, { "aria-hidden": false, render: _jsx(Button, { variant: "naked", size: "xsmall", className: "h-full" }), children: _jsx(XIcon, { alt: "Clear" }) }) })] }));
|
|
465
467
|
}
|
|
466
468
|
MenuSearch.displayName = 'Menu.Search';
|
|
467
469
|
/**
|
|
468
|
-
* Renders the standard empty-state treatment for a `Menu
|
|
469
|
-
*
|
|
470
|
+
* Renders the standard empty-state treatment for a `Menu`, e.g. when it has no items. Searchable
|
|
471
|
+
* menus render it when no item matches (see `empty`). Its text wraps instead of widening the menu.
|
|
472
|
+
* Defaults to `"No results"`.
|
|
470
473
|
*/
|
|
471
474
|
export function MenuEmpty({ children = 'No results', ...props }) {
|
|
472
|
-
|
|
475
|
+
// `contain-inline-size` keeps the text from widening a menu that sizes to fit its content
|
|
476
|
+
return (_jsx("div", { ...mergeProps({ className: cn('p-2 contain-inline-size u:text-16 u:text-muted') }, props), children: children }));
|
|
473
477
|
}
|
|
474
478
|
MenuEmpty.displayName = 'Menu.Empty';
|
|
475
479
|
/**
|