@chassis-ui/react 0.0.0-20260831090525
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/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/index.d.ts +5909 -0
- package/dist/index.js +2 -0
- package/dist/style.css +1 -0
- package/package.json +114 -0
- package/src/components/accordion/Accordion.tsx +190 -0
- package/src/components/accordion/AccordionBody.tsx +46 -0
- package/src/components/accordion/AccordionHeader.tsx +21 -0
- package/src/components/accordion/AccordionItem.tsx +59 -0
- package/src/components/accordion/context.ts +8 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/autocomplete/Autocomplete.tsx +464 -0
- package/src/components/autocomplete/AutocompleteGroup.tsx +20 -0
- package/src/components/autocomplete/AutocompleteItem.tsx +18 -0
- package/src/components/autocomplete/index.ts +6 -0
- package/src/components/avatar/Avatar.tsx +134 -0
- package/src/components/avatar/AvatarImage.tsx +19 -0
- package/src/components/avatar/AvatarStack.tsx +98 -0
- package/src/components/avatar/index.ts +6 -0
- package/src/components/badge/Badge.tsx +94 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/breadcrumb/Breadcrumb.tsx +59 -0
- package/src/components/breadcrumb/BreadcrumbItem.tsx +38 -0
- package/src/components/breadcrumb/index.ts +4 -0
- package/src/components/button/Button.tsx +229 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/button-group/ButtonGroup.tsx +55 -0
- package/src/components/button-group/ButtonToolbar.tsx +47 -0
- package/src/components/button-group/index.ts +4 -0
- package/src/components/calendar/Calendar.scss +57 -0
- package/src/components/calendar/Calendar.tsx +324 -0
- package/src/components/calendar/CalendarMonthBlock.tsx +57 -0
- package/src/components/calendar/CalendarMonthGrid.tsx +92 -0
- package/src/components/calendar/CalendarMonthYearPicker.tsx +162 -0
- package/src/components/calendar/CalendarNavButton.tsx +24 -0
- package/src/components/calendar/CalendarWeekGrid.tsx +58 -0
- package/src/components/calendar/CalendarYearGrid.tsx +126 -0
- package/src/components/calendar/RangeCalendar.scss +95 -0
- package/src/components/calendar/RangeCalendar.tsx +361 -0
- package/src/components/calendar/index.ts +5 -0
- package/src/components/card/Card.tsx +173 -0
- package/src/components/card/CardBody.tsx +75 -0
- package/src/components/card/CardFooter.tsx +47 -0
- package/src/components/card/CardGroup.tsx +51 -0
- package/src/components/card/CardHeader.tsx +47 -0
- package/src/components/card/CardImage.tsx +70 -0
- package/src/components/card/CardImageOverlay.tsx +47 -0
- package/src/components/card/CardLink.tsx +40 -0
- package/src/components/card/CardSubtitle.tsx +47 -0
- package/src/components/card/CardText.tsx +47 -0
- package/src/components/card/CardTitle.tsx +47 -0
- package/src/components/card/index.ts +22 -0
- package/src/components/carousel/Carousel.tsx +742 -0
- package/src/components/carousel/CarouselControlButton.tsx +74 -0
- package/src/components/carousel/CarouselControlNext.tsx +22 -0
- package/src/components/carousel/CarouselControlPrev.tsx +22 -0
- package/src/components/carousel/CarouselIndicators.tsx +44 -0
- package/src/components/carousel/CarouselInner.tsx +56 -0
- package/src/components/carousel/CarouselItem.tsx +51 -0
- package/src/components/carousel/CarouselOverlay.tsx +51 -0
- package/src/components/carousel/CarouselPlayPause.tsx +61 -0
- package/src/components/carousel/carouselEngine.ts +170 -0
- package/src/components/carousel/context.ts +29 -0
- package/src/components/carousel/index.ts +21 -0
- package/src/components/checkbox/Checkbox.tsx +236 -0
- package/src/components/checkbox/CheckboxGroup.tsx +145 -0
- package/src/components/checkbox/context.ts +14 -0
- package/src/components/checkbox/index.ts +4 -0
- package/src/components/chip/Chip.tsx +183 -0
- package/src/components/chip/index.ts +2 -0
- package/src/components/chip-input/ChipInput.tsx +363 -0
- package/src/components/chip-input/ChipList.tsx +95 -0
- package/src/components/chip-input/index.ts +2 -0
- package/src/components/close-button/CloseButton.tsx +205 -0
- package/src/components/close-button/index.ts +2 -0
- package/src/components/collapse/Collapse.tsx +128 -0
- package/src/components/collapse/index.ts +2 -0
- package/src/components/color-input/ColorInput.tsx +106 -0
- package/src/components/color-input/index.ts +2 -0
- package/src/components/combobox/Combobox.tsx +320 -0
- package/src/components/combobox/ComboboxGroup.tsx +27 -0
- package/src/components/combobox/ComboboxItem.tsx +44 -0
- package/src/components/combobox/ComboboxListBox.tsx +98 -0
- package/src/components/combobox/index.ts +6 -0
- package/src/components/datepicker/CalendarToggleButton.tsx +58 -0
- package/src/components/datepicker/ClearButton.tsx +35 -0
- package/src/components/datepicker/DateField.tsx +54 -0
- package/src/components/datepicker/DatePicker.scss +51 -0
- package/src/components/datepicker/DatePicker.tsx +574 -0
- package/src/components/datepicker/DateRangePicker.scss +11 -0
- package/src/components/datepicker/DateRangePicker.tsx +343 -0
- package/src/components/datepicker/index.ts +4 -0
- package/src/components/datepicker/renderDatePickerShell.tsx +104 -0
- package/src/components/drawer/Drawer.tsx +211 -0
- package/src/components/drawer/DrawerBody.tsx +47 -0
- package/src/components/drawer/DrawerFooter.tsx +51 -0
- package/src/components/drawer/DrawerHeader.tsx +60 -0
- package/src/components/drawer/DrawerTitle.tsx +49 -0
- package/src/components/drawer/index.ts +10 -0
- package/src/components/file-input/FileInput.tsx +100 -0
- package/src/components/file-input/index.ts +2 -0
- package/src/components/flex/Flex.tsx +134 -0
- package/src/components/flex/index.ts +2 -0
- package/src/components/floating-input/FloatingInput.tsx +94 -0
- package/src/components/floating-input/index.ts +2 -0
- package/src/components/form/Form.tsx +26 -0
- package/src/components/form/FormFeedback.tsx +64 -0
- package/src/components/form/FormHelp.tsx +46 -0
- package/src/components/form/FormLabel.tsx +26 -0
- package/src/components/form/index.ts +8 -0
- package/src/components/form/renderFormCheck.tsx +86 -0
- package/src/components/form-field/FormField.tsx +84 -0
- package/src/components/form-field/index.ts +2 -0
- package/src/components/form-field/renderFormField.tsx +74 -0
- package/src/components/grid/Col.tsx +69 -0
- package/src/components/grid/Container.tsx +59 -0
- package/src/components/grid/Grid.tsx +86 -0
- package/src/components/grid/GridItem.tsx +123 -0
- package/src/components/grid/Row.tsx +71 -0
- package/src/components/grid/gap.ts +8 -0
- package/src/components/grid/index.ts +10 -0
- package/src/components/icon/Icon.tsx +74 -0
- package/src/components/icon/index.ts +2 -0
- package/src/components/input-adorn/InputAdorn.tsx +55 -0
- package/src/components/input-adorn/index.ts +2 -0
- package/src/components/input-group/InputGroup.tsx +50 -0
- package/src/components/input-group/InputGroupAddon.tsx +50 -0
- package/src/components/input-group/index.ts +4 -0
- package/src/components/link/Link.tsx +148 -0
- package/src/components/link/index.ts +2 -0
- package/src/components/list/List.tsx +193 -0
- package/src/components/list/ListItem.tsx +91 -0
- package/src/components/list/index.ts +4 -0
- package/src/components/menu/Menu.tsx +398 -0
- package/src/components/menu/MenuDivider.tsx +19 -0
- package/src/components/menu/MenuHeader.tsx +55 -0
- package/src/components/menu/MenuItem.tsx +85 -0
- package/src/components/menu/MenuItemDef.ts +83 -0
- package/src/components/menu/MenuList.tsx +142 -0
- package/src/components/menu/MenuSubmenu.tsx +284 -0
- package/src/components/menu/MenuSubmenuBack.tsx +39 -0
- package/src/components/menu/MenuText.tsx +47 -0
- package/src/components/menu/MenuToggle.tsx +148 -0
- package/src/components/menu/index.ts +19 -0
- package/src/components/menu/menuNavigation.ts +106 -0
- package/src/components/menu/submenuGroup.ts +43 -0
- package/src/components/modal/Modal.tsx +162 -0
- package/src/components/modal/ModalBody.tsx +47 -0
- package/src/components/modal/ModalFooter.tsx +51 -0
- package/src/components/modal/ModalHeader.tsx +60 -0
- package/src/components/modal/ModalTitle.tsx +49 -0
- package/src/components/modal/index.ts +10 -0
- package/src/components/nav/Nav.tsx +97 -0
- package/src/components/nav/NavItem.tsx +42 -0
- package/src/components/nav/NavLink.tsx +48 -0
- package/src/components/nav/NavTitle.tsx +22 -0
- package/src/components/nav/index.ts +7 -0
- package/src/components/navbar/Navbar.tsx +112 -0
- package/src/components/navbar/NavbarBrand.tsx +55 -0
- package/src/components/navbar/NavbarNav.tsx +47 -0
- package/src/components/navbar/NavbarText.tsx +47 -0
- package/src/components/navbar/NavbarToggler.tsx +34 -0
- package/src/components/navbar/index.ts +10 -0
- package/src/components/notification/Notification.scss +15 -0
- package/src/components/notification/Notification.tsx +187 -0
- package/src/components/notification/NotificationIcon.tsx +21 -0
- package/src/components/notification/NotificationStack.tsx +66 -0
- package/src/components/notification/NotificationText.tsx +47 -0
- package/src/components/notification/NotificationTitle.tsx +47 -0
- package/src/components/notification/context.ts +14 -0
- package/src/components/notification/index.ts +17 -0
- package/src/components/notification/notificationQueue.ts +48 -0
- package/src/components/otp-input/OtpBox.tsx +68 -0
- package/src/components/otp-input/OtpInput.tsx +341 -0
- package/src/components/otp-input/index.ts +2 -0
- package/src/components/pagination/Pagination.tsx +199 -0
- package/src/components/pagination/PaginationItem.tsx +133 -0
- package/src/components/pagination/index.ts +4 -0
- package/src/components/password-strength/PasswordStrength.tsx +155 -0
- package/src/components/password-strength/index.ts +2 -0
- package/src/components/password-strength/strengthScore.ts +79 -0
- package/src/components/placeholder/Placeholder.tsx +178 -0
- package/src/components/placeholder/index.ts +2 -0
- package/src/components/popover/Popover.tsx +243 -0
- package/src/components/popover/index.ts +2 -0
- package/src/components/progress/Progress.tsx +154 -0
- package/src/components/progress/ProgressBar.tsx +88 -0
- package/src/components/progress/index.ts +4 -0
- package/src/components/radio/Radio.tsx +88 -0
- package/src/components/radio/RadioGroup.tsx +142 -0
- package/src/components/radio/context.ts +14 -0
- package/src/components/radio/index.ts +4 -0
- package/src/components/range-input/RangeInput.tsx +109 -0
- package/src/components/range-input/index.ts +2 -0
- package/src/components/select/Select.tsx +277 -0
- package/src/components/select/index.ts +2 -0
- package/src/components/skeleton/Skeleton.tsx +89 -0
- package/src/components/skeleton/SkeletonLoader.tsx +91 -0
- package/src/components/skeleton/index.ts +4 -0
- package/src/components/spinner/Spinner.tsx +77 -0
- package/src/components/spinner/index.ts +2 -0
- package/src/components/stack/Stack.tsx +81 -0
- package/src/components/stack/index.ts +2 -0
- package/src/components/stepper/Stepper.tsx +172 -0
- package/src/components/stepper/StepperItem.tsx +66 -0
- package/src/components/stepper/index.ts +4 -0
- package/src/components/switch/Switch.tsx +246 -0
- package/src/components/switch/index.ts +2 -0
- package/src/components/table/Table.css +39 -0
- package/src/components/table/Table.tsx +390 -0
- package/src/components/table/TableBody.tsx +25 -0
- package/src/components/table/TableCell.tsx +27 -0
- package/src/components/table/TableColumn.tsx +31 -0
- package/src/components/table/TableHeader.tsx +26 -0
- package/src/components/table/TableRow.tsx +24 -0
- package/src/components/table/index.ts +12 -0
- package/src/components/tabs/Tab.tsx +36 -0
- package/src/components/tabs/TabList.tsx +76 -0
- package/src/components/tabs/TabPanel.tsx +68 -0
- package/src/components/tabs/Tabs.tsx +136 -0
- package/src/components/tabs/context.ts +24 -0
- package/src/components/tabs/index.ts +8 -0
- package/src/components/text-input/TextInput.tsx +188 -0
- package/src/components/text-input/index.ts +2 -0
- package/src/components/textarea/Textarea.tsx +145 -0
- package/src/components/textarea/index.ts +2 -0
- package/src/components/toast/Toast.tsx +219 -0
- package/src/components/toast/ToastBody.tsx +64 -0
- package/src/components/toast/ToastFooter.tsx +47 -0
- package/src/components/toast/ToastHeader.tsx +104 -0
- package/src/components/toast/ToastIcon.tsx +21 -0
- package/src/components/toast/Toaster.tsx +87 -0
- package/src/components/toast/context.ts +14 -0
- package/src/components/toast/index.ts +18 -0
- package/src/components/toast/toastQueue.ts +56 -0
- package/src/components/tooltip/Tooltip.tsx +155 -0
- package/src/components/tooltip/index.ts +2 -0
- package/src/global.d.ts +2 -0
- package/src/hooks/index.ts +57 -0
- package/src/hooks/useAutoDismiss.ts +88 -0
- package/src/hooks/useButtonSemantics.ts +33 -0
- package/src/hooks/useControllableState.ts +24 -0
- package/src/hooks/useDialogElement.ts +216 -0
- package/src/hooks/useDisabledAnchorGuard.ts +22 -0
- package/src/hooks/useDismissibleTransition.ts +126 -0
- package/src/hooks/useDrawer.ts +9 -0
- package/src/hooks/useFloatingOverlay.ts +115 -0
- package/src/hooks/useForkedRef.ts +50 -0
- package/src/hooks/useFormField.ts +89 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +9 -0
- package/src/hooks/useModal.ts +9 -0
- package/src/hooks/useNotification.ts +10 -0
- package/src/hooks/useOverlayPlacement.ts +93 -0
- package/src/hooks/usePagination.ts +85 -0
- package/src/hooks/useToast.ts +9 -0
- package/src/hooks/useToastRegionQueue.ts +27 -0
- package/src/index.ts +276 -0
- package/src/types.ts +133 -0
- package/src/utils/breakpoints.ts +43 -0
- package/src/utils/comboboxCollection.tsx +152 -0
- package/src/utils/dateRangePresets.ts +6 -0
- package/src/utils/dialogTransition.ts +39 -0
- package/src/utils/focusRedirect.ts +29 -0
- package/src/utils/isWholeUnitDisabled.ts +17 -0
- package/src/utils/mergeIsDateUnavailable.ts +15 -0
- package/src/utils/overlayPlacement.ts +93 -0
- package/src/utils/pointerInteraction.ts +21 -0
- package/src/utils/polymorphic.ts +36 -0
- package/src/utils/renderMenuItemContent.tsx +62 -0
- package/src/utils/setVisibleRangeStart.ts +64 -0
- package/src/utils/spacingClassName.ts +14 -0
- package/src/utils/suppressFocusRingGlobally.ts +69 -0
- package/src/utils/validationClassName.ts +8 -0
- package/src/utils/virtualFocusStyle.ts +16 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
ElementType,
|
|
4
|
+
ForwardRefRenderFunction,
|
|
5
|
+
Fragment,
|
|
6
|
+
ReactElement,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef
|
|
10
|
+
} from 'react'
|
|
11
|
+
import classNames from 'classnames'
|
|
12
|
+
import { AriaButtonProps, useMenuTrigger, useOverlayPosition } from 'react-aria'
|
|
13
|
+
import { useMenuTriggerState } from 'react-stately'
|
|
14
|
+
|
|
15
|
+
import { useForkedRef, useIsomorphicLayoutEffect } from '../../hooks'
|
|
16
|
+
import { executeAfterTransition } from '../../utils/dialogTransition'
|
|
17
|
+
import {
|
|
18
|
+
Placement,
|
|
19
|
+
resolveMenuOverlayPositioning,
|
|
20
|
+
toAriaPlacement
|
|
21
|
+
} from '../../utils/overlayPlacement'
|
|
22
|
+
import {
|
|
23
|
+
createPolymorphicComponent,
|
|
24
|
+
PolymorphicComponentProps,
|
|
25
|
+
PolymorphicRef
|
|
26
|
+
} from '../../utils/polymorphic'
|
|
27
|
+
|
|
28
|
+
export type { Placement }
|
|
29
|
+
export type MenuFocusStrategy = 'first' | 'last'
|
|
30
|
+
|
|
31
|
+
export type MenuAutoClose = boolean | 'inside' | 'outside'
|
|
32
|
+
|
|
33
|
+
type MenuOwnProps<C extends ElementType> = {
|
|
34
|
+
/**
|
|
35
|
+
* Controls which clicks close the menu. `true` closes on any click inside or outside.
|
|
36
|
+
* `false` requires a programmatic `visible` change. `'inside'` closes only on click inside
|
|
37
|
+
* the menu. `'outside'` closes only on click outside the menu.
|
|
38
|
+
*/
|
|
39
|
+
autoClose?: MenuAutoClose
|
|
40
|
+
/**
|
|
41
|
+
* A string of all className you want applied to the base component.
|
|
42
|
+
*/
|
|
43
|
+
className?: string
|
|
44
|
+
/**
|
|
45
|
+
* Component used for the root node. Either a string to use a HTML element or a component.
|
|
46
|
+
* Defaults to `Fragment` — `MenuToggle`/`MenuList` render with no wrapping element, so
|
|
47
|
+
* they land as direct children of whatever contains the `Menu`. This matters inside
|
|
48
|
+
* `ButtonGroup`/`InputGroup`, whose CSS expects the toggle button and menu panel as direct
|
|
49
|
+
* children rather than nested inside an intermediate node.
|
|
50
|
+
*
|
|
51
|
+
* Pass an actual element (e.g. `"div"`, or `"li"` for a navbar item) to opt back into a
|
|
52
|
+
* wrapper — needed if you want to apply `className`/`style`/other rest props to a container
|
|
53
|
+
* around the whole menu, for semantic wrapping like a nav `<li>`, or for `reference="parent"`,
|
|
54
|
+
* which positions off this wrapper and has nothing to measure against without one.
|
|
55
|
+
*/
|
|
56
|
+
component?: C
|
|
57
|
+
/**
|
|
58
|
+
* Teleports the menu panel to a container element on open. Accepts an element reference, or
|
|
59
|
+
* `true` to append to `document.body`.
|
|
60
|
+
*/
|
|
61
|
+
container?: boolean | Element
|
|
62
|
+
/**
|
|
63
|
+
* Distance between the menu and its reference element, as `[skidding, distance]` in pixels.
|
|
64
|
+
*/
|
|
65
|
+
offset?: [number, number]
|
|
66
|
+
/**
|
|
67
|
+
* Callback fired when the menu requests to be hidden.
|
|
68
|
+
*/
|
|
69
|
+
onHide?: () => void
|
|
70
|
+
/**
|
|
71
|
+
* Callback fired after the menu finishes hiding.
|
|
72
|
+
*/
|
|
73
|
+
onHidden?: () => void
|
|
74
|
+
/**
|
|
75
|
+
* Callback fired when the menu requests to be shown.
|
|
76
|
+
*/
|
|
77
|
+
onShow?: () => void
|
|
78
|
+
/**
|
|
79
|
+
* Callback fired after the menu finishes showing.
|
|
80
|
+
*/
|
|
81
|
+
onShown?: () => void
|
|
82
|
+
/**
|
|
83
|
+
* Initial placement. Chassis will flip it to keep the menu in view.
|
|
84
|
+
*
|
|
85
|
+
* @type 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end'
|
|
86
|
+
*/
|
|
87
|
+
placement?: Placement
|
|
88
|
+
/**
|
|
89
|
+
* Reference element used for positioning. `'toggle'` uses the trigger. `'parent'` uses this
|
|
90
|
+
* component's own rendered element, useful for split buttons and button groups.
|
|
91
|
+
*/
|
|
92
|
+
reference?: 'toggle' | 'parent'
|
|
93
|
+
/**
|
|
94
|
+
* Toggle the visibility of the menu component.
|
|
95
|
+
*/
|
|
96
|
+
visible?: boolean
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type MenuProps<C extends ElementType = typeof Fragment> = PolymorphicComponentProps<
|
|
100
|
+
C,
|
|
101
|
+
MenuOwnProps<C>
|
|
102
|
+
>
|
|
103
|
+
|
|
104
|
+
type MenuComponent = (<C extends ElementType = typeof Fragment>(
|
|
105
|
+
props: MenuProps<C> & { ref?: PolymorphicRef<C> }
|
|
106
|
+
) => ReactElement | null) & { displayName?: string }
|
|
107
|
+
|
|
108
|
+
export interface MenuContextProps {
|
|
109
|
+
autoClose: MenuAutoClose
|
|
110
|
+
close: () => void
|
|
111
|
+
container?: boolean | Element
|
|
112
|
+
focusStrategy: MenuFocusStrategy | null
|
|
113
|
+
hide: () => void
|
|
114
|
+
menuId: string
|
|
115
|
+
menuStyle: React.CSSProperties
|
|
116
|
+
menuTriggerProps: AriaButtonProps
|
|
117
|
+
overlayRef: React.MutableRefObject<HTMLElement | null>
|
|
118
|
+
placementAttr: string
|
|
119
|
+
reference: 'toggle' | 'parent'
|
|
120
|
+
/**
|
|
121
|
+
* Registers (or, passing `null`, unregisters) a nested `MenuSubmenu`'s own portaled panel
|
|
122
|
+
* element, so this `Menu`'s outside/inside click-dismiss logic recognizes clicks landing in it
|
|
123
|
+
* as "inside" the menu — see `MenuSubmenu`'s own use for why this is necessary: its panel
|
|
124
|
+
* portals straight to `document.body`, structurally detached from this menu's own DOM subtree.
|
|
125
|
+
*/
|
|
126
|
+
registerOverlay: (id: string, node: HTMLElement | null) => void
|
|
127
|
+
show: (focusStrategy?: MenuFocusStrategy | null) => void
|
|
128
|
+
targetRef: React.MutableRefObject<HTMLElement | null>
|
|
129
|
+
toggle: (focusStrategy?: MenuFocusStrategy | null) => void
|
|
130
|
+
toggleNodeRef: React.MutableRefObject<HTMLElement | null>
|
|
131
|
+
triggerId: string
|
|
132
|
+
visible: boolean
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A fully-shaped no-op default so a `MenuToggle`/`MenuList`/`MenuSubmenu` rendered without a
|
|
136
|
+
// `Menu` ancestor (or in an environment where context can't cross a component boundary, e.g.
|
|
137
|
+
// some static-site prerenderers) degrades to an inert, always-closed menu instead of throwing.
|
|
138
|
+
const noop = () => undefined
|
|
139
|
+
|
|
140
|
+
const defaultMenuContext: MenuContextProps = {
|
|
141
|
+
autoClose: true,
|
|
142
|
+
close: noop,
|
|
143
|
+
focusStrategy: null,
|
|
144
|
+
hide: noop,
|
|
145
|
+
menuId: '',
|
|
146
|
+
menuStyle: {},
|
|
147
|
+
menuTriggerProps: {},
|
|
148
|
+
overlayRef: { current: null },
|
|
149
|
+
placementAttr: 'bottom-start',
|
|
150
|
+
reference: 'toggle',
|
|
151
|
+
registerOverlay: noop,
|
|
152
|
+
show: noop,
|
|
153
|
+
targetRef: { current: null },
|
|
154
|
+
toggle: noop,
|
|
155
|
+
toggleNodeRef: { current: null },
|
|
156
|
+
triggerId: '',
|
|
157
|
+
visible: false
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const MenuContext = createContext(defaultMenuContext)
|
|
161
|
+
|
|
162
|
+
function MenuRender<C extends ElementType = typeof Fragment>(
|
|
163
|
+
{
|
|
164
|
+
children,
|
|
165
|
+
autoClose = true,
|
|
166
|
+
className,
|
|
167
|
+
component,
|
|
168
|
+
container,
|
|
169
|
+
offset: offsetProp = [0, 2],
|
|
170
|
+
onHide,
|
|
171
|
+
onHidden,
|
|
172
|
+
onShow,
|
|
173
|
+
onShown,
|
|
174
|
+
placement = 'bottom-start',
|
|
175
|
+
reference = 'toggle',
|
|
176
|
+
visible,
|
|
177
|
+
...rest
|
|
178
|
+
}: MenuProps<C>,
|
|
179
|
+
ref: PolymorphicRef<C>
|
|
180
|
+
) {
|
|
181
|
+
const Component = component ?? (Fragment as ElementType)
|
|
182
|
+
const wrapperRef = useRef<HTMLElement>(null)
|
|
183
|
+
const forkedRef = useForkedRef(ref, wrapperRef)
|
|
184
|
+
const toggleNodeRef = useRef<HTMLElement | null>(null)
|
|
185
|
+
const targetRef = useRef<HTMLElement | null>(null)
|
|
186
|
+
const overlayRef = useRef<HTMLElement | null>(null)
|
|
187
|
+
// Portaled `MenuSubmenu` panels register themselves here (see `registerOverlay` below and
|
|
188
|
+
// `MenuSubmenu`'s own ref callback) so `handleDismiss` can recognize clicks inside them as
|
|
189
|
+
// "inside" this menu despite living outside `overlayRef.current`'s own DOM subtree.
|
|
190
|
+
const submenuOverlaysRef = useRef<Map<string, HTMLElement>>(new Map())
|
|
191
|
+
const registerOverlay = (id: string, node: HTMLElement | null) => {
|
|
192
|
+
if (node) submenuOverlaysRef.current.set(id, node)
|
|
193
|
+
else submenuOverlaysRef.current.delete(id)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const state = useMenuTriggerState({ defaultOpen: !!visible })
|
|
197
|
+
const { menuTriggerProps, menuProps } = useMenuTrigger<unknown>({}, state, toggleNodeRef)
|
|
198
|
+
|
|
199
|
+
// Sync-on-change, not strictly controlled — matches `Modal`'s `visible` semantics.
|
|
200
|
+
// Internal `show`/`hide`/`toggle` calls (from `MenuToggle`, autoClose dismissal, etc.)
|
|
201
|
+
// still work freely between prop changes; `visible` only re-asserts the open state when
|
|
202
|
+
// its own value actually changes.
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (visible === undefined) return
|
|
205
|
+
if (visible) state.open()
|
|
206
|
+
else state.close()
|
|
207
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
208
|
+
}, [visible])
|
|
209
|
+
|
|
210
|
+
useIsomorphicLayoutEffect(() => {
|
|
211
|
+
if (reference === 'parent') {
|
|
212
|
+
targetRef.current = wrapperRef.current
|
|
213
|
+
}
|
|
214
|
+
}, [reference])
|
|
215
|
+
|
|
216
|
+
// `onShown`/`onHidden` wait for chassis-css's own opacity/transform transition on `.menu`
|
|
217
|
+
// (see `_menu.scss`) to actually finish, matching `Modal`'s identically-worded "requests to
|
|
218
|
+
// be shown" vs "finishes showing" contract (`Modal.tsx`'s own `executeAfterTransition` use).
|
|
219
|
+
// `overlayRef.current` is `MenuList`'s root element — already in the DOM and already
|
|
220
|
+
// reflecting the new `.show` class by the time this effect runs, since React commits the
|
|
221
|
+
// render before passive effects execute. Falls back to firing synchronously when no
|
|
222
|
+
// `MenuList` is mounted to measure (nothing to transition, so nothing to wait for).
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
const overlay = overlayRef.current
|
|
225
|
+
|
|
226
|
+
if (state.isOpen) {
|
|
227
|
+
onShow?.()
|
|
228
|
+
if (!overlay) {
|
|
229
|
+
onShown?.()
|
|
230
|
+
return undefined
|
|
231
|
+
}
|
|
232
|
+
return executeAfterTransition(overlay, () => onShown?.(), true)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
onHide?.()
|
|
236
|
+
if (!overlay) {
|
|
237
|
+
onHidden?.()
|
|
238
|
+
return undefined
|
|
239
|
+
}
|
|
240
|
+
return executeAfterTransition(overlay, () => onHidden?.(), true)
|
|
241
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
242
|
+
}, [state.isOpen])
|
|
243
|
+
|
|
244
|
+
const { overlayProps, placement: resolvedPlacement } = useOverlayPosition({
|
|
245
|
+
targetRef,
|
|
246
|
+
overlayRef,
|
|
247
|
+
placement: toAriaPlacement(placement),
|
|
248
|
+
offset: offsetProp[1],
|
|
249
|
+
crossOffset: offsetProp[0],
|
|
250
|
+
containerPadding: 8,
|
|
251
|
+
isOpen: state.isOpen,
|
|
252
|
+
// `useOverlayPosition` closes on any window scroll via a backward-compat `WeakMap` that
|
|
253
|
+
// `useMenuTrigger` populates for `targetRef.current` (see `MenuToggle`'s `setRefs`).
|
|
254
|
+
// Passing `null` opts out so the menu repositions with its trigger instead of vanishing —
|
|
255
|
+
// matching `Autocomplete`/`Combobox`, neither of which is wired into that map.
|
|
256
|
+
onClose: null
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const { menuStyle, placementAttr } = resolveMenuOverlayPositioning(
|
|
260
|
+
overlayProps.style,
|
|
261
|
+
placement,
|
|
262
|
+
resolvedPlacement
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const show = (focusStrategy?: MenuFocusStrategy | null) => state.open(focusStrategy)
|
|
266
|
+
const hide = () => state.close()
|
|
267
|
+
const toggleVisible = (focusStrategy?: MenuFocusStrategy | null) => state.toggle(focusStrategy)
|
|
268
|
+
const close = () => {
|
|
269
|
+
state.close()
|
|
270
|
+
toggleNodeRef.current?.focus()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Deferred so the click that opened the menu doesn't immediately close it, and scoped
|
|
274
|
+
// to match menu.js's `clearMenus`: skip the toggle itself, honor `inside`/`outside`
|
|
275
|
+
// modes, and let Tab or clicks on form controls inside the menu pass through untouched.
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (!state.isOpen || autoClose === false) return undefined
|
|
278
|
+
|
|
279
|
+
const handleDismiss = (event: MouseEvent | KeyboardEvent) => {
|
|
280
|
+
if (event instanceof KeyboardEvent && event.key !== 'Tab') return
|
|
281
|
+
|
|
282
|
+
const target = event.target as Node
|
|
283
|
+
const toggleNode = toggleNodeRef.current
|
|
284
|
+
const menuNode = overlayRef.current
|
|
285
|
+
|
|
286
|
+
if (toggleNode?.contains(target)) return
|
|
287
|
+
|
|
288
|
+
const isMenuTarget =
|
|
289
|
+
!!menuNode?.contains(target) ||
|
|
290
|
+
Array.from(submenuOverlaysRef.current.values()).some((node) => node.contains(target))
|
|
291
|
+
|
|
292
|
+
if (autoClose === 'inside' && !isMenuTarget) return
|
|
293
|
+
if (autoClose === 'outside' && isMenuTarget) return
|
|
294
|
+
|
|
295
|
+
if (
|
|
296
|
+
isMenuTarget &&
|
|
297
|
+
((event instanceof KeyboardEvent && event.key === 'Tab') ||
|
|
298
|
+
/input|select|option|textarea|form/i.test((target as HTMLElement).tagName ?? ''))
|
|
299
|
+
) {
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
hide()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const id = window.setTimeout(() => {
|
|
307
|
+
window.addEventListener('click', handleDismiss)
|
|
308
|
+
window.addEventListener('keyup', handleDismiss)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return () => {
|
|
312
|
+
window.clearTimeout(id)
|
|
313
|
+
window.removeEventListener('click', handleDismiss)
|
|
314
|
+
window.removeEventListener('keyup', handleDismiss)
|
|
315
|
+
}
|
|
316
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
317
|
+
}, [state.isOpen, autoClose])
|
|
318
|
+
|
|
319
|
+
// Escape should always close the menu, independent of `autoClose`'s inside/outside/false
|
|
320
|
+
// modes. `MenuList`'s own `onKeyDown` handling only fires when focus is already inside the
|
|
321
|
+
// panel (e.g. opened via ArrowDown) — a mouse-press open via `useMenuTrigger` leaves focus on
|
|
322
|
+
// the trigger button, where that handler is unreachable. Listening on `window` catches Escape
|
|
323
|
+
// regardless of where focus currently is.
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (!state.isOpen) return undefined
|
|
326
|
+
|
|
327
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
328
|
+
if (event.key !== 'Escape') return
|
|
329
|
+
close()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
window.addEventListener('keydown', handleEscape)
|
|
333
|
+
|
|
334
|
+
return () => {
|
|
335
|
+
window.removeEventListener('keydown', handleEscape)
|
|
336
|
+
}
|
|
337
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
338
|
+
}, [state.isOpen])
|
|
339
|
+
|
|
340
|
+
const contextValue: MenuContextProps = useMemo(
|
|
341
|
+
() => ({
|
|
342
|
+
autoClose,
|
|
343
|
+
close,
|
|
344
|
+
container,
|
|
345
|
+
focusStrategy: state.focusStrategy as MenuFocusStrategy | null,
|
|
346
|
+
hide,
|
|
347
|
+
menuId: menuProps.id ?? '',
|
|
348
|
+
menuStyle,
|
|
349
|
+
menuTriggerProps,
|
|
350
|
+
overlayRef,
|
|
351
|
+
placementAttr,
|
|
352
|
+
reference,
|
|
353
|
+
registerOverlay,
|
|
354
|
+
show,
|
|
355
|
+
targetRef,
|
|
356
|
+
toggle: toggleVisible,
|
|
357
|
+
toggleNodeRef,
|
|
358
|
+
triggerId: menuTriggerProps.id ?? '',
|
|
359
|
+
visible: state.isOpen
|
|
360
|
+
}),
|
|
361
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
362
|
+
[
|
|
363
|
+
autoClose,
|
|
364
|
+
container,
|
|
365
|
+
state.focusStrategy,
|
|
366
|
+
menuProps.id,
|
|
367
|
+
menuStyle,
|
|
368
|
+
menuTriggerProps,
|
|
369
|
+
placementAttr,
|
|
370
|
+
reference,
|
|
371
|
+
state.isOpen
|
|
372
|
+
]
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<MenuContext.Provider value={contextValue}>
|
|
377
|
+
{Component === Fragment ? (
|
|
378
|
+
children
|
|
379
|
+
) : (
|
|
380
|
+
// `show` mirrors vanilla menu.js's `this._parent.classList.add('show')` — needed for
|
|
381
|
+
// e.g. `.nav-item.show .nav-link` to put a `NavLink`-rooted `MenuToggle` into its
|
|
382
|
+
// pressed look while open, since a bare `.nav-link.show` has no styling of its own.
|
|
383
|
+
<Component
|
|
384
|
+
className={classNames(className, { show: state.isOpen })}
|
|
385
|
+
{...rest}
|
|
386
|
+
ref={forkedRef}
|
|
387
|
+
>
|
|
388
|
+
{children}
|
|
389
|
+
</Component>
|
|
390
|
+
)}
|
|
391
|
+
</MenuContext.Provider>
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export const Menu = createPolymorphicComponent<MenuComponent>(
|
|
396
|
+
MenuRender as ForwardRefRenderFunction<Element, MenuProps<ElementType>>,
|
|
397
|
+
'Menu'
|
|
398
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React, { forwardRef, HTMLAttributes } from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
|
|
4
|
+
export interface MenuDividerProps extends HTMLAttributes<HTMLHRElement> {
|
|
5
|
+
/**
|
|
6
|
+
* A string of all className you want applied to the component.
|
|
7
|
+
*/
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const MenuDivider = forwardRef<HTMLHRElement, MenuDividerProps>(
|
|
12
|
+
({ className, ...rest }, ref) => {
|
|
13
|
+
const _className = classNames('menu-divider', className)
|
|
14
|
+
|
|
15
|
+
return <hr className={_className} {...rest} ref={ref} />
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
MenuDivider.displayName = 'MenuDivider'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { ElementType, ForwardRefRenderFunction, ReactElement } from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createPolymorphicComponent,
|
|
6
|
+
PolymorphicComponentProps,
|
|
7
|
+
PolymorphicRef
|
|
8
|
+
} from '../../utils/polymorphic'
|
|
9
|
+
|
|
10
|
+
type MenuHeaderOwnProps<C extends ElementType> = {
|
|
11
|
+
/**
|
|
12
|
+
* A string of all className you want applied to the component.
|
|
13
|
+
*/
|
|
14
|
+
className?: string
|
|
15
|
+
/**
|
|
16
|
+
* Component used for the root node. Either a string to use a HTML element or a component.
|
|
17
|
+
*/
|
|
18
|
+
component?: C
|
|
19
|
+
/**
|
|
20
|
+
* ARIA role applied to the root node. Defaults to `'presentation'` — see the render function
|
|
21
|
+
* for why.
|
|
22
|
+
*/
|
|
23
|
+
role?: React.AriaRole
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type MenuHeaderProps<C extends ElementType = 'h4'> = PolymorphicComponentProps<
|
|
27
|
+
C,
|
|
28
|
+
MenuHeaderOwnProps<C>
|
|
29
|
+
>
|
|
30
|
+
|
|
31
|
+
type MenuHeaderComponent = (<C extends ElementType = 'h4'>(
|
|
32
|
+
props: MenuHeaderProps<C> & { ref?: PolymorphicRef<C> }
|
|
33
|
+
) => ReactElement | null) & { displayName?: string }
|
|
34
|
+
|
|
35
|
+
function MenuHeaderRender<C extends ElementType = 'h4'>(
|
|
36
|
+
{ children, className, component, role = 'presentation', ...rest }: MenuHeaderProps<C>,
|
|
37
|
+
ref: PolymorphicRef<C>
|
|
38
|
+
) {
|
|
39
|
+
const Component = component ?? 'h4'
|
|
40
|
+
const _className = classNames('menu-header', className)
|
|
41
|
+
|
|
42
|
+
// Always rendered as a direct child of `role="menu"` (see MenuList) — ARIA's menu role only
|
|
43
|
+
// permits menuitem/group/separator children, so a bare heading fails aria-required-children.
|
|
44
|
+
// `role="presentation"` opts the element itself out without hiding its text content.
|
|
45
|
+
return (
|
|
46
|
+
<Component className={_className} role={role} {...rest} ref={ref}>
|
|
47
|
+
{children}
|
|
48
|
+
</Component>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const MenuHeader = createPolymorphicComponent<MenuHeaderComponent>(
|
|
53
|
+
MenuHeaderRender as ForwardRefRenderFunction<Element, MenuHeaderProps<ElementType>>,
|
|
54
|
+
'MenuHeader'
|
|
55
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ElementType,
|
|
3
|
+
ForwardRefRenderFunction,
|
|
4
|
+
MouseEvent,
|
|
5
|
+
ReactElement,
|
|
6
|
+
ReactNode
|
|
7
|
+
} from 'react'
|
|
8
|
+
import classNames from 'classnames'
|
|
9
|
+
|
|
10
|
+
import { createPolymorphicComponent, PolymorphicRef } from '../../utils/polymorphic'
|
|
11
|
+
import { renderMenuItemContent } from '../../utils/renderMenuItemContent'
|
|
12
|
+
import { LinkProps, Link } from '../link/Link'
|
|
13
|
+
|
|
14
|
+
type MenuItemOwnProps = {
|
|
15
|
+
/**
|
|
16
|
+
* A string of all className you want applied to the component.
|
|
17
|
+
*/
|
|
18
|
+
className?: string
|
|
19
|
+
/**
|
|
20
|
+
* Secondary line of text rendered below `children` (`.menu-item-description`).
|
|
21
|
+
*/
|
|
22
|
+
description?: ReactNode
|
|
23
|
+
/**
|
|
24
|
+
* Icon rendered at the item's leading edge (`.menu-item-icon`).
|
|
25
|
+
*/
|
|
26
|
+
icon?: ReactNode
|
|
27
|
+
/**
|
|
28
|
+
* Marks the item as the current selection in a choice list. Renders in a heavier font weight.
|
|
29
|
+
* Combine with a manually-composed `.menu-item-check` icon (in `children`, or via `icon` on a
|
|
30
|
+
* plain, non-selection item) to also show a checkmark — this prop alone doesn't add one, so
|
|
31
|
+
* existing font-weight-only usage keeps rendering unchanged.
|
|
32
|
+
*/
|
|
33
|
+
selected?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type MenuItemProps<C extends ElementType = 'a'> = LinkProps<C> & MenuItemOwnProps
|
|
37
|
+
|
|
38
|
+
type MenuItemComponent = (<C extends ElementType = 'a'>(
|
|
39
|
+
props: MenuItemProps<C> & { ref?: PolymorphicRef<C> }
|
|
40
|
+
) => ReactElement | null) & { displayName?: string }
|
|
41
|
+
|
|
42
|
+
function MenuItemRender<C extends ElementType = 'a'>(
|
|
43
|
+
{
|
|
44
|
+
children,
|
|
45
|
+
className,
|
|
46
|
+
component,
|
|
47
|
+
description,
|
|
48
|
+
href,
|
|
49
|
+
icon,
|
|
50
|
+
onClick,
|
|
51
|
+
selected,
|
|
52
|
+
...rest
|
|
53
|
+
}: MenuItemProps<C>,
|
|
54
|
+
ref: PolymorphicRef<C>
|
|
55
|
+
) {
|
|
56
|
+
const _className = classNames('menu-item', { selected }, className)
|
|
57
|
+
|
|
58
|
+
// `href="#"` is a common placeholder for menu items that act via `onClick` rather than
|
|
59
|
+
// real navigation. Left alone, a plain anchor click still navigates to the empty fragment,
|
|
60
|
+
// which scrolls the page to the top — so we suppress that default for the placeholder case
|
|
61
|
+
// only, leaving real same-page anchors (`href="#some-id"`) free to navigate as expected.
|
|
62
|
+
const handleClick = (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
|
|
63
|
+
if (href === '#') event.preventDefault()
|
|
64
|
+
onClick?.(event)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Link
|
|
69
|
+
role="menuitem"
|
|
70
|
+
component={component as ElementType}
|
|
71
|
+
href={href}
|
|
72
|
+
onClick={handleClick}
|
|
73
|
+
{...(rest as Record<string, unknown>)}
|
|
74
|
+
className={_className}
|
|
75
|
+
ref={ref as PolymorphicRef<ElementType>}
|
|
76
|
+
>
|
|
77
|
+
{renderMenuItemContent({ icon, label: children, description })}
|
|
78
|
+
</Link>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const MenuItem = createPolymorphicComponent<MenuItemComponent>(
|
|
83
|
+
MenuItemRender as ForwardRefRenderFunction<Element, MenuItemProps<ElementType>>,
|
|
84
|
+
'MenuItem'
|
|
85
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
import { Key } from 'react-stately'
|
|
3
|
+
|
|
4
|
+
export interface MenuItemDef {
|
|
5
|
+
/**
|
|
6
|
+
* Discriminates this entry from `MenuHeaderDef`/`MenuDividerDef` in a `MenuItemsDef`
|
|
7
|
+
* array. Omit for a normal item — it's the default.
|
|
8
|
+
*/
|
|
9
|
+
type?: 'item'
|
|
10
|
+
/**
|
|
11
|
+
* React key for this item.
|
|
12
|
+
*/
|
|
13
|
+
id: Key
|
|
14
|
+
/**
|
|
15
|
+
* Label content for the item.
|
|
16
|
+
*/
|
|
17
|
+
label: ReactNode
|
|
18
|
+
/**
|
|
19
|
+
* Text used for filtering/typeahead. Required when `label` isn't a plain string — components
|
|
20
|
+
* that filter (e.g. `Combobox`, `Autocomplete`) can't derive it from rich content.
|
|
21
|
+
*/
|
|
22
|
+
textValue?: string
|
|
23
|
+
/**
|
|
24
|
+
* URL for the item, when it acts as a link.
|
|
25
|
+
*/
|
|
26
|
+
href?: string
|
|
27
|
+
/**
|
|
28
|
+
* Callback fired when the item is activated.
|
|
29
|
+
*/
|
|
30
|
+
onClick?: () => void
|
|
31
|
+
/**
|
|
32
|
+
* Marks the item as disabled — it can't be clicked or reached via the keyboard.
|
|
33
|
+
*/
|
|
34
|
+
disabled?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Marks the item as the current selection in a choice list.
|
|
37
|
+
*/
|
|
38
|
+
selected?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Icon rendered at the item's leading edge (`.menu-item-icon`).
|
|
41
|
+
*/
|
|
42
|
+
icon?: ReactNode
|
|
43
|
+
/**
|
|
44
|
+
* Secondary line of text rendered below the label (`.menu-item-description`).
|
|
45
|
+
*/
|
|
46
|
+
description?: ReactNode
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MenuHeaderDef {
|
|
50
|
+
/**
|
|
51
|
+
* Discriminates this entry as a non-interactive group header.
|
|
52
|
+
*/
|
|
53
|
+
type: 'header'
|
|
54
|
+
/**
|
|
55
|
+
* React key for this entry.
|
|
56
|
+
*/
|
|
57
|
+
id: Key
|
|
58
|
+
/**
|
|
59
|
+
* Header content.
|
|
60
|
+
*/
|
|
61
|
+
label: ReactNode
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface MenuDividerDef {
|
|
65
|
+
/**
|
|
66
|
+
* Discriminates this entry as a divider.
|
|
67
|
+
*/
|
|
68
|
+
type: 'divider'
|
|
69
|
+
/**
|
|
70
|
+
* React key for this entry.
|
|
71
|
+
*/
|
|
72
|
+
id: Key
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A flat array of item/header/divider definitions for data-driven rendering — headers and
|
|
77
|
+
* dividers are interleaved with items in authoring order, matching how chassis-css itself
|
|
78
|
+
* renders grouped items (flat DOM siblings under `.menu`, no wrapping element per group).
|
|
79
|
+
*
|
|
80
|
+
* Covers flat items, headers, and dividers only — nested/recursive submenus aren't
|
|
81
|
+
* representable here. Compose with `children`/`MenuSubmenu` directly for those.
|
|
82
|
+
*/
|
|
83
|
+
export type MenuItemsDef = (MenuItemDef | MenuHeaderDef | MenuDividerDef)[]
|