@edgeandnode/eds-react 1.0.1 → 1.1.0
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 +50 -8
- package/dist/EDSContext.d.ts +4 -1
- package/dist/EDSContext.d.ts.map +1 -1
- package/dist/EDSContext.js +7 -0
- 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 +60 -68
- package/dist/EDSProvider.js.map +1 -1
- package/dist/components/Address.d.ts +3 -3
- package/dist/components/Address.d.ts.map +1 -1
- package/dist/components/Address.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 +3 -2
- package/dist/components/Avatar.js.map +1 -1
- package/dist/components/Breadcrumbs.parts.d.ts +7 -6
- package/dist/components/Breadcrumbs.parts.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.parts.js +15 -14
- package/dist/components/Breadcrumbs.parts.js.map +1 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +3 -2
- 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/Cluster.d.ts +3 -3
- package/dist/components/Cluster.d.ts.map +1 -1
- package/dist/components/Cluster.js.map +1 -1
- package/dist/components/CodeBlock.parts.d.ts.map +1 -1
- package/dist/components/CodeBlock.parts.js +16 -20
- package/dist/components/CodeBlock.parts.js.map +1 -1
- package/dist/components/CopyButton.d.ts.map +1 -1
- package/dist/components/CopyButton.js +3 -2
- package/dist/components/CopyButton.js.map +1 -1
- package/dist/components/Icon.d.ts.map +1 -1
- package/dist/components/Icon.js +3 -3
- package/dist/components/Icon.js.map +1 -1
- package/dist/components/Keyboard.js +1 -1
- package/dist/components/LocalTheme.d.ts +29 -0
- package/dist/components/LocalTheme.d.ts.map +1 -0
- package/dist/components/LocalTheme.js +28 -0
- package/dist/components/LocalTheme.js.map +1 -0
- 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 +121 -115
- 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 +22 -11
- 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/SegmentedControl.parts.d.ts.map +1 -1
- package/dist/components/SegmentedControl.parts.js +3 -2
- package/dist/components/SegmentedControl.parts.js.map +1 -1
- package/dist/components/Select.parts.d.ts +12 -7
- package/dist/components/Select.parts.d.ts.map +1 -1
- package/dist/components/Select.parts.js +28 -18
- package/dist/components/Select.parts.js.map +1 -1
- package/dist/components/TabSet.parts.d.ts.map +1 -1
- package/dist/components/TabSet.parts.js +3 -2
- package/dist/components/TabSet.parts.js.map +1 -1
- package/dist/components/Table.parts.d.ts.map +1 -1
- package/dist/components/Table.parts.js +3 -4
- package/dist/components/Table.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 +6 -5
- package/dist/components/ThemeSwitcher.js.map +1 -1
- package/dist/components/Tooltip.parts.js +2 -2
- package/dist/components/Tooltip.parts.js.map +1 -1
- package/dist/components/base/Addon.d.ts.map +1 -1
- package/dist/components/base/Addon.js +2 -4
- package/dist/components/base/Addon.js.map +1 -1
- package/dist/components/base/ButtonOrLink.parts.d.ts.map +1 -1
- package/dist/components/base/ButtonOrLink.parts.js +2 -1
- package/dist/components/base/ButtonOrLink.parts.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 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/hooks/useCSSProp.js +1 -1
- package/dist/hooks/useCSSProp.js.map +1 -1
- package/dist/hooks/useEDS.d.ts +24 -1
- package/dist/hooks/useEDS.d.ts.map +1 -1
- package/dist/hooks/useEDS.js +26 -2
- package/dist/hooks/useEDS.js.map +1 -1
- package/dist/hooks/useStyleObserver.d.ts.map +1 -1
- package/dist/hooks/useStyleObserver.js +2 -1
- package/dist/hooks/useStyleObserver.js.map +1 -1
- package/dist/icons/CalendarDynamicIcon.d.ts.map +1 -1
- package/dist/icons/CalendarDynamicIcon.js +3 -1
- package/dist/icons/CalendarDynamicIcon.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/dist/utils/getReactNodeKey.d.ts.map +1 -1
- package/dist/utils/getReactNodeKey.js +31 -13
- package/dist/utils/getReactNodeKey.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/internal/getTooltipProps.d.ts +9 -0
- package/dist/utils/internal/getTooltipProps.d.ts.map +1 -0
- package/dist/utils/internal/getTooltipProps.js +18 -0
- package/dist/utils/internal/getTooltipProps.js.map +1 -0
- package/dist/utils/internal/index.d.ts +4 -0
- package/dist/utils/internal/index.d.ts.map +1 -0
- package/dist/utils/internal/index.js +4 -0
- package/dist/utils/internal/index.js.map +1 -0
- package/dist/utils/internal/resolveLazy.d.ts +29 -0
- package/dist/utils/internal/resolveLazy.d.ts.map +1 -0
- package/dist/utils/internal/resolveLazy.js +40 -0
- package/dist/utils/internal/resolveLazy.js.map +1 -0
- package/dist/utils/internal/styleScheduler.d.ts.map +1 -0
- package/dist/utils/internal/styleScheduler.js.map +1 -0
- package/dist/utils/isComponentType.d.ts +9 -0
- package/dist/utils/isComponentType.d.ts.map +1 -0
- package/dist/utils/isComponentType.js +22 -0
- package/dist/utils/isComponentType.js.map +1 -0
- package/dist/utils/isReactNode.d.ts +7 -0
- package/dist/utils/isReactNode.d.ts.map +1 -1
- package/dist/utils/isReactNode.js +11 -0
- package/dist/utils/isReactNode.js.map +1 -1
- package/dist/utils/reactNodeToString.d.ts +3 -0
- package/dist/utils/reactNodeToString.d.ts.map +1 -1
- package/dist/utils/reactNodeToString.js +6 -0
- package/dist/utils/reactNodeToString.js.map +1 -1
- package/dist/utils/styleObserver.js +1 -1
- package/dist/utils/trimReactNode.d.ts +3 -0
- package/dist/utils/trimReactNode.d.ts.map +1 -1
- package/dist/utils/trimReactNode.js +6 -0
- package/dist/utils/trimReactNode.js.map +1 -1
- package/package.json +24 -23
- package/scripts/update-skill.cjs +18 -5
- package/skill/SKILL.md +95 -57
- package/skill/references/tokens.md +2 -8
- package/src/EDSContext.ts +10 -1
- package/src/EDSProvider.tsx +111 -80
- package/src/components/Address.tsx +4 -3
- package/src/components/Avatar.tsx +10 -5
- package/src/components/Breadcrumbs.parts.tsx +35 -18
- package/src/components/Button.tsx +7 -2
- package/src/components/Card.tsx +14 -8
- package/src/components/Cluster.tsx +5 -3
- package/src/components/CodeBlock.parts.tsx +20 -24
- package/src/components/CopyButton.tsx +3 -2
- package/src/components/Icon.tsx +3 -9
- package/src/components/Keyboard.tsx +1 -1
- package/src/components/LocalTheme.tsx +47 -0
- package/src/components/Menu.parts.tsx +368 -245
- package/src/components/Modal.parts.tsx +28 -11
- 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/SegmentedControl.parts.tsx +3 -8
- package/src/components/Select.parts.tsx +43 -34
- package/src/components/TabSet.parts.tsx +3 -5
- package/src/components/Table.parts.tsx +3 -3
- package/src/components/ThemeSwitcher.tsx +13 -15
- package/src/components/Tooltip.parts.tsx +2 -2
- package/src/components/base/Addon.tsx +3 -4
- package/src/components/base/ButtonOrLink.parts.tsx +2 -1
- 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 -0
- package/src/hooks/useCSSProp.ts +1 -1
- package/src/hooks/useEDS.ts +26 -2
- package/src/hooks/useStyleObserver.ts +2 -1
- package/src/icons/CalendarDynamicIcon.tsx +1 -0
- package/src/index.ts +2 -1
- package/src/theme-persistence.ts +31 -0
- package/src/utils/getReactNodeKey.ts +30 -14
- package/src/utils/index.ts +1 -1
- package/src/utils/internal/getTooltipProps.ts +25 -0
- package/src/utils/internal/index.ts +3 -0
- package/src/utils/internal/resolveLazy.ts +44 -0
- package/src/utils/isComponentType.ts +27 -0
- package/src/utils/isReactNode.ts +11 -0
- package/src/utils/reactNodeToString.ts +7 -0
- package/src/utils/styleObserver.ts +1 -1
- package/src/utils/trimReactNode.tsx +7 -0
- package/dist/utils/styleScheduler.d.ts.map +0 -1
- package/dist/utils/styleScheduler.js.map +0 -1
- package/src/__snapshots__/compiled.css +0 -22499
- package/styles/tailwind.storybook.css +0 -19
- /package/dist/utils/{styleScheduler.d.ts → internal/styleScheduler.d.ts} +0 -0
- /package/dist/utils/{styleScheduler.js → internal/styleScheduler.js} +0 -0
- /package/src/utils/{styleScheduler.ts → internal/styleScheduler.ts} +0 -0
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
useMemo,
|
|
10
10
|
useRef,
|
|
11
11
|
useState,
|
|
12
|
-
type ChangeEvent,
|
|
13
12
|
type ComponentProps,
|
|
14
13
|
type KeyboardEvent,
|
|
15
14
|
type PointerEvent,
|
|
@@ -41,13 +40,7 @@ import {
|
|
|
41
40
|
useCSSState,
|
|
42
41
|
useEDS,
|
|
43
42
|
} from '../hooks/index.ts'
|
|
44
|
-
import {
|
|
45
|
-
cn,
|
|
46
|
-
getCSSPropsAttributes,
|
|
47
|
-
setInputValue,
|
|
48
|
-
splitProps,
|
|
49
|
-
type OptionValue,
|
|
50
|
-
} from '../utils/index.ts'
|
|
43
|
+
import { cn, getCSSPropsAttributes, splitProps, type OptionValue } from '../utils/index.ts'
|
|
51
44
|
import { renderAddon, type AddonValue } from './base/Addon.tsx'
|
|
52
45
|
import { ButtonOrLink, type ButtonOrLinkProps } from './base/ButtonOrLink.tsx'
|
|
53
46
|
import { Portal } from './base/Portal.tsx'
|
|
@@ -62,6 +55,8 @@ import { Checkbox } from './Checkbox.tsx'
|
|
|
62
55
|
import { MenuMeta, type MenuItemMeta } from './Menu.meta.ts'
|
|
63
56
|
|
|
64
57
|
const MenuTriggerContext = createContext<boolean>(false)
|
|
58
|
+
const MenuSearchableContext = createContext<boolean>(false)
|
|
59
|
+
const MenuSearchLoadingContext = createContext<boolean>(false)
|
|
65
60
|
|
|
66
61
|
type MenuItemType = 'plain' | 'checkbox' | 'radio'
|
|
67
62
|
|
|
@@ -131,6 +126,39 @@ export declare namespace MenuProps {
|
|
|
131
126
|
* @default false
|
|
132
127
|
*/
|
|
133
128
|
highlightCheckedOnOpen?: boolean | undefined
|
|
129
|
+
/**
|
|
130
|
+
* Adds `Menu.Search` to the header and filters items by their text (or `label`) as the user
|
|
131
|
+
* types. A custom `header` must include `Menu.Search`, which handles the keyboard navigation.
|
|
132
|
+
* Pass a function to customize matching, or `'manual'` to filter the items yourself (e.g.
|
|
133
|
+
* results from an API). Defaults to `true` if `searchValue` or `onSearchValueChange` is set.
|
|
134
|
+
*/
|
|
135
|
+
searchable?:
|
|
136
|
+
| boolean
|
|
137
|
+
| ((item: { label: string; searchValue: string }) => boolean)
|
|
138
|
+
| 'manual'
|
|
139
|
+
| undefined
|
|
140
|
+
/** The search query, to control it along with `onSearchValueChange`. */
|
|
141
|
+
searchValue?: string | undefined
|
|
142
|
+
/**
|
|
143
|
+
* Called when the search query changes, including when it's reset after the menu closes (then
|
|
144
|
+
* `eventDetails.reason` is `'popup-close'`).
|
|
145
|
+
*/
|
|
146
|
+
onSearchValueChange?:
|
|
147
|
+
| ((searchValue: string, eventDetails: Menu.FilterProvider.ChangeEventDetails) => void)
|
|
148
|
+
| undefined
|
|
149
|
+
/**
|
|
150
|
+
* Shown in a searchable menu when no item matches. A string is rendered in a `Menu.Empty`, and
|
|
151
|
+
* `null` renders nothing.
|
|
152
|
+
*
|
|
153
|
+
* @default <Menu.Empty />
|
|
154
|
+
*/
|
|
155
|
+
empty?: ReactNode | undefined
|
|
156
|
+
/**
|
|
157
|
+
* Marks the items as loading (e.g. search results from an API). In a searchable menu, a
|
|
158
|
+
* `LoadingIcon` also shows in the search input, or in place of the empty state until a search
|
|
159
|
+
* finishes.
|
|
160
|
+
*/
|
|
161
|
+
loading?: boolean | undefined
|
|
134
162
|
}
|
|
135
163
|
interface CommonProps extends HTMLProps, OwnProps, EDSComponentProps<typeof MenuMeta> {}
|
|
136
164
|
interface NonRadioProps extends CommonProps, GroupProps.NonRadioProps {}
|
|
@@ -155,9 +183,14 @@ export function MenuRoot<T extends OptionValue>({
|
|
|
155
183
|
open: controlledOpen,
|
|
156
184
|
defaultOpen,
|
|
157
185
|
onOpenChange,
|
|
158
|
-
header,
|
|
186
|
+
header: passedHeader,
|
|
159
187
|
footer,
|
|
160
188
|
highlightCheckedOnOpen,
|
|
189
|
+
searchable: passedSearchable,
|
|
190
|
+
searchValue,
|
|
191
|
+
onSearchValueChange,
|
|
192
|
+
empty,
|
|
193
|
+
loading,
|
|
161
194
|
label,
|
|
162
195
|
type,
|
|
163
196
|
value,
|
|
@@ -166,7 +199,7 @@ export function MenuRoot<T extends OptionValue>({
|
|
|
166
199
|
children,
|
|
167
200
|
...props
|
|
168
201
|
}: MenuProps<T>) {
|
|
169
|
-
const { dirProps } = useEDS()
|
|
202
|
+
const { dirProps, localThemeClass } = useEDS()
|
|
170
203
|
|
|
171
204
|
const isSubmenu = useContext(MenuGroupContext) !== null
|
|
172
205
|
const isContextMenu = triggerMode === 'right-click' && !isSubmenu
|
|
@@ -247,6 +280,12 @@ export function MenuRoot<T extends OptionValue>({
|
|
|
247
280
|
|
|
248
281
|
useHighlightCheckedOnOpen(popupRef, open, highlightCheckedOnOpen)
|
|
249
282
|
|
|
283
|
+
const searchable =
|
|
284
|
+
passedSearchable ?? (searchValue !== undefined || onSearchValueChange !== undefined)
|
|
285
|
+
const [onSearchValueChangeAfterClose, reportPendingSearchReset] =
|
|
286
|
+
useSearchResetAfterClose(onSearchValueChange)
|
|
287
|
+
const header = passedHeader !== undefined ? passedHeader : searchable ? <MenuSearch /> : undefined
|
|
288
|
+
|
|
250
289
|
return (
|
|
251
290
|
/**
|
|
252
291
|
* This wrapper element ensures a `Menu` always renders a single element, unlike Base UI's which
|
|
@@ -264,76 +303,87 @@ export function MenuRoot<T extends OptionValue>({
|
|
|
264
303
|
i:safari:not-data-is-submenu:size-max`,
|
|
265
304
|
)}
|
|
266
305
|
>
|
|
267
|
-
<
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
onOpenChange={(newOpen) => {
|
|
272
|
-
// A menu with no trigger can still be opened/closed by the consumer, but it should not send events
|
|
273
|
-
if (hasTrigger) setOpen(newOpen)
|
|
274
|
-
}}
|
|
275
|
-
{...(!isSubmenu && !isContextMenu && { modal: hasTrigger })}
|
|
276
|
-
closeParentOnEsc
|
|
306
|
+
<MenuFilter
|
|
307
|
+
searchable={searchable}
|
|
308
|
+
searchValue={searchValue}
|
|
309
|
+
onSearchValueChange={onSearchValueChangeAfterClose}
|
|
277
310
|
>
|
|
278
|
-
<
|
|
279
|
-
{
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
props,
|
|
293
|
-
)}
|
|
294
|
-
hidden={undefined}
|
|
295
|
-
/>
|
|
296
|
-
</Portal>
|
|
297
|
-
<Menu.Portal
|
|
298
|
-
container={
|
|
299
|
-
!hasTrigger
|
|
300
|
-
? portalRef
|
|
301
|
-
: /**
|
|
302
|
-
* Specifying `document.body` because while it is the default for top-level menus, a submenu
|
|
303
|
-
* defaults to using the same portal as its parent, which is not desirable when the parent is
|
|
304
|
-
* rendered inline (it can cause the submenu to appear behind other elements).
|
|
305
|
-
*/
|
|
306
|
-
typeof document !== 'undefined'
|
|
307
|
-
? document.body
|
|
308
|
-
: undefined
|
|
309
|
-
}
|
|
311
|
+
<MenuRoot
|
|
312
|
+
key={`${hasTrigger}-${triggerMode}`}
|
|
313
|
+
actionsRef={actionsRef}
|
|
314
|
+
open={open}
|
|
315
|
+
onOpenChange={(newOpen) => {
|
|
316
|
+
if (newOpen) reportPendingSearchReset()
|
|
317
|
+
// A menu with no trigger can still be opened/closed by the consumer, but it should not send events
|
|
318
|
+
if (hasTrigger) setOpen(newOpen)
|
|
319
|
+
}}
|
|
320
|
+
onOpenChangeComplete={(newOpen) => {
|
|
321
|
+
if (!newOpen) reportPendingSearchReset()
|
|
322
|
+
}}
|
|
323
|
+
{...(!isSubmenu && !isContextMenu && { modal: hasTrigger })}
|
|
324
|
+
closeParentOnEsc
|
|
310
325
|
>
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
326
|
+
<MenuTriggerContext.Provider value={true}>
|
|
327
|
+
{renderedTriggerElement ?? (
|
|
328
|
+
<MenuInlineFallback label={label} portalRef={portalRef} rootProps={props} />
|
|
329
|
+
)}
|
|
330
|
+
</MenuTriggerContext.Provider>
|
|
331
|
+
{/* Ensure changes to CSS props are detected even when the menu is not mounted */}
|
|
332
|
+
<Portal>
|
|
333
|
+
<span
|
|
334
|
+
ref={cssPropsPolyfillRef}
|
|
335
|
+
{...mergeProps(
|
|
336
|
+
{ className: cn('eds-menu invisible! block!', cssPropsClassName) },
|
|
337
|
+
cssPropsAttributes,
|
|
338
|
+
cssPropsPolyfillAttributes,
|
|
339
|
+
dirProps,
|
|
340
|
+
props,
|
|
341
|
+
)}
|
|
342
|
+
hidden={undefined}
|
|
343
|
+
/>
|
|
344
|
+
</Portal>
|
|
345
|
+
<Menu.Portal
|
|
346
|
+
container={
|
|
347
|
+
!hasTrigger
|
|
348
|
+
? portalRef
|
|
349
|
+
: /**
|
|
350
|
+
* Specifying `document.body` because while it is the default for top-level menus, a submenu
|
|
351
|
+
* defaults to using the same portal as its parent, which is not desirable when the parent is
|
|
352
|
+
* rendered inline (it can cause the submenu to appear behind other elements).
|
|
353
|
+
*/
|
|
354
|
+
typeof document !== 'undefined'
|
|
355
|
+
? document.body
|
|
356
|
+
: undefined
|
|
320
357
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
{
|
|
336
|
-
|
|
358
|
+
// Inline menus are portalled in place, so they already inherit the local theme
|
|
359
|
+
className={hasTrigger ? localThemeClass : undefined}
|
|
360
|
+
>
|
|
361
|
+
<Menu.Positioner
|
|
362
|
+
ref={passedRef}
|
|
363
|
+
anchor={anchor}
|
|
364
|
+
side={
|
|
365
|
+
cssProps.side === 'start'
|
|
366
|
+
? 'inline-start'
|
|
367
|
+
: cssProps.side === 'end'
|
|
368
|
+
? 'inline-end'
|
|
369
|
+
: cssProps.side
|
|
370
|
+
}
|
|
371
|
+
sideOffset={twToPx(cssProps.gap)}
|
|
372
|
+
align={cssProps.align}
|
|
373
|
+
alignOffset={twToPx(cssProps.alignOffset)}
|
|
374
|
+
collisionPadding={8}
|
|
375
|
+
collisionAvoidance={{
|
|
376
|
+
side: 'flip',
|
|
377
|
+
align: 'flip',
|
|
378
|
+
fallbackAxisSide:
|
|
379
|
+
cssProps.side === 'start' || cssProps.side === 'end' ? 'end' : 'none',
|
|
380
|
+
}}
|
|
381
|
+
data-has-trigger={hasTrigger || undefined}
|
|
382
|
+
data-match-trigger-width={cssProps.matchTriggerWidth || undefined}
|
|
383
|
+
data-match-trigger-height={cssProps.matchTriggerHeight || undefined}
|
|
384
|
+
{...mergeProps(
|
|
385
|
+
{
|
|
386
|
+
className: cn(`
|
|
337
387
|
eds-menu root-flex
|
|
338
388
|
[--available-height-but-not-too-small:max(var(--available-height),--spacing(16))]
|
|
339
389
|
[anchor-name:--eds-menu]
|
|
@@ -352,83 +402,153 @@ export function MenuRoot<T extends OptionValue>({
|
|
|
352
402
|
u:i:data-[match-trigger-width=at-most]:max-w-[min(var(--anchor-width),var(--available-width))]
|
|
353
403
|
u:i:data-[match-trigger-width=true]:w-(--anchor-width)
|
|
354
404
|
`, cssPropsClassName),
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
405
|
+
},
|
|
406
|
+
cssPropsAttributes,
|
|
407
|
+
cssPropsPolyfillAttributes,
|
|
408
|
+
dirProps,
|
|
409
|
+
props,
|
|
410
|
+
)}
|
|
411
|
+
render={(renderProps, state) => (
|
|
412
|
+
<>
|
|
413
|
+
<div {...renderProps} style={hasTrigger ? renderProps.style : undefined} />
|
|
414
|
+
{/**
|
|
415
|
+
* The `data-anchor-hidden:hidden` class above hides the menu if it's open when the trigger gets
|
|
416
|
+
* hidden, but this actually closes it to restore pointer interactions outside the menu (though the
|
|
417
|
+
* class is still necessary to prevent a flash of incorrect menu position). We're doing it in an
|
|
418
|
+
* effect to prevent a "Cannot update a component while rendering a different component" error.
|
|
419
|
+
*/}
|
|
420
|
+
{hasTrigger && state.open && state.anchorHidden && actionsRef.current ? (
|
|
421
|
+
<LayoutEffect effect={actionsRef.current.close} />
|
|
422
|
+
) : null}
|
|
423
|
+
</>
|
|
424
|
+
)}
|
|
425
|
+
>
|
|
426
|
+
<Menu.Popup
|
|
427
|
+
ref={popupRef}
|
|
428
|
+
data-initial-open={initialOpen.current || undefined}
|
|
429
|
+
className="
|
|
380
430
|
flex grow origin-(--transform-origin) flex-col overflow-clip rounded-inherit bg-muted outline-0 transition
|
|
381
431
|
data-ending-style:scale-95
|
|
382
432
|
data-ending-style:opacity-0
|
|
383
433
|
data-starting-style:not-data-initial-open:scale-95
|
|
384
434
|
data-starting-style:not-data-initial-open:opacity-0
|
|
385
435
|
"
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
436
|
+
onKeyDown={handleMenuPopupKeyDown}
|
|
437
|
+
>
|
|
438
|
+
<MenuPopupContent header={header} footer={footer} empty={empty} loading={loading}>
|
|
439
|
+
{/* When `label` or `type` is provided at the root level, automatically wrap `children` in a group */}
|
|
440
|
+
{label !== undefined || type !== undefined ? (
|
|
441
|
+
type === 'radio' ? (
|
|
442
|
+
<MenuGroup
|
|
443
|
+
label={label}
|
|
444
|
+
type={type}
|
|
445
|
+
value={value}
|
|
446
|
+
defaultValue={defaultValue}
|
|
447
|
+
onValueChange={onValueChange}
|
|
448
|
+
>
|
|
449
|
+
{children}
|
|
450
|
+
</MenuGroup>
|
|
451
|
+
) : (
|
|
452
|
+
<MenuGroup label={label} type={type}>
|
|
453
|
+
{children}
|
|
454
|
+
</MenuGroup>
|
|
455
|
+
)
|
|
401
456
|
) : (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
</Menu.Popup>
|
|
411
|
-
</Menu.Positioner>
|
|
412
|
-
</Menu.Portal>
|
|
413
|
-
</MenuRoot>
|
|
457
|
+
children
|
|
458
|
+
)}
|
|
459
|
+
</MenuPopupContent>
|
|
460
|
+
</Menu.Popup>
|
|
461
|
+
</Menu.Positioner>
|
|
462
|
+
</Menu.Portal>
|
|
463
|
+
</MenuRoot>
|
|
464
|
+
</MenuFilter>
|
|
414
465
|
</span>
|
|
415
466
|
)
|
|
416
467
|
}
|
|
417
468
|
MenuRoot.displayName = 'Menu'
|
|
418
469
|
|
|
470
|
+
/** Makes the menu filterable when it's `searchable` (see `MenuProps.OwnProps['searchable']`). */
|
|
471
|
+
function MenuFilter({
|
|
472
|
+
searchable,
|
|
473
|
+
searchValue,
|
|
474
|
+
onSearchValueChange,
|
|
475
|
+
children,
|
|
476
|
+
}: Pick<MenuProps.OwnProps, 'searchable' | 'searchValue' | 'onSearchValueChange'> & {
|
|
477
|
+
children: ReactNode
|
|
478
|
+
}) {
|
|
479
|
+
const filter = useMemo(
|
|
480
|
+
() =>
|
|
481
|
+
searchable === 'manual'
|
|
482
|
+
? null
|
|
483
|
+
: typeof searchable === 'function'
|
|
484
|
+
? (label: string, query: string) => searchable({ label, searchValue: query })
|
|
485
|
+
: undefined,
|
|
486
|
+
[searchable],
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<MenuSearchableContext.Provider value={Boolean(searchable)}>
|
|
491
|
+
{searchable ? (
|
|
492
|
+
<Menu.FilterProvider
|
|
493
|
+
filter={filter}
|
|
494
|
+
autoHighlight
|
|
495
|
+
value={searchValue}
|
|
496
|
+
onValueChange={onSearchValueChange}
|
|
497
|
+
>
|
|
498
|
+
{children}
|
|
499
|
+
</Menu.FilterProvider>
|
|
500
|
+
) : (
|
|
501
|
+
children
|
|
502
|
+
)}
|
|
503
|
+
</MenuSearchableContext.Provider>
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
|
|
419
507
|
/**
|
|
420
|
-
*
|
|
421
|
-
*
|
|
508
|
+
* Holds back the search reset that Base UI reports as the menu starts closing until it has finished
|
|
509
|
+
* closing (or reopens), so items filtered outside of Base UI (e.g. with `searchable="manual"`)
|
|
510
|
+
* don't change while it animates out. Base UI keeps filtering its own items with the previous query
|
|
511
|
+
* until then too.
|
|
512
|
+
*/
|
|
513
|
+
function useSearchResetAfterClose(onSearchValueChange: MenuProps.OwnProps['onSearchValueChange']) {
|
|
514
|
+
const pendingResetRef = useRef<(() => void) | null>(null)
|
|
515
|
+
const onSearchValueChangeAfterClose: MenuProps.OwnProps['onSearchValueChange'] =
|
|
516
|
+
onSearchValueChange &&
|
|
517
|
+
((searchValue, eventDetails) => {
|
|
518
|
+
if (eventDetails.reason === 'popup-close') {
|
|
519
|
+
pendingResetRef.current = () => onSearchValueChange(searchValue, eventDetails)
|
|
520
|
+
} else {
|
|
521
|
+
onSearchValueChange(searchValue, eventDetails)
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
const reportPendingReset = () => {
|
|
525
|
+
pendingResetRef.current?.()
|
|
526
|
+
pendingResetRef.current = null
|
|
527
|
+
}
|
|
528
|
+
return [onSearchValueChangeAfterClose, reportPendingReset] as const
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Popup chrome: minimum-width spacer, optional header, scrollable items area (with a searchable
|
|
533
|
+
* menu's empty state), optional footer, border overlay.
|
|
422
534
|
*/
|
|
423
535
|
function MenuPopupContent({
|
|
424
536
|
header,
|
|
425
537
|
footer,
|
|
538
|
+
empty,
|
|
539
|
+
loading = false,
|
|
426
540
|
children,
|
|
427
541
|
}: {
|
|
428
542
|
header?: ReactNode | undefined
|
|
429
543
|
footer?: ReactNode | undefined
|
|
544
|
+
/** Only for searchable menus, where Base UI shows it when no item matches. */
|
|
545
|
+
empty?: ReactNode | undefined
|
|
546
|
+
loading?: boolean | undefined
|
|
430
547
|
children?: ReactNode | undefined
|
|
431
548
|
}) {
|
|
549
|
+
const searchable = useContext(MenuSearchableContext)
|
|
550
|
+
const [emptyShowsLoading, setEmptyShowsLoading] = useState(false)
|
|
551
|
+
|
|
432
552
|
return (
|
|
433
553
|
<>
|
|
434
554
|
{/**
|
|
@@ -440,15 +560,25 @@ function MenuPopupContent({
|
|
|
440
560
|
<div className="w-58" />
|
|
441
561
|
{header ? (
|
|
442
562
|
<div className="nested/menu-header u:shrink-0 u:border-b u:border-muted u:p-4">
|
|
443
|
-
{
|
|
563
|
+
<MenuSearchLoadingContext.Provider value={loading && !emptyShowsLoading}>
|
|
564
|
+
{header}
|
|
565
|
+
</MenuSearchLoadingContext.Provider>
|
|
444
566
|
</div>
|
|
445
567
|
) : null}
|
|
446
568
|
<div className="grow scroll-p-8 scrollbar-thin overflow-x-clip overflow-y-auto gradient-mask-y">
|
|
447
569
|
<div className="overflow-clip p-2">
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
570
|
+
{searchable ? (
|
|
571
|
+
<Menu.Empty>
|
|
572
|
+
<MenuEmptyContent
|
|
573
|
+
empty={empty}
|
|
574
|
+
loading={loading}
|
|
575
|
+
onLoadingShownChange={setEmptyShowsLoading}
|
|
576
|
+
/>
|
|
577
|
+
</Menu.Empty>
|
|
578
|
+
) : null}
|
|
579
|
+
<Menu.List aria-busy={loading || undefined} className="outline-0">
|
|
580
|
+
<MenuGroupContext.Provider value="root">{children}</MenuGroupContext.Provider>
|
|
581
|
+
</Menu.List>
|
|
452
582
|
</div>
|
|
453
583
|
</div>
|
|
454
584
|
{footer ? (
|
|
@@ -535,8 +665,8 @@ function MenuInlineFallback({
|
|
|
535
665
|
{...mergeProps(
|
|
536
666
|
{
|
|
537
667
|
className: cn(`
|
|
538
|
-
pointer-events-none absolute inset-[anchor(inside)] size-auto! [position-anchor:--eds-menu]
|
|
539
|
-
not-open:hidden
|
|
668
|
+
pointer-events-none! absolute! inset-[anchor(inside)]! z-auto! m-0! size-auto! [position-anchor:--eds-menu]!
|
|
669
|
+
not-open:hidden!
|
|
540
670
|
u:rounded-12
|
|
541
671
|
`),
|
|
542
672
|
},
|
|
@@ -622,9 +752,44 @@ function renderMenuTriggerElement({
|
|
|
622
752
|
}
|
|
623
753
|
}
|
|
624
754
|
|
|
755
|
+
/**
|
|
756
|
+
* The content of a searchable menu's `Menu.Empty`, which Base UI only renders while no item
|
|
757
|
+
* matches. Until a search finishes, `empty` would be premature, so `loading` shows a `LoadingIcon`
|
|
758
|
+
* instead (rather than in the search input).
|
|
759
|
+
*/
|
|
760
|
+
function MenuEmptyContent({
|
|
761
|
+
empty,
|
|
762
|
+
loading,
|
|
763
|
+
onLoadingShownChange,
|
|
764
|
+
}: {
|
|
765
|
+
empty: ReactNode
|
|
766
|
+
loading: boolean
|
|
767
|
+
onLoadingShownChange: (loadingShown: boolean) => void
|
|
768
|
+
}) {
|
|
769
|
+
const [searchFinished, setSearchFinished] = useState(!loading)
|
|
770
|
+
if (!loading && !searchFinished) setSearchFinished(true)
|
|
771
|
+
|
|
772
|
+
useLayoutEffect(() => {
|
|
773
|
+
onLoadingShownChange(!searchFinished)
|
|
774
|
+
return () => onLoadingShownChange(false)
|
|
775
|
+
}, [searchFinished, onLoadingShownChange])
|
|
776
|
+
|
|
777
|
+
if (!searchFinished) {
|
|
778
|
+
return (
|
|
779
|
+
<MenuEmpty>
|
|
780
|
+
<span className="flex h-lh items-center justify-center">
|
|
781
|
+
<LoadingIcon />
|
|
782
|
+
</span>
|
|
783
|
+
</MenuEmpty>
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
return empty === undefined || typeof empty === 'string' ? <MenuEmpty>{empty}</MenuEmpty> : empty
|
|
787
|
+
}
|
|
788
|
+
|
|
625
789
|
/**
|
|
626
790
|
* Focus the `[data-checked]` item on open so Base UI's composite syncs its active index to it (Base
|
|
627
|
-
* UI has no `initialActiveIndex` API).
|
|
791
|
+
* UI has no `initialActiveIndex` API). In a searchable menu, typing then moves focus back to the
|
|
792
|
+
* search input.
|
|
628
793
|
*/
|
|
629
794
|
function useHighlightCheckedOnOpen(
|
|
630
795
|
popupRef: RefObject<HTMLDivElement | null>,
|
|
@@ -646,19 +811,13 @@ function useHighlightCheckedOnOpen(
|
|
|
646
811
|
) {
|
|
647
812
|
return
|
|
648
813
|
}
|
|
649
|
-
const searchInput = popup.querySelector<HTMLInputElement>('input[type="search"]')
|
|
650
|
-
const searchWasFocused = searchInput !== null && document.activeElement === searchInput
|
|
651
814
|
checkedItem.focus({ preventScroll: false })
|
|
652
|
-
if (searchWasFocused) searchInput.focus({ preventScroll: true })
|
|
653
815
|
})
|
|
654
816
|
return () => cancelAnimationFrame(frameId)
|
|
655
817
|
}, [popupRef, open, enabled])
|
|
656
818
|
}
|
|
657
819
|
|
|
658
|
-
/**
|
|
659
|
-
* Keydown handler for `Menu.Popup`. Handles Shift + Tab escaping the popup and inline-search-input
|
|
660
|
-
* / composite-item coordination.
|
|
661
|
-
*/
|
|
820
|
+
/** Keydown handler for `Menu.Popup`. Handles Shift + Tab escaping the popup. */
|
|
662
821
|
function handleMenuPopupKeyDown(event: BaseUIEvent<KeyboardEvent<HTMLDivElement>>) {
|
|
663
822
|
/**
|
|
664
823
|
* Ensure Shift + Tab doesn't close the menu if there is a previous focusable element that is not
|
|
@@ -690,59 +849,6 @@ function handleMenuPopupKeyDown(event: BaseUIEvent<KeyboardEvent<HTMLDivElement>
|
|
|
690
849
|
(!activeElementIsMenuItem && previousElementIsMenuItem)
|
|
691
850
|
) {
|
|
692
851
|
event.preventBaseUIHandler()
|
|
693
|
-
return
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
// If there is a search input in the menu...
|
|
697
|
-
const searchInput = event.currentTarget.querySelector('input[type="search"]')
|
|
698
|
-
if (searchInput instanceof HTMLInputElement) {
|
|
699
|
-
// If the user typed some text, ensure it goes in the input instead of Base UI focusing the item that matches
|
|
700
|
-
if (
|
|
701
|
-
(event.key.length === 1 && event.key !== ' ') ||
|
|
702
|
-
event.key === 'Backspace' ||
|
|
703
|
-
event.key === 'ArrowLeft' ||
|
|
704
|
-
event.key === 'ArrowRight'
|
|
705
|
-
) {
|
|
706
|
-
event.preventBaseUIHandler()
|
|
707
|
-
searchInput.focus()
|
|
708
|
-
} else {
|
|
709
|
-
const searchInputIsFocused = document.activeElement === searchInput
|
|
710
|
-
const menuItems = [...event.currentTarget.querySelectorAll<HTMLElement>('.eds-menu-item')]
|
|
711
|
-
const highlightedMenuItem = menuItems.find((item) => item.matches('[data-highlighted]'))
|
|
712
|
-
/**
|
|
713
|
-
* If the user pressed the down arrow while the search input is focused and there is a single
|
|
714
|
-
* menu item in the results, move the focus to it because it may already be highlighted, which
|
|
715
|
-
* would prevent Base UI from focusing it.
|
|
716
|
-
*/
|
|
717
|
-
if (event.key === 'ArrowDown' && searchInputIsFocused && menuItems.length === 1) {
|
|
718
|
-
event.preventBaseUIHandler()
|
|
719
|
-
menuItems[0]!.focus()
|
|
720
|
-
}
|
|
721
|
-
// Inversely, if the user pressed the up arrow and the focus is on the only menu item, move it back to the search input
|
|
722
|
-
else if (
|
|
723
|
-
event.key === 'ArrowUp' &&
|
|
724
|
-
menuItems.length === 1 &&
|
|
725
|
-
document.activeElement === menuItems[0]
|
|
726
|
-
) {
|
|
727
|
-
event.preventDefault()
|
|
728
|
-
searchInput.focus()
|
|
729
|
-
}
|
|
730
|
-
// If the user pressed Enter while the search input is focused and a menu item is highlighted, select it
|
|
731
|
-
else if (event.key === 'Enter' && searchInputIsFocused && highlightedMenuItem) {
|
|
732
|
-
highlightedMenuItem.focus()
|
|
733
|
-
window.setTimeout(() => {
|
|
734
|
-
highlightedMenuItem.dispatchEvent(
|
|
735
|
-
new KeyboardEvent('keydown', {
|
|
736
|
-
key: 'Enter',
|
|
737
|
-
code: 'Enter',
|
|
738
|
-
keyCode: 13,
|
|
739
|
-
which: 13,
|
|
740
|
-
bubbles: true,
|
|
741
|
-
cancelable: true,
|
|
742
|
-
}),
|
|
743
|
-
)
|
|
744
|
-
}, 0)
|
|
745
|
-
}
|
|
746
852
|
}
|
|
747
853
|
}
|
|
748
854
|
}
|
|
@@ -794,7 +900,11 @@ export function MenuGroup<T extends OptionValue>({
|
|
|
794
900
|
<MenuGroupContext.Provider value={menuGroupContextValue}>
|
|
795
901
|
<Menu.Group
|
|
796
902
|
{...mergeProps(
|
|
797
|
-
{
|
|
903
|
+
{
|
|
904
|
+
className: cn(
|
|
905
|
+
'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',
|
|
906
|
+
),
|
|
907
|
+
},
|
|
798
908
|
props,
|
|
799
909
|
)}
|
|
800
910
|
render={
|
|
@@ -825,6 +935,11 @@ export declare namespace MenuItemProps {
|
|
|
825
935
|
'defaultChecked' | 'defaultValue' | 'onChange' | 'type' | 'value'
|
|
826
936
|
> {}
|
|
827
937
|
interface OwnProps {
|
|
938
|
+
/**
|
|
939
|
+
* Text that search and typeahead match against. Defaults to the item's rendered text, addons
|
|
940
|
+
* included.
|
|
941
|
+
*/
|
|
942
|
+
label?: string | undefined
|
|
828
943
|
addonBefore?: AddonValue | undefined
|
|
829
944
|
addonAfter?: AddonValue | undefined
|
|
830
945
|
disabled?: boolean | undefined
|
|
@@ -873,6 +988,7 @@ export type MenuItemProps<T extends OptionValue = OptionValue> =
|
|
|
873
988
|
export function MenuItem<T extends OptionValue>({
|
|
874
989
|
ref: passedRef,
|
|
875
990
|
type: passedType,
|
|
991
|
+
label,
|
|
876
992
|
addonBefore: passedAddonBefore,
|
|
877
993
|
addonAfter: passedAddonAfter,
|
|
878
994
|
variant,
|
|
@@ -1021,7 +1137,14 @@ export function MenuItem<T extends OptionValue>({
|
|
|
1021
1137
|
}
|
|
1022
1138
|
|
|
1023
1139
|
if (type === 'submenu-trigger') {
|
|
1024
|
-
return
|
|
1140
|
+
return (
|
|
1141
|
+
<Menu.SubmenuTrigger
|
|
1142
|
+
nativeButton
|
|
1143
|
+
label={label}
|
|
1144
|
+
disabled={disabled}
|
|
1145
|
+
render={renderItem(baseProps)}
|
|
1146
|
+
/>
|
|
1147
|
+
)
|
|
1025
1148
|
}
|
|
1026
1149
|
|
|
1027
1150
|
if (type === 'checkbox') {
|
|
@@ -1029,6 +1152,7 @@ export function MenuItem<T extends OptionValue>({
|
|
|
1029
1152
|
<CheckboxItem
|
|
1030
1153
|
{...baseProps}
|
|
1031
1154
|
nativeButton
|
|
1155
|
+
label={label}
|
|
1032
1156
|
checked={checked}
|
|
1033
1157
|
defaultChecked={defaultChecked}
|
|
1034
1158
|
onCheckedChange={onCheckedChange}
|
|
@@ -1044,6 +1168,7 @@ export function MenuItem<T extends OptionValue>({
|
|
|
1044
1168
|
{...baseProps}
|
|
1045
1169
|
nativeButton
|
|
1046
1170
|
closeOnClick
|
|
1171
|
+
label={label}
|
|
1047
1172
|
value={passedValue !== undefined ? passedValue : autoValue}
|
|
1048
1173
|
disabled={disabled}
|
|
1049
1174
|
render={renderItem}
|
|
@@ -1053,13 +1178,22 @@ export function MenuItem<T extends OptionValue>({
|
|
|
1053
1178
|
|
|
1054
1179
|
if (href !== undefined) {
|
|
1055
1180
|
return (
|
|
1056
|
-
<Menu.LinkItem
|
|
1181
|
+
<Menu.LinkItem
|
|
1182
|
+
{...baseProps}
|
|
1183
|
+
closeOnClick
|
|
1184
|
+
label={label}
|
|
1185
|
+
href={href}
|
|
1186
|
+
target={target}
|
|
1187
|
+
render={renderItem}
|
|
1188
|
+
>
|
|
1057
1189
|
{children}
|
|
1058
1190
|
</Menu.LinkItem>
|
|
1059
1191
|
)
|
|
1060
1192
|
}
|
|
1061
1193
|
|
|
1062
|
-
return
|
|
1194
|
+
return (
|
|
1195
|
+
<Menu.Item {...baseProps} nativeButton label={label} disabled={disabled} render={renderItem} />
|
|
1196
|
+
)
|
|
1063
1197
|
}
|
|
1064
1198
|
MenuItem.displayName = 'Menu.Item'
|
|
1065
1199
|
|
|
@@ -1080,29 +1214,21 @@ function MenuItemAddon({ children, ...props }: ComponentProps<'span'>) {
|
|
|
1080
1214
|
)
|
|
1081
1215
|
}
|
|
1082
1216
|
|
|
1083
|
-
export interface MenuSearchProps extends Omit<
|
|
1217
|
+
export interface MenuSearchProps extends Omit<
|
|
1218
|
+
ComponentProps<'input'>,
|
|
1219
|
+
'children' | 'value' | 'defaultValue'
|
|
1220
|
+
> {
|
|
1084
1221
|
/** @default 'Search' */
|
|
1085
1222
|
placeholder?: string | undefined
|
|
1086
|
-
value?: string | undefined
|
|
1087
|
-
defaultValue?: string | undefined
|
|
1088
|
-
onValueChange?: ((value: string) => void) | undefined
|
|
1089
1223
|
}
|
|
1090
1224
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
onValueChange,
|
|
1097
|
-
...props
|
|
1098
|
-
}: MenuSearchProps) {
|
|
1099
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
1100
|
-
const inputPassedRef = useMergedRefs(inputRef, passedRef)
|
|
1101
|
-
|
|
1102
|
-
const placeholder = passedPlaceholder ?? 'Search'
|
|
1103
|
-
|
|
1104
|
-
const [value, setValue] = useControlled(controlledValue, defaultValue ?? '', onValueChange)
|
|
1225
|
+
/** Search input for the `header` of a `searchable` menu, which filters its items. */
|
|
1226
|
+
export function MenuSearch({ placeholder = 'Search', ...props }: MenuSearchProps) {
|
|
1227
|
+
if (!useContext(MenuSearchableContext)) {
|
|
1228
|
+
throw new Error('[Menu] `Menu.Search` must be used in a `searchable` menu')
|
|
1229
|
+
}
|
|
1105
1230
|
|
|
1231
|
+
const loading = useContext(MenuSearchLoadingContext)
|
|
1106
1232
|
const { rootProps, nestedProps } = splitProps(props)
|
|
1107
1233
|
|
|
1108
1234
|
return (
|
|
@@ -1112,36 +1238,27 @@ export function MenuSearch({
|
|
|
1112
1238
|
rootProps,
|
|
1113
1239
|
)}
|
|
1114
1240
|
>
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1241
|
+
{loading ? (
|
|
1242
|
+
<LoadingIcon className="absolute inset-y-0 inset-s-3.5 my-auto size-3.5" />
|
|
1243
|
+
) : (
|
|
1244
|
+
<MagnifyingGlassIcon alt="" className="absolute inset-y-0 inset-s-3.5 my-auto size-3.5" />
|
|
1245
|
+
)}
|
|
1246
|
+
<Menu.Input
|
|
1119
1247
|
placeholder={placeholder}
|
|
1120
1248
|
aria-label={placeholder}
|
|
1121
|
-
value={value}
|
|
1122
1249
|
{...mergeProps(
|
|
1123
|
-
{
|
|
1124
|
-
className: cn('peer -mb-px h-10 w-0 grow ps-8.5 outline-0'),
|
|
1125
|
-
onChange: (event: ChangeEvent<HTMLInputElement>) => setValue(event.target.value),
|
|
1126
|
-
},
|
|
1250
|
+
{ className: cn('peer -mb-px h-10 w-0 grow ps-8.5 outline-0') },
|
|
1127
1251
|
nestedProps,
|
|
1128
1252
|
)}
|
|
1129
1253
|
/>
|
|
1130
1254
|
<div className="me-1.5 flex size-7 shrink-0 items-center justify-center peer-placeholder-shown:hidden">
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
onClick={() => {
|
|
1136
|
-
const input = inputRef.current
|
|
1137
|
-
if (!input) return
|
|
1138
|
-
setInputValue(input, '')
|
|
1139
|
-
input.focus()
|
|
1140
|
-
}}
|
|
1141
|
-
className="h-full"
|
|
1255
|
+
{/* Exposed to assistive tech, unlike Base UI's default */}
|
|
1256
|
+
<Menu.Clear
|
|
1257
|
+
aria-hidden={false}
|
|
1258
|
+
render={<Button variant="naked" size="xsmall" className="h-full" />}
|
|
1142
1259
|
>
|
|
1143
1260
|
<XIcon alt="Clear" />
|
|
1144
|
-
</
|
|
1261
|
+
</Menu.Clear>
|
|
1145
1262
|
</div>
|
|
1146
1263
|
</div>
|
|
1147
1264
|
)
|
|
@@ -1151,12 +1268,18 @@ MenuSearch.displayName = 'Menu.Search'
|
|
|
1151
1268
|
export interface MenuEmptyProps extends ComponentProps<'div'> {}
|
|
1152
1269
|
|
|
1153
1270
|
/**
|
|
1154
|
-
* Renders the standard empty-state treatment for a `Menu
|
|
1155
|
-
*
|
|
1271
|
+
* Renders the standard empty-state treatment for a `Menu`, e.g. when it has no items. Searchable
|
|
1272
|
+
* menus render it when no item matches (see `empty`). Its text wraps instead of widening the menu.
|
|
1273
|
+
* Defaults to `"No results"`.
|
|
1156
1274
|
*/
|
|
1157
1275
|
export function MenuEmpty({ children = 'No results', ...props }: MenuEmptyProps) {
|
|
1276
|
+
// `contain-inline-size` keeps the text from widening a menu that sizes to fit its content
|
|
1158
1277
|
return (
|
|
1159
|
-
<div
|
|
1278
|
+
<div
|
|
1279
|
+
{...mergeProps({ className: cn('p-2 contain-inline-size u:text-16 u:text-muted') }, props)}
|
|
1280
|
+
>
|
|
1281
|
+
{children}
|
|
1282
|
+
</div>
|
|
1160
1283
|
)
|
|
1161
1284
|
}
|
|
1162
1285
|
MenuEmpty.displayName = 'Menu.Empty'
|