@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.
Files changed (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/index.d.ts +5909 -0
  4. package/dist/index.js +2 -0
  5. package/dist/style.css +1 -0
  6. package/package.json +114 -0
  7. package/src/components/accordion/Accordion.tsx +190 -0
  8. package/src/components/accordion/AccordionBody.tsx +46 -0
  9. package/src/components/accordion/AccordionHeader.tsx +21 -0
  10. package/src/components/accordion/AccordionItem.tsx +59 -0
  11. package/src/components/accordion/context.ts +8 -0
  12. package/src/components/accordion/index.ts +8 -0
  13. package/src/components/autocomplete/Autocomplete.tsx +464 -0
  14. package/src/components/autocomplete/AutocompleteGroup.tsx +20 -0
  15. package/src/components/autocomplete/AutocompleteItem.tsx +18 -0
  16. package/src/components/autocomplete/index.ts +6 -0
  17. package/src/components/avatar/Avatar.tsx +134 -0
  18. package/src/components/avatar/AvatarImage.tsx +19 -0
  19. package/src/components/avatar/AvatarStack.tsx +98 -0
  20. package/src/components/avatar/index.ts +6 -0
  21. package/src/components/badge/Badge.tsx +94 -0
  22. package/src/components/badge/index.ts +2 -0
  23. package/src/components/breadcrumb/Breadcrumb.tsx +59 -0
  24. package/src/components/breadcrumb/BreadcrumbItem.tsx +38 -0
  25. package/src/components/breadcrumb/index.ts +4 -0
  26. package/src/components/button/Button.tsx +229 -0
  27. package/src/components/button/index.ts +2 -0
  28. package/src/components/button-group/ButtonGroup.tsx +55 -0
  29. package/src/components/button-group/ButtonToolbar.tsx +47 -0
  30. package/src/components/button-group/index.ts +4 -0
  31. package/src/components/calendar/Calendar.scss +57 -0
  32. package/src/components/calendar/Calendar.tsx +324 -0
  33. package/src/components/calendar/CalendarMonthBlock.tsx +57 -0
  34. package/src/components/calendar/CalendarMonthGrid.tsx +92 -0
  35. package/src/components/calendar/CalendarMonthYearPicker.tsx +162 -0
  36. package/src/components/calendar/CalendarNavButton.tsx +24 -0
  37. package/src/components/calendar/CalendarWeekGrid.tsx +58 -0
  38. package/src/components/calendar/CalendarYearGrid.tsx +126 -0
  39. package/src/components/calendar/RangeCalendar.scss +95 -0
  40. package/src/components/calendar/RangeCalendar.tsx +361 -0
  41. package/src/components/calendar/index.ts +5 -0
  42. package/src/components/card/Card.tsx +173 -0
  43. package/src/components/card/CardBody.tsx +75 -0
  44. package/src/components/card/CardFooter.tsx +47 -0
  45. package/src/components/card/CardGroup.tsx +51 -0
  46. package/src/components/card/CardHeader.tsx +47 -0
  47. package/src/components/card/CardImage.tsx +70 -0
  48. package/src/components/card/CardImageOverlay.tsx +47 -0
  49. package/src/components/card/CardLink.tsx +40 -0
  50. package/src/components/card/CardSubtitle.tsx +47 -0
  51. package/src/components/card/CardText.tsx +47 -0
  52. package/src/components/card/CardTitle.tsx +47 -0
  53. package/src/components/card/index.ts +22 -0
  54. package/src/components/carousel/Carousel.tsx +742 -0
  55. package/src/components/carousel/CarouselControlButton.tsx +74 -0
  56. package/src/components/carousel/CarouselControlNext.tsx +22 -0
  57. package/src/components/carousel/CarouselControlPrev.tsx +22 -0
  58. package/src/components/carousel/CarouselIndicators.tsx +44 -0
  59. package/src/components/carousel/CarouselInner.tsx +56 -0
  60. package/src/components/carousel/CarouselItem.tsx +51 -0
  61. package/src/components/carousel/CarouselOverlay.tsx +51 -0
  62. package/src/components/carousel/CarouselPlayPause.tsx +61 -0
  63. package/src/components/carousel/carouselEngine.ts +170 -0
  64. package/src/components/carousel/context.ts +29 -0
  65. package/src/components/carousel/index.ts +21 -0
  66. package/src/components/checkbox/Checkbox.tsx +236 -0
  67. package/src/components/checkbox/CheckboxGroup.tsx +145 -0
  68. package/src/components/checkbox/context.ts +14 -0
  69. package/src/components/checkbox/index.ts +4 -0
  70. package/src/components/chip/Chip.tsx +183 -0
  71. package/src/components/chip/index.ts +2 -0
  72. package/src/components/chip-input/ChipInput.tsx +363 -0
  73. package/src/components/chip-input/ChipList.tsx +95 -0
  74. package/src/components/chip-input/index.ts +2 -0
  75. package/src/components/close-button/CloseButton.tsx +205 -0
  76. package/src/components/close-button/index.ts +2 -0
  77. package/src/components/collapse/Collapse.tsx +128 -0
  78. package/src/components/collapse/index.ts +2 -0
  79. package/src/components/color-input/ColorInput.tsx +106 -0
  80. package/src/components/color-input/index.ts +2 -0
  81. package/src/components/combobox/Combobox.tsx +320 -0
  82. package/src/components/combobox/ComboboxGroup.tsx +27 -0
  83. package/src/components/combobox/ComboboxItem.tsx +44 -0
  84. package/src/components/combobox/ComboboxListBox.tsx +98 -0
  85. package/src/components/combobox/index.ts +6 -0
  86. package/src/components/datepicker/CalendarToggleButton.tsx +58 -0
  87. package/src/components/datepicker/ClearButton.tsx +35 -0
  88. package/src/components/datepicker/DateField.tsx +54 -0
  89. package/src/components/datepicker/DatePicker.scss +51 -0
  90. package/src/components/datepicker/DatePicker.tsx +574 -0
  91. package/src/components/datepicker/DateRangePicker.scss +11 -0
  92. package/src/components/datepicker/DateRangePicker.tsx +343 -0
  93. package/src/components/datepicker/index.ts +4 -0
  94. package/src/components/datepicker/renderDatePickerShell.tsx +104 -0
  95. package/src/components/drawer/Drawer.tsx +211 -0
  96. package/src/components/drawer/DrawerBody.tsx +47 -0
  97. package/src/components/drawer/DrawerFooter.tsx +51 -0
  98. package/src/components/drawer/DrawerHeader.tsx +60 -0
  99. package/src/components/drawer/DrawerTitle.tsx +49 -0
  100. package/src/components/drawer/index.ts +10 -0
  101. package/src/components/file-input/FileInput.tsx +100 -0
  102. package/src/components/file-input/index.ts +2 -0
  103. package/src/components/flex/Flex.tsx +134 -0
  104. package/src/components/flex/index.ts +2 -0
  105. package/src/components/floating-input/FloatingInput.tsx +94 -0
  106. package/src/components/floating-input/index.ts +2 -0
  107. package/src/components/form/Form.tsx +26 -0
  108. package/src/components/form/FormFeedback.tsx +64 -0
  109. package/src/components/form/FormHelp.tsx +46 -0
  110. package/src/components/form/FormLabel.tsx +26 -0
  111. package/src/components/form/index.ts +8 -0
  112. package/src/components/form/renderFormCheck.tsx +86 -0
  113. package/src/components/form-field/FormField.tsx +84 -0
  114. package/src/components/form-field/index.ts +2 -0
  115. package/src/components/form-field/renderFormField.tsx +74 -0
  116. package/src/components/grid/Col.tsx +69 -0
  117. package/src/components/grid/Container.tsx +59 -0
  118. package/src/components/grid/Grid.tsx +86 -0
  119. package/src/components/grid/GridItem.tsx +123 -0
  120. package/src/components/grid/Row.tsx +71 -0
  121. package/src/components/grid/gap.ts +8 -0
  122. package/src/components/grid/index.ts +10 -0
  123. package/src/components/icon/Icon.tsx +74 -0
  124. package/src/components/icon/index.ts +2 -0
  125. package/src/components/input-adorn/InputAdorn.tsx +55 -0
  126. package/src/components/input-adorn/index.ts +2 -0
  127. package/src/components/input-group/InputGroup.tsx +50 -0
  128. package/src/components/input-group/InputGroupAddon.tsx +50 -0
  129. package/src/components/input-group/index.ts +4 -0
  130. package/src/components/link/Link.tsx +148 -0
  131. package/src/components/link/index.ts +2 -0
  132. package/src/components/list/List.tsx +193 -0
  133. package/src/components/list/ListItem.tsx +91 -0
  134. package/src/components/list/index.ts +4 -0
  135. package/src/components/menu/Menu.tsx +398 -0
  136. package/src/components/menu/MenuDivider.tsx +19 -0
  137. package/src/components/menu/MenuHeader.tsx +55 -0
  138. package/src/components/menu/MenuItem.tsx +85 -0
  139. package/src/components/menu/MenuItemDef.ts +83 -0
  140. package/src/components/menu/MenuList.tsx +142 -0
  141. package/src/components/menu/MenuSubmenu.tsx +284 -0
  142. package/src/components/menu/MenuSubmenuBack.tsx +39 -0
  143. package/src/components/menu/MenuText.tsx +47 -0
  144. package/src/components/menu/MenuToggle.tsx +148 -0
  145. package/src/components/menu/index.ts +19 -0
  146. package/src/components/menu/menuNavigation.ts +106 -0
  147. package/src/components/menu/submenuGroup.ts +43 -0
  148. package/src/components/modal/Modal.tsx +162 -0
  149. package/src/components/modal/ModalBody.tsx +47 -0
  150. package/src/components/modal/ModalFooter.tsx +51 -0
  151. package/src/components/modal/ModalHeader.tsx +60 -0
  152. package/src/components/modal/ModalTitle.tsx +49 -0
  153. package/src/components/modal/index.ts +10 -0
  154. package/src/components/nav/Nav.tsx +97 -0
  155. package/src/components/nav/NavItem.tsx +42 -0
  156. package/src/components/nav/NavLink.tsx +48 -0
  157. package/src/components/nav/NavTitle.tsx +22 -0
  158. package/src/components/nav/index.ts +7 -0
  159. package/src/components/navbar/Navbar.tsx +112 -0
  160. package/src/components/navbar/NavbarBrand.tsx +55 -0
  161. package/src/components/navbar/NavbarNav.tsx +47 -0
  162. package/src/components/navbar/NavbarText.tsx +47 -0
  163. package/src/components/navbar/NavbarToggler.tsx +34 -0
  164. package/src/components/navbar/index.ts +10 -0
  165. package/src/components/notification/Notification.scss +15 -0
  166. package/src/components/notification/Notification.tsx +187 -0
  167. package/src/components/notification/NotificationIcon.tsx +21 -0
  168. package/src/components/notification/NotificationStack.tsx +66 -0
  169. package/src/components/notification/NotificationText.tsx +47 -0
  170. package/src/components/notification/NotificationTitle.tsx +47 -0
  171. package/src/components/notification/context.ts +14 -0
  172. package/src/components/notification/index.ts +17 -0
  173. package/src/components/notification/notificationQueue.ts +48 -0
  174. package/src/components/otp-input/OtpBox.tsx +68 -0
  175. package/src/components/otp-input/OtpInput.tsx +341 -0
  176. package/src/components/otp-input/index.ts +2 -0
  177. package/src/components/pagination/Pagination.tsx +199 -0
  178. package/src/components/pagination/PaginationItem.tsx +133 -0
  179. package/src/components/pagination/index.ts +4 -0
  180. package/src/components/password-strength/PasswordStrength.tsx +155 -0
  181. package/src/components/password-strength/index.ts +2 -0
  182. package/src/components/password-strength/strengthScore.ts +79 -0
  183. package/src/components/placeholder/Placeholder.tsx +178 -0
  184. package/src/components/placeholder/index.ts +2 -0
  185. package/src/components/popover/Popover.tsx +243 -0
  186. package/src/components/popover/index.ts +2 -0
  187. package/src/components/progress/Progress.tsx +154 -0
  188. package/src/components/progress/ProgressBar.tsx +88 -0
  189. package/src/components/progress/index.ts +4 -0
  190. package/src/components/radio/Radio.tsx +88 -0
  191. package/src/components/radio/RadioGroup.tsx +142 -0
  192. package/src/components/radio/context.ts +14 -0
  193. package/src/components/radio/index.ts +4 -0
  194. package/src/components/range-input/RangeInput.tsx +109 -0
  195. package/src/components/range-input/index.ts +2 -0
  196. package/src/components/select/Select.tsx +277 -0
  197. package/src/components/select/index.ts +2 -0
  198. package/src/components/skeleton/Skeleton.tsx +89 -0
  199. package/src/components/skeleton/SkeletonLoader.tsx +91 -0
  200. package/src/components/skeleton/index.ts +4 -0
  201. package/src/components/spinner/Spinner.tsx +77 -0
  202. package/src/components/spinner/index.ts +2 -0
  203. package/src/components/stack/Stack.tsx +81 -0
  204. package/src/components/stack/index.ts +2 -0
  205. package/src/components/stepper/Stepper.tsx +172 -0
  206. package/src/components/stepper/StepperItem.tsx +66 -0
  207. package/src/components/stepper/index.ts +4 -0
  208. package/src/components/switch/Switch.tsx +246 -0
  209. package/src/components/switch/index.ts +2 -0
  210. package/src/components/table/Table.css +39 -0
  211. package/src/components/table/Table.tsx +390 -0
  212. package/src/components/table/TableBody.tsx +25 -0
  213. package/src/components/table/TableCell.tsx +27 -0
  214. package/src/components/table/TableColumn.tsx +31 -0
  215. package/src/components/table/TableHeader.tsx +26 -0
  216. package/src/components/table/TableRow.tsx +24 -0
  217. package/src/components/table/index.ts +12 -0
  218. package/src/components/tabs/Tab.tsx +36 -0
  219. package/src/components/tabs/TabList.tsx +76 -0
  220. package/src/components/tabs/TabPanel.tsx +68 -0
  221. package/src/components/tabs/Tabs.tsx +136 -0
  222. package/src/components/tabs/context.ts +24 -0
  223. package/src/components/tabs/index.ts +8 -0
  224. package/src/components/text-input/TextInput.tsx +188 -0
  225. package/src/components/text-input/index.ts +2 -0
  226. package/src/components/textarea/Textarea.tsx +145 -0
  227. package/src/components/textarea/index.ts +2 -0
  228. package/src/components/toast/Toast.tsx +219 -0
  229. package/src/components/toast/ToastBody.tsx +64 -0
  230. package/src/components/toast/ToastFooter.tsx +47 -0
  231. package/src/components/toast/ToastHeader.tsx +104 -0
  232. package/src/components/toast/ToastIcon.tsx +21 -0
  233. package/src/components/toast/Toaster.tsx +87 -0
  234. package/src/components/toast/context.ts +14 -0
  235. package/src/components/toast/index.ts +18 -0
  236. package/src/components/toast/toastQueue.ts +56 -0
  237. package/src/components/tooltip/Tooltip.tsx +155 -0
  238. package/src/components/tooltip/index.ts +2 -0
  239. package/src/global.d.ts +2 -0
  240. package/src/hooks/index.ts +57 -0
  241. package/src/hooks/useAutoDismiss.ts +88 -0
  242. package/src/hooks/useButtonSemantics.ts +33 -0
  243. package/src/hooks/useControllableState.ts +24 -0
  244. package/src/hooks/useDialogElement.ts +216 -0
  245. package/src/hooks/useDisabledAnchorGuard.ts +22 -0
  246. package/src/hooks/useDismissibleTransition.ts +126 -0
  247. package/src/hooks/useDrawer.ts +9 -0
  248. package/src/hooks/useFloatingOverlay.ts +115 -0
  249. package/src/hooks/useForkedRef.ts +50 -0
  250. package/src/hooks/useFormField.ts +89 -0
  251. package/src/hooks/useIsomorphicLayoutEffect.ts +9 -0
  252. package/src/hooks/useModal.ts +9 -0
  253. package/src/hooks/useNotification.ts +10 -0
  254. package/src/hooks/useOverlayPlacement.ts +93 -0
  255. package/src/hooks/usePagination.ts +85 -0
  256. package/src/hooks/useToast.ts +9 -0
  257. package/src/hooks/useToastRegionQueue.ts +27 -0
  258. package/src/index.ts +276 -0
  259. package/src/types.ts +133 -0
  260. package/src/utils/breakpoints.ts +43 -0
  261. package/src/utils/comboboxCollection.tsx +152 -0
  262. package/src/utils/dateRangePresets.ts +6 -0
  263. package/src/utils/dialogTransition.ts +39 -0
  264. package/src/utils/focusRedirect.ts +29 -0
  265. package/src/utils/isWholeUnitDisabled.ts +17 -0
  266. package/src/utils/mergeIsDateUnavailable.ts +15 -0
  267. package/src/utils/overlayPlacement.ts +93 -0
  268. package/src/utils/pointerInteraction.ts +21 -0
  269. package/src/utils/polymorphic.ts +36 -0
  270. package/src/utils/renderMenuItemContent.tsx +62 -0
  271. package/src/utils/setVisibleRangeStart.ts +64 -0
  272. package/src/utils/spacingClassName.ts +14 -0
  273. package/src/utils/suppressFocusRingGlobally.ts +69 -0
  274. package/src/utils/validationClassName.ts +8 -0
  275. 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)[]