@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,14 @@
1
+ import { createContext } from 'react'
2
+
3
+ export interface ToastContextProps {
4
+ /**
5
+ * Whether the toast is currently visible.
6
+ */
7
+ visible?: boolean
8
+ /**
9
+ * Dismisses the toast. Wire this to any element's `onClick` — see `useToast`.
10
+ */
11
+ close: () => void
12
+ }
13
+
14
+ export const ToastContext = createContext<ToastContextProps>({ close: () => {} })
@@ -0,0 +1,18 @@
1
+ export { Toast } from './Toast'
2
+ export type { ToastProps } from './Toast'
3
+ export { ToastBody } from './ToastBody'
4
+ export type { ToastBodyProps } from './ToastBody'
5
+ export { ToastFooter } from './ToastFooter'
6
+ export type { ToastFooterProps } from './ToastFooter'
7
+ export { ToastHeader } from './ToastHeader'
8
+ export type { ToastHeaderProps } from './ToastHeader'
9
+ export { ToastIcon } from './ToastIcon'
10
+ export type { ToastIconProps } from './ToastIcon'
11
+
12
+ // Toaster is an independent manager/container component (react-hot-toast-style), not a Toast
13
+ // sub-part — it subscribes to the shared toastQueue and renders Toast instances from it, rather
14
+ // than being composed as a child of Toast. Stays a separate top-level export.
15
+ export { Toaster } from './Toaster'
16
+ export type { ToasterProps } from './Toaster'
17
+ export { addToast, closeToast, toastQueue } from './toastQueue'
18
+ export type { ToastContent } from './toastQueue'
@@ -0,0 +1,56 @@
1
+ import { ReactNode } from 'react'
2
+ import { ToastQueue } from 'react-stately'
3
+
4
+ import { ContextColor } from '../../types'
5
+
6
+ export interface ToastContent {
7
+ /**
8
+ * Apply a CSS fade transition to the toast.
9
+ */
10
+ animation?: boolean
11
+ /**
12
+ * Auto hide the toast. The timer starts once the show transition completes and pauses
13
+ * while the pointer is over the toast or focus is within it.
14
+ */
15
+ autohide?: boolean
16
+ /**
17
+ * Content of the toast — typically a `Toast.Header`/`Toast.Body`/`Toast.Footer`.
18
+ */
19
+ children: ReactNode
20
+ /**
21
+ * Sets the color of the component to one of Chassis context colors.
22
+ */
23
+ color?: ContextColor
24
+ /**
25
+ * Delay hiding the toast (ms).
26
+ */
27
+ delay?: number
28
+ /**
29
+ * Apply a full-color background with inverted text. Only meaningful alongside `color`.
30
+ */
31
+ solid?: boolean
32
+ /**
33
+ * Apply a semi-transparent background.
34
+ */
35
+ translucent?: boolean
36
+ }
37
+
38
+ // The queue backing `Toaster` — a module-level singleton so `addToast()` is callable from
39
+ // anywhere (an event handler, an async callback) without threading a `push` prop through
40
+ // render. `Toaster` subscribes to it via `useToastQueue`; nothing renders until a
41
+ // `<Toaster />` is actually mounted somewhere to display the queue's contents.
42
+ export const toastQueue = new ToastQueue<ToastContent>()
43
+
44
+ // Adds a toast to the queue. Returns the toast's key, which can be passed to `closeToast` to
45
+ // dismiss it programmatically.
46
+ export function addToast(
47
+ children: ReactNode,
48
+ options: Omit<ToastContent, 'children'> = {}
49
+ ): string {
50
+ return toastQueue.add({ children, ...options })
51
+ }
52
+
53
+ // Removes a queued toast immediately, bypassing its own exit animation.
54
+ export function closeToast(key: string): void {
55
+ toastQueue.close(key)
56
+ }
@@ -0,0 +1,155 @@
1
+ import React, { FC, ReactElement, ReactNode, useRef } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import classNames from 'classnames'
4
+ import { mergeProps, useOverlayPosition, useTooltip, useTooltipTrigger } from 'react-aria'
5
+ import { useTooltipTriggerState } from 'react-stately'
6
+ import { Transition } from 'react-transition-group'
7
+
8
+ import { getOverlayArrowStyle, getOverlayTransitionClass, useFloatingOverlay } from '../../hooks'
9
+ import { Placement, resolveDataPlacement, toAriaPlacement } from '../../utils/overlayPlacement'
10
+
11
+ export type { Placement }
12
+
13
+ export interface TooltipProps {
14
+ children: ReactElement
15
+ /**
16
+ * Content node for your component.
17
+ */
18
+ content: ReactNode | string
19
+ /**
20
+ * Offset of the tooltip relative to its target, as `[crossAxis, mainAxis]`.
21
+ */
22
+ offset?: [number, number]
23
+ /**
24
+ * Callback fired when the component requests to be hidden.
25
+ */
26
+ onHide?: () => void
27
+ /**
28
+ * Callback fired when the component requests to be shown.
29
+ */
30
+ onShow?: () => void
31
+ /**
32
+ * Describes the preferred placement of your component. Chassis will flip it to keep it in
33
+ * view.
34
+ */
35
+ placement?: Placement
36
+ /**
37
+ * Tooltips always show on focus, since keyboard/screen-reader users need them too. Set to
38
+ * `'focus'` to disable the hover trigger and show on focus only.
39
+ */
40
+ trigger?: 'hover' | 'focus'
41
+ /**
42
+ * Toggle the visibility of the tooltip component.
43
+ */
44
+ visible?: boolean
45
+ }
46
+
47
+ export const Tooltip: FC<TooltipProps> = ({
48
+ children,
49
+ content,
50
+ placement = 'top',
51
+ offset: offsetProp = [0, 6],
52
+ onHide,
53
+ onShow,
54
+ trigger,
55
+ visible,
56
+ ...rest
57
+ }) => {
58
+ const arrowRef = useRef<HTMLDivElement>(null)
59
+ const triggerRef = useRef<HTMLElement | null>(null)
60
+ const floatingRef = useRef<HTMLDivElement>(null)
61
+
62
+ // react-stately defaults to a 1500ms warmup delay (and 500ms cooldown) before a first tooltip
63
+ // shows, spectrum-style — chassis-css's own JS plugin defaults to instant (`delay: 0`), so
64
+ // match that here rather than leaving new adopters to wonder why the first hover lags.
65
+ const state = useTooltipTriggerState({ trigger, delay: 0, closeDelay: 0 })
66
+ const { triggerProps, tooltipProps: tooltipTriggerProps } = useTooltipTrigger(
67
+ { trigger },
68
+ state,
69
+ triggerRef
70
+ )
71
+ const { tooltipProps } = useTooltip({}, state)
72
+
73
+ // Bypasses the hook's hover-warmup delay (`open`/`close`'s `immediate` argument) since a
74
+ // programmatic `visible` change, an `onShow`/`onHide` sync, or a containing dialog closing
75
+ // should all apply right away, same as before.
76
+ const portalContainer = useFloatingOverlay({
77
+ close: () => state.close(true),
78
+ isOpen: state.isOpen,
79
+ open: () => state.open(true),
80
+ onHide,
81
+ onShow,
82
+ triggerRef,
83
+ visible
84
+ })
85
+
86
+ const {
87
+ overlayProps,
88
+ arrowProps,
89
+ placement: resolvedPlacement
90
+ } = useOverlayPosition({
91
+ targetRef: triggerRef,
92
+ overlayRef: floatingRef,
93
+ placement: toAriaPlacement(placement),
94
+ offset: offsetProp[1],
95
+ crossOffset: offsetProp[0],
96
+ arrowSize: 8,
97
+ arrowRef,
98
+ isOpen: state.isOpen
99
+ })
100
+
101
+ const floatingStyle: React.CSSProperties = {
102
+ position: overlayProps.style?.position as React.CSSProperties['position'],
103
+ top: overlayProps.style?.top,
104
+ left: overlayProps.style?.left
105
+ }
106
+ const placementAttr = resolveDataPlacement(placement, resolvedPlacement)
107
+
108
+ return (
109
+ <>
110
+ {React.cloneElement(children, {
111
+ ref: (node: HTMLElement | null) => {
112
+ triggerRef.current = node
113
+ },
114
+ ...triggerProps
115
+ })}
116
+ {typeof window !== 'undefined' &&
117
+ createPortal(
118
+ <Transition
119
+ in={state.isOpen}
120
+ mountOnEnter
121
+ nodeRef={floatingRef}
122
+ timeout={{
123
+ enter: 0,
124
+ exit: 200
125
+ }}
126
+ unmountOnExit
127
+ >
128
+ {(transitionState) => {
129
+ const transitionClass = getOverlayTransitionClass(transitionState)
130
+ return (
131
+ <div
132
+ className={classNames('tooltip cx-tooltip-auto', transitionClass)}
133
+ data-cx-placement={placementAttr}
134
+ ref={floatingRef}
135
+ style={floatingStyle}
136
+ {...mergeProps(tooltipTriggerProps, tooltipProps)}
137
+ {...rest}
138
+ >
139
+ <div
140
+ className="tooltip-arrow"
141
+ {...arrowProps}
142
+ style={getOverlayArrowStyle(arrowProps)}
143
+ ></div>
144
+ <div className="tooltip-inner">{content}</div>
145
+ </div>
146
+ )
147
+ }}
148
+ </Transition>,
149
+ portalContainer ?? document.body
150
+ )}
151
+ </>
152
+ )
153
+ }
154
+
155
+ Tooltip.displayName = 'Tooltip'
@@ -0,0 +1,2 @@
1
+ export { Tooltip } from './Tooltip'
2
+ export type { TooltipProps } from './Tooltip'
@@ -0,0 +1,2 @@
1
+ declare module '*.css'
2
+ declare module '*.scss'
@@ -0,0 +1,57 @@
1
+ import { useAutoDismiss } from './useAutoDismiss'
2
+ import { useButtonSemantics } from './useButtonSemantics'
3
+ import { useControllableState } from './useControllableState'
4
+ import { useDialogElement } from './useDialogElement'
5
+ import { useDisabledAnchorGuard } from './useDisabledAnchorGuard'
6
+ import { useDismissibleTransition } from './useDismissibleTransition'
7
+ import { useDrawer } from './useDrawer'
8
+ import {
9
+ getOverlayArrowStyle,
10
+ getOverlayTransitionClass,
11
+ useFloatingOverlay
12
+ } from './useFloatingOverlay'
13
+ import { useForkedRef } from './useForkedRef'
14
+ import { useFormField } from './useFormField'
15
+ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
16
+ import { useModal } from './useModal'
17
+ import { useNotification } from './useNotification'
18
+ import { useOverlayPlacement } from './useOverlayPlacement'
19
+ import { usePagination } from './usePagination'
20
+ import { useToast } from './useToast'
21
+ import { useToastRegionQueue } from './useToastRegionQueue'
22
+
23
+ export {
24
+ getOverlayArrowStyle,
25
+ getOverlayTransitionClass,
26
+ useAutoDismiss,
27
+ useButtonSemantics,
28
+ useControllableState,
29
+ useDialogElement,
30
+ useDisabledAnchorGuard,
31
+ useDismissibleTransition,
32
+ useDrawer,
33
+ useFloatingOverlay,
34
+ useForkedRef,
35
+ useFormField,
36
+ useIsomorphicLayoutEffect,
37
+ useModal,
38
+ useNotification,
39
+ useOverlayPlacement,
40
+ usePagination,
41
+ useToast,
42
+ useToastRegionQueue
43
+ }
44
+ export type { UseAutoDismissOptions, UseAutoDismissResult } from './useAutoDismiss'
45
+ export type { UseDialogElementOptions } from './useDialogElement'
46
+ export type {
47
+ UseDismissibleTransitionOptions,
48
+ UseDismissibleTransitionResult
49
+ } from './useDismissibleTransition'
50
+ export type { UseDrawerResult } from './useDrawer'
51
+ export type { UseFloatingOverlayOptions } from './useFloatingOverlay'
52
+ export type { UseFormFieldOptions, UseFormFieldResult } from './useFormField'
53
+ export type { UseModalResult } from './useModal'
54
+ export type { UseNotificationResult } from './useNotification'
55
+ export type { UsePaginationResult } from './usePagination'
56
+ export type { UseToastResult } from './useToast'
57
+ export type { UseToastRegionQueueResult } from './useToastRegionQueue'
@@ -0,0 +1,88 @@
1
+ import { useCallback, useEffect, useRef } from 'react'
2
+
3
+ export interface UseAutoDismissOptions {
4
+ /**
5
+ * Enables the auto-dismiss timer. When `false`, `onHide` is never called and the returned
6
+ * handlers are no-ops.
7
+ */
8
+ enabled?: boolean
9
+ /**
10
+ * Delay in ms before `onHide` fires once the timer is (re)started.
11
+ */
12
+ delay?: number
13
+ /**
14
+ * Whether the dismissible element is currently visible. The timer only runs while `true`.
15
+ */
16
+ visible: boolean
17
+ /**
18
+ * Called once the delay elapses without an intervening pointer/focus interaction.
19
+ */
20
+ onHide: () => void
21
+ }
22
+
23
+ export interface UseAutoDismissResult {
24
+ onMouseEnter: () => void
25
+ onMouseLeave: () => void
26
+ onFocus: () => void
27
+ onBlur: () => void
28
+ }
29
+
30
+ // Shared timer logic for components that auto-dismiss after a delay, pausing while the pointer
31
+ // or focus is on the element (mirrors the behavior `Toast` already implements). Extracted so new
32
+ // auto-dismissing components don't reimplement the pause/resume bookkeeping.
33
+ export function useAutoDismiss({
34
+ enabled = false,
35
+ delay = 5000,
36
+ visible,
37
+ onHide
38
+ }: UseAutoDismissOptions): UseAutoDismissResult {
39
+ const timeout = useRef<number>()
40
+ const hasMouseInteraction = useRef(false)
41
+ const hasKeyboardInteraction = useRef(false)
42
+
43
+ const clearAutoDismissTimeout = useCallback(() => {
44
+ clearTimeout(timeout.current)
45
+ timeout.current = undefined
46
+ }, [])
47
+
48
+ const maybeScheduleHide = useCallback(() => {
49
+ clearAutoDismissTimeout()
50
+ if (!enabled || hasMouseInteraction.current || hasKeyboardInteraction.current) {
51
+ return
52
+ }
53
+ timeout.current = window.setTimeout(onHide, delay)
54
+ }, [enabled, delay, onHide, clearAutoDismissTimeout])
55
+
56
+ // Clears on unmount.
57
+ useEffect(() => clearAutoDismissTimeout, [clearAutoDismissTimeout])
58
+
59
+ useEffect(() => {
60
+ if (visible) {
61
+ maybeScheduleHide()
62
+ } else {
63
+ clearAutoDismissTimeout()
64
+ }
65
+ }, [visible, enabled, delay, maybeScheduleHide, clearAutoDismissTimeout])
66
+
67
+ const onMouseEnter = useCallback(() => {
68
+ hasMouseInteraction.current = true
69
+ clearAutoDismissTimeout()
70
+ }, [clearAutoDismissTimeout])
71
+
72
+ const onMouseLeave = useCallback(() => {
73
+ hasMouseInteraction.current = false
74
+ maybeScheduleHide()
75
+ }, [maybeScheduleHide])
76
+
77
+ const onFocus = useCallback(() => {
78
+ hasKeyboardInteraction.current = true
79
+ clearAutoDismissTimeout()
80
+ }, [clearAutoDismissTimeout])
81
+
82
+ const onBlur = useCallback(() => {
83
+ hasKeyboardInteraction.current = false
84
+ maybeScheduleHide()
85
+ }, [maybeScheduleHide])
86
+
87
+ return { onMouseEnter, onMouseLeave, onFocus, onBlur }
88
+ }
@@ -0,0 +1,33 @@
1
+ import { MouseEventHandler, Ref, RefObject, useRef } from 'react'
2
+ import { AriaButtonProps, useButton } from 'react-aria'
3
+
4
+ import { useForkedRef } from './useForkedRef'
5
+
6
+ interface UseButtonSemanticsOptions<T extends HTMLElement> {
7
+ disabled?: boolean
8
+ onClick?: MouseEventHandler<T>
9
+ }
10
+
11
+ // Synthesizes native button semantics (role, tabIndex, Enter/Space activation) for a
12
+ // non-native `component` — a `<button>`/`<a>`/`<input>` gets all of this for free from the
13
+ // browser, but an arbitrary `component` (e.g. `'div'`, `'span'`) doesn't. Shared by
14
+ // `Link`/`Button`/`CloseButton`, which otherwise duplicated this ref/hook wiring near-verbatim.
15
+ // `elementType: 'div'` and the `HTMLDivElement` cast below are fixed regardless of the actual
16
+ // rendered tag — `useButton` only uses `elementType` to pick which ARIA attributes apply
17
+ // (a `div`-shaped set is the right one for an arbitrary non-native element either way), not to
18
+ // constrain the ref's real type.
19
+ export const useButtonSemantics = <T extends HTMLElement>(
20
+ ref: Ref<T>,
21
+ { disabled, onClick }: UseButtonSemanticsOptions<T>
22
+ ) => {
23
+ const buttonRef = useRef<T | null>(null)
24
+ const forkedRef = useForkedRef(ref, buttonRef)
25
+ const buttonAriaProps: AriaButtonProps<'div'> = {
26
+ elementType: 'div',
27
+ isDisabled: disabled,
28
+ onClick: onClick as unknown as AriaButtonProps<'div'>['onClick']
29
+ }
30
+ const { buttonProps } = useButton(buttonAriaProps, buttonRef as RefObject<HTMLDivElement | null>)
31
+
32
+ return { buttonProps, forkedRef }
33
+ }
@@ -0,0 +1,24 @@
1
+ import { useState } from 'react'
2
+
3
+ /**
4
+ * Shared controlled/uncontrolled state pattern: `value` present means controlled (the returned
5
+ * state always mirrors it), otherwise state is tracked internally starting from `defaultValue`.
6
+ * The returned setter updates internal state (when uncontrolled) and always calls `onChange`,
7
+ * leaving the caller free to layer additional logic (e.g. an `onComplete` callback) around it.
8
+ */
9
+ export function useControllableState<T>(
10
+ value: T | undefined,
11
+ defaultValue: T,
12
+ onChange?: (value: T) => void
13
+ ): [T, (next: T) => void] {
14
+ const isControlled = value !== undefined
15
+ const [uncontrolledValue, setUncontrolledValue] = useState<T>(defaultValue)
16
+ const state = isControlled ? (value as T) : uncontrolledValue
17
+
18
+ const setState = (next: T) => {
19
+ if (!isControlled) setUncontrolledValue(next)
20
+ onChange?.(next)
21
+ }
22
+
23
+ return [state, setState]
24
+ }
@@ -0,0 +1,216 @@
1
+ import {
2
+ ForwardedRef,
3
+ MouseEvent as ReactMouseEvent,
4
+ SyntheticEvent,
5
+ useEffect,
6
+ useRef,
7
+ useState
8
+ } from 'react'
9
+ import { usePreventScroll } from 'react-aria'
10
+
11
+ import { useForkedRef } from './useForkedRef'
12
+ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
13
+ import { executeAfterTransition } from '../utils/dialogTransition'
14
+
15
+ export interface UseDialogElementOptions {
16
+ /**
17
+ * Same-named prop from the caller (`Modal`/`Drawer`) — only ever consulted here to
18
+ * distinguish `'static'` from everything else for the backdrop-click/Escape bounce.
19
+ */
20
+ backdrop?: boolean | 'static'
21
+ /**
22
+ * Disables the open/close transition entirely.
23
+ */
24
+ instant?: boolean
25
+ /**
26
+ * Whether this open should use `showModal()` (top-layer, native backdrop, scroll-locked) or
27
+ * `show()` — `Modal` passes its own `modal` prop straight through; `Drawer` derives it from
28
+ * `backdrop`/`scroll` instead, since its vanilla chassis-css counterpart does the same.
29
+ */
30
+ isModal: boolean
31
+ /**
32
+ * Closes on the escape key / native `cancel` event.
33
+ */
34
+ keyboard?: boolean
35
+ /**
36
+ * Fires synchronously right before a closed dialog actually opens (after the "already open"
37
+ * bail-out, before capturing the pre-open trigger element) — `Drawer`'s only hook into the
38
+ * show effect, used to auto-close every other open drawer first.
39
+ */
40
+ onBeforeShow?: (dialog: HTMLDialogElement) => void
41
+ onClose?: () => void
42
+ onClosePrevented?: () => void
43
+ onHidden?: () => void
44
+ onShow?: () => void
45
+ onShown?: () => void
46
+ ref: ForwardedRef<HTMLDialogElement>
47
+ visible?: boolean
48
+ }
49
+
50
+ // Shared open/close machinery for a native `<dialog>`-based component — `Modal` and `Drawer`
51
+ // otherwise duplicated this near-verbatim: forked ref, the show/begin-hide effect (showModal()
52
+ // vs. show(), initial focus, scroll lock), the finish-hide effect (waits out the exit
53
+ // transition), the non-modal Escape-key listener (the native `cancel` event only fires for
54
+ // dialogs opened with `showModal()`), and the static-backdrop "bounce" used by both the
55
+ // backdrop-click handler and the Escape/`cancel` handler. What's left in each caller is only
56
+ // what's genuinely component-specific: `Modal`/`Drawer`'s own `_className` building, and
57
+ // `Drawer`'s cross-instance "auto-close every other open drawer" registry (wired in via
58
+ // `onBeforeShow`, since it needs to run at one precise point inside the show effect).
59
+ export const useDialogElement = ({
60
+ backdrop,
61
+ instant,
62
+ isModal,
63
+ keyboard = true,
64
+ onBeforeShow,
65
+ onClose,
66
+ onClosePrevented,
67
+ onHidden,
68
+ onShow,
69
+ onShown,
70
+ ref,
71
+ visible
72
+ }: UseDialogElementOptions) => {
73
+ const dialogRef = useRef<HTMLDialogElement>(null)
74
+ const forkedRef = useForkedRef(ref, dialogRef)
75
+
76
+ const [_visible, setVisible] = useState(visible)
77
+ const [hiding, setHiding] = useState(false)
78
+ const [staticBounce, setStaticBounce] = useState(false)
79
+ const [scrollLocked, setScrollLocked] = useState(false)
80
+ const openedAsModalRef = useRef(false)
81
+ const triggerRef = useRef<HTMLElement | null>(null)
82
+
83
+ // Locked from showModal() until the exit transition finishes (see the show/hide effect
84
+ // below) — usePreventScroll releases it automatically on unmount too, even mid-transition.
85
+ usePreventScroll({ isDisabled: !scrollLocked })
86
+
87
+ useEffect(() => {
88
+ setVisible(visible)
89
+ }, [visible])
90
+
91
+ const close = () => {
92
+ onClose?.()
93
+ }
94
+
95
+ const triggerStaticBounce = () => {
96
+ onClosePrevented?.()
97
+ const dialog = dialogRef.current
98
+ if (!dialog) return
99
+ setStaticBounce(true)
100
+ executeAfterTransition(dialog, () => setStaticBounce(false), !instant)
101
+ }
102
+
103
+ // Read through refs, kept current every render, so the keydown listener below — which only
104
+ // resubscribes when `keyboard` changes — never invokes a stale `close`/`triggerStaticBounce`
105
+ // closure if `onClose`/`onClosePrevented`/`instant` change identity without `keyboard` also
106
+ // changing. Same pattern as `Drawer`'s own `closeRef` and `useFloatingOverlay`'s `closeRef`.
107
+ const closeRef = useRef(close)
108
+ closeRef.current = close
109
+ const triggerStaticBounceRef = useRef(triggerStaticBounce)
110
+ triggerStaticBounceRef.current = triggerStaticBounce
111
+
112
+ // Show / begin-hide
113
+ useIsomorphicLayoutEffect(() => {
114
+ const dialog = dialogRef.current
115
+ if (!dialog) return undefined
116
+
117
+ if (_visible) {
118
+ if (dialog.open) return undefined
119
+
120
+ onBeforeShow?.(dialog)
121
+
122
+ triggerRef.current =
123
+ document.activeElement instanceof HTMLElement ? document.activeElement : null
124
+
125
+ openedAsModalRef.current = isModal
126
+ if (isModal) {
127
+ dialog.showModal()
128
+ setScrollLocked(true)
129
+ } else {
130
+ dialog.show()
131
+ }
132
+
133
+ const autofocusEl = dialog.querySelector<HTMLElement>('[autofocus]')
134
+ if (autofocusEl) {
135
+ autofocusEl.focus()
136
+ } else {
137
+ dialog.setAttribute('tabindex', '-1')
138
+ dialog.focus()
139
+ }
140
+
141
+ onShow?.()
142
+ setHiding(false)
143
+
144
+ return executeAfterTransition(dialog, () => onShown?.(), !instant)
145
+ }
146
+
147
+ if (!dialog.open) return undefined
148
+ setHiding(true)
149
+ return undefined
150
+ }, [_visible])
151
+
152
+ // Finish hide once the exit transition (or lack thereof) completes
153
+ useEffect(() => {
154
+ const dialog = dialogRef.current
155
+ if (!hiding || !dialog) return undefined
156
+
157
+ return executeAfterTransition(
158
+ dialog,
159
+ () => {
160
+ if (dialog.open) {
161
+ dialog.close()
162
+ }
163
+ if (openedAsModalRef.current) {
164
+ setScrollLocked(false)
165
+ }
166
+ setHiding(false)
167
+ onHidden?.()
168
+ const trigger = triggerRef.current
169
+ if (trigger && document.contains(trigger)) {
170
+ trigger.focus()
171
+ }
172
+ },
173
+ !instant
174
+ )
175
+ }, [hiding])
176
+
177
+ // Escape key for non-modal (show()) dialogs — the native `cancel` event below only
178
+ // fires for dialogs opened with showModal().
179
+ useEffect(() => {
180
+ const dialog = dialogRef.current
181
+ if (!dialog) return undefined
182
+
183
+ const handleKeyDown = (event: KeyboardEvent) => {
184
+ if (event.key !== 'Escape' || openedAsModalRef.current) return
185
+ event.preventDefault()
186
+ if (!keyboard) {
187
+ triggerStaticBounceRef.current()
188
+ return
189
+ }
190
+ closeRef.current()
191
+ }
192
+
193
+ dialog.addEventListener('keydown', handleKeyDown)
194
+ return () => dialog.removeEventListener('keydown', handleKeyDown)
195
+ }, [keyboard])
196
+
197
+ const handleCancel = (event: SyntheticEvent<HTMLDialogElement>) => {
198
+ event.preventDefault()
199
+ if (!keyboard) {
200
+ triggerStaticBounce()
201
+ return
202
+ }
203
+ close()
204
+ }
205
+
206
+ const handleBackdropClick = (event: ReactMouseEvent<HTMLDialogElement>) => {
207
+ if (event.target !== dialogRef.current || !openedAsModalRef.current) return
208
+ if (backdrop === 'static') {
209
+ triggerStaticBounce()
210
+ return
211
+ }
212
+ close()
213
+ }
214
+
215
+ return { close, dialogRef, forkedRef, handleBackdropClick, handleCancel, hiding, staticBounce }
216
+ }