@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,47 @@
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 NotificationTextOwnProps<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
+
21
+ export type NotificationTextProps<C extends ElementType = 'div'> = PolymorphicComponentProps<
22
+ C,
23
+ NotificationTextOwnProps<C>
24
+ >
25
+
26
+ type NotificationTextComponent = (<C extends ElementType = 'div'>(
27
+ props: NotificationTextProps<C> & { ref?: PolymorphicRef<C> }
28
+ ) => ReactElement | null) & { displayName?: string }
29
+
30
+ function NotificationTextRender<C extends ElementType = 'div'>(
31
+ { children, component, className, ...rest }: NotificationTextProps<C>,
32
+ ref: PolymorphicRef<C>
33
+ ) {
34
+ const Component = component ?? 'div'
35
+ const _className = classNames('notification-text', className)
36
+
37
+ return (
38
+ <Component className={_className} {...rest} ref={ref}>
39
+ {children}
40
+ </Component>
41
+ )
42
+ }
43
+
44
+ export const NotificationText = createPolymorphicComponent<NotificationTextComponent>(
45
+ NotificationTextRender as ForwardRefRenderFunction<Element, NotificationTextProps<ElementType>>,
46
+ 'NotificationText'
47
+ )
@@ -0,0 +1,47 @@
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 NotificationTitleOwnProps<C extends ElementType> = {
11
+ /**
12
+ * A string of all className you want applied to the base 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
+
21
+ export type NotificationTitleProps<C extends ElementType = 'h4'> = PolymorphicComponentProps<
22
+ C,
23
+ NotificationTitleOwnProps<C>
24
+ >
25
+
26
+ type NotificationTitleComponent = (<C extends ElementType = 'h4'>(
27
+ props: NotificationTitleProps<C> & { ref?: PolymorphicRef<C> }
28
+ ) => ReactElement | null) & { displayName?: string }
29
+
30
+ function NotificationTitleRender<C extends ElementType = 'h4'>(
31
+ { children, className, component, ...rest }: NotificationTitleProps<C>,
32
+ ref: PolymorphicRef<C>
33
+ ) {
34
+ const Component = component ?? 'h4'
35
+ const _className = classNames('notification-title', className)
36
+
37
+ return (
38
+ <Component className={_className} {...rest} ref={ref}>
39
+ {children}
40
+ </Component>
41
+ )
42
+ }
43
+
44
+ export const NotificationTitle = createPolymorphicComponent<NotificationTitleComponent>(
45
+ NotificationTitleRender as ForwardRefRenderFunction<Element, NotificationTitleProps<ElementType>>,
46
+ 'NotificationTitle'
47
+ )
@@ -0,0 +1,14 @@
1
+ import { createContext } from 'react'
2
+
3
+ export interface NotificationContextProps {
4
+ /**
5
+ * Whether the notification is currently visible.
6
+ */
7
+ visible?: boolean
8
+ /**
9
+ * Dismisses the notification. Wire this to any element's `onClick` — see `useNotification`.
10
+ */
11
+ close: () => void
12
+ }
13
+
14
+ export const NotificationContext = createContext<NotificationContextProps>({ close: () => {} })
@@ -0,0 +1,17 @@
1
+ export { Notification } from './Notification'
2
+ export type { NotificationProps } from './Notification'
3
+ export { NotificationTitle } from './NotificationTitle'
4
+ export type { NotificationTitleProps } from './NotificationTitle'
5
+ export { NotificationIcon } from './NotificationIcon'
6
+ export type { NotificationIconProps } from './NotificationIcon'
7
+ export { NotificationText } from './NotificationText'
8
+ export type { NotificationTextProps } from './NotificationText'
9
+
10
+ // NotificationStack is an independent manager/container component (react-hot-toast-style), not
11
+ // a Notification sub-part — it subscribes to the shared notificationQueue and renders
12
+ // Notification instances from it, rather than being composed as a child of Notification. Stays
13
+ // a separate top-level export, mirroring Toast/Toaster.
14
+ export { NotificationStack } from './NotificationStack'
15
+ export type { NotificationStackProps } from './NotificationStack'
16
+ export { addNotification, closeNotification, notificationQueue } from './notificationQueue'
17
+ export type { NotificationContent } from './notificationQueue'
@@ -0,0 +1,48 @@
1
+ import { ReactNode } from 'react'
2
+ import { ToastQueue } from 'react-stately'
3
+
4
+ import { NotificationProps } from './Notification'
5
+
6
+ export interface NotificationContent extends Pick<
7
+ NotificationProps,
8
+ | 'actions'
9
+ | 'autohide'
10
+ | 'closeLabel'
11
+ | 'color'
12
+ | 'delay'
13
+ | 'dismissible'
14
+ | 'icon'
15
+ | 'role'
16
+ | 'solid'
17
+ | 'text'
18
+ | 'title'
19
+ | 'titleComponent'
20
+ > {
21
+ /**
22
+ * Content of the notification. Compose manually (typically a `NotificationIcon`/
23
+ * `NotificationTitle`/`NotificationText`), or leave empty and use the `icon`/`title`/`text`
24
+ * shorthand options instead.
25
+ */
26
+ children?: ReactNode
27
+ }
28
+
29
+ // The queue backing `NotificationStack` — a module-level singleton so `addNotification()` is
30
+ // callable from anywhere (an event handler, an async callback) without threading a `push` prop
31
+ // through render. `NotificationStack` subscribes to it via `useToastQueue`; nothing renders
32
+ // until a `<NotificationStack />` is actually mounted somewhere to display the queue's contents.
33
+ export const notificationQueue = new ToastQueue<NotificationContent>()
34
+
35
+ // Adds a notification to the queue. Returns the notification's key, which can be passed to
36
+ // `closeNotification` to dismiss it programmatically. `children` is optional — omit it for a
37
+ // notification composed entirely from the `icon`/`title`/`text` shorthand options.
38
+ export function addNotification(
39
+ children?: ReactNode,
40
+ options: Omit<NotificationContent, 'children'> = {}
41
+ ): string {
42
+ return notificationQueue.add({ children, ...options })
43
+ }
44
+
45
+ // Removes a queued notification immediately, bypassing its own exit animation.
46
+ export function closeNotification(key: string): void {
47
+ notificationQueue.close(key)
48
+ }
@@ -0,0 +1,68 @@
1
+ import React, { ClipboardEvent, ForwardedRef, forwardRef, KeyboardEvent } from 'react'
2
+ import classNames from 'classnames'
3
+ import { mergeProps, useFocusRing, useObjectRef, useTextField } from 'react-aria'
4
+
5
+ import { validationClassName } from '../../utils/validationClassName'
6
+
7
+ interface OtpBoxProps {
8
+ disabled?: boolean
9
+ index: number
10
+ invalid?: boolean
11
+ mask?: boolean
12
+ onChangeValue: (value: string) => void
13
+ onKeyDownBox: (event: KeyboardEvent<HTMLInputElement>) => void
14
+ onPasteBox: (event: ClipboardEvent<HTMLInputElement>) => void
15
+ size?: 'small' | 'large'
16
+ valid?: boolean
17
+ value: string
18
+ }
19
+
20
+ export const OtpBox = forwardRef(
21
+ (
22
+ {
23
+ disabled,
24
+ index,
25
+ invalid,
26
+ mask,
27
+ onChangeValue,
28
+ onKeyDownBox,
29
+ onPasteBox,
30
+ size,
31
+ valid,
32
+ value
33
+ }: OtpBoxProps,
34
+ forwardedRef: ForwardedRef<HTMLInputElement>
35
+ ) => {
36
+ const ref = useObjectRef(forwardedRef)
37
+ const { focusProps, isFocusVisible } = useFocusRing()
38
+ const { inputProps } = useTextField(
39
+ {
40
+ 'aria-label': `Digit ${index + 1}`,
41
+ autoComplete: index === 0 ? 'one-time-code' : 'off',
42
+ isDisabled: disabled,
43
+ isInvalid: invalid,
44
+ maxLength: 1,
45
+ onChange: onChangeValue,
46
+ onFocus: (event) => (event.target as HTMLInputElement).select(),
47
+ onKeyDown: onKeyDownBox,
48
+ type: mask ? 'password' : 'text',
49
+ value
50
+ },
51
+ ref
52
+ )
53
+
54
+ return (
55
+ <input
56
+ {...mergeProps(inputProps, focusProps)}
57
+ className={classNames('form-input', size, validationClassName(invalid, valid))}
58
+ data-focus-visible={isFocusVisible || undefined}
59
+ inputMode="numeric"
60
+ onPaste={onPasteBox}
61
+ pattern="\d*"
62
+ ref={ref}
63
+ />
64
+ )
65
+ }
66
+ )
67
+
68
+ OtpBox.displayName = 'OtpBox'
@@ -0,0 +1,341 @@
1
+ import React, {
2
+ ClipboardEvent,
3
+ forwardRef,
4
+ Fragment,
5
+ HTMLAttributes,
6
+ KeyboardEvent,
7
+ ReactNode,
8
+ useMemo,
9
+ useRef
10
+ } from 'react'
11
+ import classNames from 'classnames'
12
+
13
+ import { useControllableState, useForkedRef, useFormField } from '../../hooks'
14
+ import { validationClassName } from '../../utils/validationClassName'
15
+ import { renderFormField } from '../form-field/renderFormField'
16
+ import { OtpBox } from './OtpBox'
17
+
18
+ export interface OtpInputProps extends Omit<
19
+ HTMLAttributes<HTMLDivElement>,
20
+ 'onChange' | 'defaultValue'
21
+ > {
22
+ /**
23
+ * Identifies the element that describes the group, e.g. a `FormHelp` help element.
24
+ */
25
+ 'aria-describedby'?: string
26
+ /**
27
+ * An accessible label for the group, used when there's no visible `<label>`.
28
+ */
29
+ 'aria-label'?: string
30
+ /**
31
+ * Identifies a visible `<label>` element for the group.
32
+ */
33
+ 'aria-labelledby'?: string
34
+ /**
35
+ * A string of all className you want applied to the base component.
36
+ */
37
+ className?: string
38
+ /**
39
+ * The initial code (uncontrolled), e.g. `"123"` for a partially filled code.
40
+ */
41
+ defaultValue?: string
42
+ /**
43
+ * Prevents input and makes every box non-interactive.
44
+ */
45
+ disabled?: boolean
46
+ /**
47
+ * Splits the boxes into groups (e.g. `[3, 3]` for a "123-456" layout), rendering a
48
+ * `.form-otp-separator` between each group. The total number of boxes becomes the sum of
49
+ * `groupSizes`, overriding `length`.
50
+ */
51
+ groupSizes?: number[]
52
+ /**
53
+ * A description for the field, rendered below the boxes.
54
+ */
55
+ help?: ReactNode
56
+ /**
57
+ * `id` forwarded to the group container.
58
+ */
59
+ id?: string
60
+ /**
61
+ * Visually connects the boxes into a single bordered control. When `groupSizes` is set, each
62
+ * group is connected separately.
63
+ */
64
+ inputGroup?: boolean
65
+ /**
66
+ * Set component validation state to invalid.
67
+ */
68
+ invalid?: boolean
69
+ /**
70
+ * An error message for the field, rendered below the boxes when `invalid` is set.
71
+ */
72
+ invalidFeedback?: ReactNode
73
+ /**
74
+ * The field's caption, rendered as a `FormLabel` associated with the group via
75
+ * `aria-labelledby` (there's no single input to target with `htmlFor`).
76
+ */
77
+ label?: ReactNode
78
+ /**
79
+ * Number of digit boxes. Ignored when `groupSizes` is set. Defaults to `6`.
80
+ */
81
+ length?: number
82
+ /**
83
+ * Mask entered digits using `type="password"` boxes.
84
+ */
85
+ mask?: boolean
86
+ /**
87
+ * `name` of an auto-created hidden input kept in sync with the code, for native form
88
+ * submission. Omit to skip creating it.
89
+ */
90
+ name?: string
91
+ /**
92
+ * Callback fired whenever the code changes.
93
+ */
94
+ onChange?: (value: string) => void
95
+ /**
96
+ * Callback fired once every box is filled.
97
+ */
98
+ onComplete?: (value: string) => void
99
+ /**
100
+ * Content of the separator rendered between groups when `groupSizes` is set. Defaults to `–`.
101
+ */
102
+ separator?: ReactNode
103
+ /**
104
+ * Size the boxes small or large.
105
+ */
106
+ size?: 'small' | 'large'
107
+ /**
108
+ * Set component validation state to valid.
109
+ */
110
+ valid?: boolean
111
+ /**
112
+ * A success message for the field, rendered below the boxes when `valid` is set.
113
+ */
114
+ validFeedback?: ReactNode
115
+ /**
116
+ * The code (controlled), digits only, e.g. `"123456"`.
117
+ */
118
+ value?: string
119
+ }
120
+
121
+ const onlyDigits = (raw: string) => raw.replace(/\D/g, '')
122
+
123
+ const toBoxes = (raw: string, total: number): string[] => {
124
+ const digits = [...onlyDigits(raw)].slice(0, total)
125
+ return Array.from({ length: total }, (_, i) => digits[i] ?? '')
126
+ }
127
+
128
+ export const OtpInput = forwardRef<HTMLDivElement, OtpInputProps>(
129
+ (
130
+ {
131
+ className,
132
+ defaultValue,
133
+ disabled,
134
+ groupSizes,
135
+ help,
136
+ id,
137
+ inputGroup,
138
+ invalid,
139
+ invalidFeedback,
140
+ label,
141
+ length = 6,
142
+ mask,
143
+ name,
144
+ onChange,
145
+ onComplete,
146
+ separator = '–',
147
+ size,
148
+ valid,
149
+ validFeedback,
150
+ value,
151
+ ...rest
152
+ }: OtpInputProps,
153
+ ref
154
+ ): ReactNode => {
155
+ const total =
156
+ groupSizes && groupSizes.length > 0 ? groupSizes.reduce((a, b) => a + b, 0) : length
157
+
158
+ const [rawValue, setRawValue] = useControllableState(value, defaultValue ?? '', onChange)
159
+ const boxes = useMemo(() => toBoxes(rawValue, total), [rawValue, total])
160
+
161
+ const boxRefs = useRef<Array<HTMLInputElement | null>>([])
162
+ const focusBox = (index: number) => boxRefs.current[index]?.focus()
163
+ const containerRef = useRef<HTMLDivElement>(null)
164
+ const forkedRef = useForkedRef(ref, containerRef)
165
+
166
+ const commit = (nextBoxes: string[]) => {
167
+ const next = nextBoxes.join('')
168
+ setRawValue(next)
169
+ if (nextBoxes.length === total && nextBoxes.every((box) => box !== '')) {
170
+ onComplete?.(next)
171
+ }
172
+ }
173
+
174
+ const handleChange = (index: number, raw: string) => {
175
+ const digits = onlyDigits(raw)
176
+
177
+ if (digits.length > 1) {
178
+ // Multi-character value landing in a single box (autofill, or a fast typist outrunning
179
+ // the auto-advance) — distribute across this and subsequent boxes, matching chassis-css's
180
+ // vanilla otp-input.js `_handleInput`.
181
+ const chars = [...digits]
182
+ const nextBoxes = [...boxes]
183
+ for (let i = 0; i < chars.length && index + i < total; i++) {
184
+ nextBoxes[index + i] = chars[i]!
185
+ }
186
+ commit(nextBoxes)
187
+ focusBox(Math.min(index + chars.length, total - 1))
188
+ return
189
+ }
190
+
191
+ const nextBoxes = [...boxes]
192
+ nextBoxes[index] = digits
193
+ commit(nextBoxes)
194
+ if (digits && index < total - 1) focusBox(index + 1)
195
+ }
196
+
197
+ const handleKeyDown = (index: number, event: KeyboardEvent<HTMLInputElement>) => {
198
+ if (disabled) return
199
+
200
+ switch (event.key) {
201
+ case 'Backspace': {
202
+ if (!boxes[index] && index > 0) {
203
+ event.preventDefault()
204
+ const nextBoxes = [...boxes]
205
+ nextBoxes[index - 1] = ''
206
+ commit(nextBoxes)
207
+ focusBox(index - 1)
208
+ }
209
+ break
210
+ }
211
+ case 'Delete': {
212
+ event.preventDefault()
213
+ // `boxes` (and every copy derived from it) is always exactly `total` entries long — see
214
+ // `toBoxes` — so `i + 1` (bounded by `i < total - 1`) never reads past the end.
215
+ const nextBoxes = [...boxes]
216
+ for (let i = index; i < total - 1; i++) nextBoxes[i] = nextBoxes[i + 1]!
217
+ nextBoxes[total - 1] = ''
218
+ commit(nextBoxes)
219
+ break
220
+ }
221
+ case 'ArrowLeft': {
222
+ if (index > 0) {
223
+ event.preventDefault()
224
+ focusBox(index - 1)
225
+ }
226
+ break
227
+ }
228
+ case 'ArrowRight': {
229
+ if (index < total - 1) {
230
+ event.preventDefault()
231
+ focusBox(index + 1)
232
+ }
233
+ break
234
+ }
235
+ default:
236
+ break
237
+ }
238
+ }
239
+
240
+ const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {
241
+ event.preventDefault()
242
+ if (disabled) return
243
+ const digits = onlyDigits(event.clipboardData.getData('text')).slice(0, total)
244
+ if (!digits) return
245
+ commit(toBoxes(digits, total))
246
+ focusBox(Math.min(digits.length, total) - 1)
247
+ }
248
+
249
+ const renderBoxes = (start: number, count: number) =>
250
+ Array.from({ length: count }, (_, i) => start + i).map((i) => (
251
+ <OtpBox
252
+ disabled={disabled}
253
+ index={i}
254
+ invalid={invalid}
255
+ key={i}
256
+ mask={mask}
257
+ onChangeValue={(next) => handleChange(i, next)}
258
+ onKeyDownBox={(event) => handleKeyDown(i, event)}
259
+ onPasteBox={handlePaste}
260
+ ref={(el) => {
261
+ boxRefs.current[i] = el
262
+ }}
263
+ size={size}
264
+ valid={valid}
265
+ value={boxes[i] ?? ''}
266
+ />
267
+ ))
268
+
269
+ let content: ReactNode
270
+ if (groupSizes && groupSizes.length > 0) {
271
+ let cursor = 0
272
+ content = groupSizes.map((groupSize, groupIndex) => {
273
+ const start = cursor
274
+ cursor += groupSize
275
+ return (
276
+ // eslint-disable-next-line react/no-array-index-key
277
+ <Fragment key={groupIndex}>
278
+ {groupIndex > 0 && <span className="form-otp-separator">{separator}</span>}
279
+ {inputGroup ? (
280
+ <div className="input-group">{renderBoxes(start, groupSize)}</div>
281
+ ) : (
282
+ renderBoxes(start, groupSize)
283
+ )}
284
+ </Fragment>
285
+ )
286
+ })
287
+ } else {
288
+ content = renderBoxes(0, total)
289
+ }
290
+
291
+ const {
292
+ describedBy,
293
+ feedbackId,
294
+ helpId,
295
+ inputId: groupId,
296
+ labelId,
297
+ labelledBy
298
+ } = useFormField({
299
+ ariaDescribedBy: rest['aria-describedby'],
300
+ ariaLabelledBy: rest['aria-labelledby'],
301
+ help,
302
+ id,
303
+ invalid,
304
+ invalidFeedback,
305
+ label,
306
+ valid,
307
+ validFeedback
308
+ })
309
+
310
+ return renderFormField({
311
+ children: (
312
+ <div
313
+ {...rest}
314
+ aria-describedby={describedBy}
315
+ aria-labelledby={labelledBy}
316
+ className={classNames(
317
+ 'form-otp',
318
+ { 'input-group': inputGroup && !(groupSizes && groupSizes.length > 0) },
319
+ validationClassName(invalid, valid),
320
+ className
321
+ )}
322
+ id={groupId}
323
+ ref={forkedRef}
324
+ role="group"
325
+ >
326
+ {content}
327
+ {name && <input disabled={disabled} name={name} type="hidden" value={boxes.join('')} />}
328
+ </div>
329
+ ),
330
+ help,
331
+ ids: { feedback: feedbackId, help: helpId, label: labelId },
332
+ invalid,
333
+ invalidFeedback,
334
+ label,
335
+ valid,
336
+ validFeedback
337
+ })
338
+ }
339
+ )
340
+
341
+ OtpInput.displayName = 'OtpInput'
@@ -0,0 +1,2 @@
1
+ export { OtpInput } from './OtpInput'
2
+ export type { OtpInputProps } from './OtpInput'