@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,128 @@
1
+ import React, { forwardRef, HTMLAttributes, useRef, useState } from 'react'
2
+ import classNames from 'classnames'
3
+ import { CSSTransition } from 'react-transition-group'
4
+
5
+ import { useForkedRef } from '../../hooks'
6
+
7
+ export interface CollapseProps extends HTMLAttributes<HTMLDivElement> {
8
+ /**
9
+ * A string of all className you want applied to the base component.
10
+ */
11
+ className?: string
12
+ /**
13
+ * Set horizontal collapsing to transition the width instead of height.
14
+ */
15
+ horizontal?: boolean
16
+ /**
17
+ * Callback fired when the component requests to be hidden.
18
+ */
19
+ onHide?: () => void
20
+ /**
21
+ * Callback fired when the component requests to be shown.
22
+ */
23
+ onShow?: () => void
24
+ /**
25
+ * Toggle the visibility of component.
26
+ */
27
+ visible?: boolean
28
+ }
29
+
30
+ export const Collapse = forwardRef<HTMLDivElement, CollapseProps>(
31
+ ({ children, className, horizontal, onHide, onShow, visible, ...rest }, ref) => {
32
+ const [height, setHeight] = useState<number>()
33
+ const [width, setWidth] = useState<number>()
34
+ const collapseRef = useRef<HTMLDivElement>(null)
35
+ const forkedRef = useForkedRef(ref, collapseRef)
36
+
37
+ const getTransitionClass = (state: string) => {
38
+ return state === 'entering'
39
+ ? 'collapsing'
40
+ : state === 'entered'
41
+ ? 'collapse show'
42
+ : state === 'exiting'
43
+ ? 'collapsing'
44
+ : 'collapse'
45
+ }
46
+
47
+ const onEntering = () => {
48
+ onShow && onShow()
49
+
50
+ if (horizontal) {
51
+ collapseRef.current && setWidth(collapseRef.current.scrollWidth)
52
+ return
53
+ }
54
+ collapseRef.current && setHeight(collapseRef.current.scrollHeight)
55
+ }
56
+
57
+ const onEntered = () => {
58
+ if (horizontal) {
59
+ setWidth(0)
60
+ return
61
+ }
62
+ setHeight(0)
63
+ }
64
+
65
+ const onExit = () => {
66
+ if (horizontal) {
67
+ collapseRef.current && setWidth(collapseRef.current.scrollWidth)
68
+ return
69
+ }
70
+ collapseRef.current && setHeight(collapseRef.current.scrollHeight)
71
+ }
72
+
73
+ const onExiting = () => {
74
+ onHide && onHide()
75
+ if (horizontal) {
76
+ setWidth(0)
77
+ return
78
+ }
79
+ setHeight(0)
80
+ }
81
+
82
+ const onExited = () => {
83
+ if (horizontal) {
84
+ setWidth(0)
85
+ return
86
+ }
87
+ setHeight(0)
88
+ }
89
+
90
+ const _className = classNames(
91
+ {
92
+ 'collapse-horizontal': horizontal
93
+ },
94
+ className
95
+ )
96
+
97
+ return (
98
+ <CSSTransition
99
+ in={visible}
100
+ nodeRef={collapseRef}
101
+ onEntering={onEntering}
102
+ onEntered={onEntered}
103
+ onExit={onExit}
104
+ onExiting={onExiting}
105
+ onExited={onExited}
106
+ timeout={350}
107
+ >
108
+ {(state) => {
109
+ const transitionClass = getTransitionClass(state)
110
+ const currentHeight = height === 0 ? null : { height }
111
+ const currentWidth = width === 0 ? null : { width }
112
+ return (
113
+ <div
114
+ className={classNames(_className, transitionClass)}
115
+ style={{ ...currentHeight, ...currentWidth }}
116
+ {...rest}
117
+ ref={forkedRef}
118
+ >
119
+ {children}
120
+ </div>
121
+ )
122
+ }}
123
+ </CSSTransition>
124
+ )
125
+ }
126
+ )
127
+
128
+ Collapse.displayName = 'Collapse'
@@ -0,0 +1,2 @@
1
+ export { Collapse } from './Collapse'
2
+ export type { CollapseProps } from './Collapse'
@@ -0,0 +1,106 @@
1
+ import React, { ChangeEventHandler, forwardRef, InputHTMLAttributes, ReactNode } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import { useFormField } from '../../hooks'
5
+ import { validationClassName } from '../../utils/validationClassName'
6
+ import { renderFormField } from '../form-field/renderFormField'
7
+
8
+ export interface ColorInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
9
+ /**
10
+ * A string of all className you want applied to the component.
11
+ */
12
+ className?: string
13
+ /**
14
+ * The value of the input, uncontrolled.
15
+ */
16
+ defaultValue?: string
17
+ /**
18
+ * Toggle the disabled state for the component.
19
+ */
20
+ disabled?: boolean
21
+ /**
22
+ * A description for the field, rendered below the input.
23
+ */
24
+ help?: ReactNode
25
+ /**
26
+ * Set component validation state to invalid.
27
+ */
28
+ invalid?: boolean
29
+ /**
30
+ * An error message for the field, rendered below the input when `invalid` is set.
31
+ */
32
+ invalidFeedback?: ReactNode
33
+ /**
34
+ * The field's caption, rendered as a `FormLabel` associated with this input.
35
+ */
36
+ label?: ReactNode
37
+ /**
38
+ * Method called immediately after the `value` prop changes.
39
+ */
40
+ onChange?: ChangeEventHandler<HTMLInputElement>
41
+ /**
42
+ * Size the component small or large.
43
+ */
44
+ size?: 'small' | 'large'
45
+ /**
46
+ * Set component validation state to valid.
47
+ */
48
+ valid?: boolean
49
+ /**
50
+ * A success message for the field, rendered below the input when `valid` is set.
51
+ */
52
+ validFeedback?: ReactNode
53
+ /**
54
+ * The `value` attribute of component.
55
+ *
56
+ * @controllable onChange
57
+ * */
58
+ value?: string
59
+ }
60
+
61
+ export const ColorInput = forwardRef<HTMLInputElement, ColorInputProps>(
62
+ (
63
+ { className, help, id, invalid, invalidFeedback, label, size, valid, validFeedback, ...rest },
64
+ ref
65
+ ) => {
66
+ const { describedBy, feedbackId, helpId, inputId } = useFormField({
67
+ ariaDescribedBy: rest['aria-describedby'],
68
+ help,
69
+ id,
70
+ invalid,
71
+ invalidFeedback,
72
+ valid,
73
+ validFeedback
74
+ })
75
+
76
+ const _className = classNames(
77
+ 'form-input',
78
+ size,
79
+ validationClassName(invalid, valid),
80
+ className
81
+ )
82
+
83
+ return renderFormField({
84
+ children: (
85
+ <input
86
+ {...rest}
87
+ aria-describedby={describedBy}
88
+ aria-invalid={invalid || undefined}
89
+ className={_className}
90
+ id={inputId}
91
+ ref={ref}
92
+ type="color"
93
+ />
94
+ ),
95
+ help,
96
+ ids: { feedback: feedbackId, help: helpId, input: inputId },
97
+ invalid,
98
+ invalidFeedback,
99
+ label,
100
+ valid,
101
+ validFeedback
102
+ })
103
+ }
104
+ )
105
+
106
+ ColorInput.displayName = 'ColorInput'
@@ -0,0 +1,2 @@
1
+ export { ColorInput } from './ColorInput'
2
+ export type { ColorInputProps } from './ColorInput'
@@ -0,0 +1,320 @@
1
+ import React, {
2
+ forwardRef,
3
+ HTMLAttributes,
4
+ InputHTMLAttributes,
5
+ ReactNode,
6
+ useId,
7
+ useMemo,
8
+ useRef
9
+ } from 'react'
10
+ import { createPortal } from 'react-dom'
11
+ import classNames from 'classnames'
12
+ import { useComboBox, useFilter, useOverlayPosition } from 'react-aria'
13
+ import { Key, useComboBoxState } from 'react-stately'
14
+
15
+ import { useFloatingOverlay, useForkedRef, useFormField } from '../../hooks'
16
+ import {
17
+ buildEntriesFromChildren,
18
+ buildEntriesFromItemsDef,
19
+ ComboboxEntry,
20
+ comboboxCollectionChildren,
21
+ getDisabledKeys
22
+ } from '../../utils/comboboxCollection'
23
+ import {
24
+ COMBOBOX_MENU_OVERLAY_STYLE,
25
+ resolveMenuOverlayPositioning,
26
+ toAriaPlacement
27
+ } from '../../utils/overlayPlacement'
28
+ import { validationClassName } from '../../utils/validationClassName'
29
+ import { renderFormField } from '../form-field/renderFormField'
30
+ import { MenuItemsDef } from '../menu/MenuItemDef'
31
+ import { ComboboxGroup } from './ComboboxGroup'
32
+ import { ComboboxItem } from './ComboboxItem'
33
+ import { ComboboxListBox } from './ComboboxListBox'
34
+
35
+ export interface ComboboxProps extends Omit<
36
+ HTMLAttributes<HTMLDivElement>,
37
+ 'onChange' | 'defaultValue'
38
+ > {
39
+ /**
40
+ * An accessible label for the combobox, used when there's no visible `<label>`.
41
+ */
42
+ 'aria-label'?: string
43
+ /**
44
+ * Identifies a visible `<label>` element for the combobox.
45
+ */
46
+ 'aria-labelledby'?: string
47
+ /**
48
+ * `ComboboxItem` elements, optionally wrapped in `ComboboxGroup` — read as data by
49
+ * `Combobox` to build the option list. Not rendered directly. Ignored when `items` is set.
50
+ */
51
+ children?: ReactNode
52
+ /**
53
+ * A string of all className you want applied to the base component.
54
+ */
55
+ className?: string
56
+ /**
57
+ * The initial selected option's id (uncontrolled).
58
+ */
59
+ defaultValue?: Key | null
60
+ /**
61
+ * Prevents the combobox from being focused or interacted with.
62
+ */
63
+ disabled?: boolean
64
+ /**
65
+ * A description for the field, rendered below the combobox.
66
+ */
67
+ help?: ReactNode
68
+ /**
69
+ * `id` forwarded to the input element — useful for pairing with a `<label for>`.
70
+ */
71
+ id?: string
72
+ /**
73
+ * Set component validation state to invalid.
74
+ */
75
+ invalid?: boolean
76
+ /**
77
+ * An error message for the field, rendered below the combobox when `invalid` is set.
78
+ */
79
+ invalidFeedback?: ReactNode
80
+ /**
81
+ * Array of item/header/divider definitions for data-driven rendering. When provided, children
82
+ * are ignored. A `'header'` entry starts a group that all following items join until the next
83
+ * header or the end of the array. `'divider'` entries are a no-op here (dividers aren't
84
+ * meaningful for a listbox/option collection) — use `ComboboxGroup` composition instead if
85
+ * you need finer control over grouping. An entry's `href`/`onClick` are `Menu`-only and are not
86
+ * read here — use `onChange` to react to the selection instead.
87
+ */
88
+ items?: MenuItemsDef
89
+ /**
90
+ * The field's caption, rendered as a `FormLabel` associated with the input.
91
+ */
92
+ label?: ReactNode
93
+ /**
94
+ * `name` of an auto-created hidden input, kept in sync with the selection, for native form
95
+ * submission. Omit to skip creating one.
96
+ */
97
+ name?: string
98
+ /**
99
+ * Text shown in the listbox when no options match the current query.
100
+ */
101
+ noResultsText?: ReactNode
102
+ /**
103
+ * Callback fired when the selected option changes.
104
+ */
105
+ onChange?: (value: Key | null) => void
106
+ /**
107
+ * Placeholder shown in the input when nothing is selected.
108
+ */
109
+ placeholder?: string
110
+ /**
111
+ * Size the component small or large.
112
+ */
113
+ size?: 'small' | 'large'
114
+ /**
115
+ * Set component validation state to valid.
116
+ */
117
+ valid?: boolean
118
+ /**
119
+ * A success message for the field, rendered below the combobox when `valid` is set.
120
+ */
121
+ validFeedback?: ReactNode
122
+ /**
123
+ * The selected option's id (controlled).
124
+ */
125
+ value?: Key | null
126
+ }
127
+
128
+ export const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
129
+ (
130
+ {
131
+ 'aria-describedby': ariaDescribedBy,
132
+ 'aria-label': ariaLabel,
133
+ 'aria-labelledby': ariaLabelledBy,
134
+ children,
135
+ className,
136
+ defaultValue,
137
+ disabled,
138
+ help,
139
+ id,
140
+ invalid,
141
+ invalidFeedback,
142
+ items,
143
+ label,
144
+ name,
145
+ noResultsText = 'No results found',
146
+ onChange,
147
+ placeholder,
148
+ size,
149
+ valid,
150
+ validFeedback,
151
+ value,
152
+ ...rest
153
+ }: ComboboxProps,
154
+ ref
155
+ ): ReactNode => {
156
+ // `entries`/`disabledKeys` only need to change when the data driving them does — without this,
157
+ // both re-derive from scratch on every render, including every keystroke while typing.
158
+ const entries = useMemo(
159
+ () =>
160
+ items
161
+ ? buildEntriesFromItemsDef(items)
162
+ : buildEntriesFromChildren(children, { Group: ComboboxGroup, Item: ComboboxItem }),
163
+ [items, children]
164
+ )
165
+ const disabledKeys = useMemo(() => getDisabledKeys(entries), [entries])
166
+
167
+ // Case- and accent-insensitive substring matching, mirroring chassis-css's own
168
+ // always-case-insensitive combobox.js filtering.
169
+ const { contains } = useFilter({ sensitivity: 'base' })
170
+
171
+ const state = useComboBoxState<ComboboxEntry>({
172
+ children: comboboxCollectionChildren,
173
+ defaultItems: entries,
174
+ disabledKeys,
175
+ defaultFilter: contains,
176
+ defaultValue,
177
+ value,
178
+ onChange,
179
+ allowsEmptyCollection: true,
180
+ // Matches chassis-css's vanilla combobox.js: the menu opens on focus, not only once the
181
+ // user starts typing.
182
+ menuTrigger: 'focus'
183
+ })
184
+
185
+ const wrapperRef = useRef<HTMLDivElement>(null)
186
+ const forkedWrapperRef = useForkedRef(ref, wrapperRef)
187
+ const inputRef = useRef<HTMLInputElement>(null)
188
+ const listBoxRef = useRef<HTMLElement>(null)
189
+ const popoverRef = useRef<HTMLDivElement>(null)
190
+ const noResultsId = useId()
191
+
192
+ const { describedBy, feedbackId, helpId, inputId, labelId, labelledBy } = useFormField({
193
+ ariaDescribedBy,
194
+ ariaLabelledBy,
195
+ help,
196
+ id,
197
+ invalid,
198
+ invalidFeedback,
199
+ label,
200
+ valid,
201
+ validFeedback
202
+ })
203
+
204
+ const { inputProps, listBoxProps } = useComboBox<ComboboxEntry>(
205
+ {
206
+ 'aria-label': ariaLabel,
207
+ 'aria-labelledby': labelledBy,
208
+ id: inputId,
209
+ inputRef,
210
+ listBoxRef,
211
+ popoverRef,
212
+ isDisabled: disabled,
213
+ placeholder
214
+ },
215
+ state
216
+ )
217
+
218
+ const { overlayProps, placement: resolvedPlacement } = useOverlayPosition({
219
+ targetRef: wrapperRef,
220
+ overlayRef: popoverRef,
221
+ placement: toAriaPlacement('bottom-start'),
222
+ offset: 2,
223
+ isOpen: state.isOpen,
224
+ // Leaving `onClose` unset doesn't actually disable react-aria's close-on-scroll listener —
225
+ // only an explicit `null` does. Matches the fix already applied to `Autocomplete`'s identical
226
+ // positioning call.
227
+ onClose: null
228
+ })
229
+
230
+ // Portals the panel to `document.body` (or an enclosing open `<dialog>`) instead of rendering
231
+ // it inline, matching `Popover`/`Tooltip` — an inline-rendered panel gets clipped by any
232
+ // ancestor with `overflow: hidden`/`auto` (a `ModalBody`, a scrollable card, a table cell), and
233
+ // `Combobox` is commonly composed inside exactly those. See `COMBOBOX_MENU_OVERLAY_STYLE` for
234
+ // the styling this trades away by leaving the DOM position `.combobox + .menu` relies on.
235
+ const portalContainer = useFloatingOverlay({
236
+ close: state.close,
237
+ isOpen: state.isOpen,
238
+ open: state.open,
239
+ triggerRef: wrapperRef
240
+ })
241
+
242
+ const { menuStyle, placementAttr } = resolveMenuOverlayPositioning(
243
+ overlayProps.style,
244
+ 'bottom-start',
245
+ resolvedPlacement
246
+ )
247
+ const overlayStyle: React.CSSProperties = { ...COMBOBOX_MENU_OVERLAY_STYLE, ...menuStyle }
248
+
249
+ const inputHtmlProps = inputProps as InputHTMLAttributes<HTMLInputElement>
250
+ const showNoResults = state.collection.size === 0
251
+ // Merges the no-results message into the input's own description (for AT that reads
252
+ // descriptions on demand) in addition to the `aria-live` region below (for AT that announces
253
+ // live-region changes proactively as the user types) — belt and braces, since screen reader
254
+ // support for announcing a freshly-mounted live region is inconsistent.
255
+ const inputDescribedBy =
256
+ [describedBy, showNoResults && noResultsId].filter(Boolean).join(' ') || undefined
257
+
258
+ return renderFormField({
259
+ children: (
260
+ <>
261
+ <div
262
+ className={classNames(
263
+ 'form-input',
264
+ 'combobox',
265
+ size,
266
+ { disabled },
267
+ validationClassName(invalid, valid),
268
+ className
269
+ )}
270
+ ref={forkedWrapperRef}
271
+ {...rest}
272
+ >
273
+ <input
274
+ autoComplete="off"
275
+ className="combobox-value"
276
+ {...inputHtmlProps}
277
+ aria-describedby={inputDescribedBy}
278
+ aria-invalid={invalid || undefined}
279
+ ref={inputRef}
280
+ />
281
+ </div>
282
+ {typeof window !== 'undefined' &&
283
+ createPortal(
284
+ <div
285
+ className={classNames('menu', { show: state.isOpen })}
286
+ data-cx-placement={placementAttr}
287
+ style={overlayStyle}
288
+ hidden={!state.isOpen}
289
+ ref={popoverRef}
290
+ >
291
+ <ComboboxListBox
292
+ state={state}
293
+ listBoxProps={listBoxProps}
294
+ listBoxRef={listBoxRef}
295
+ />
296
+ {showNoResults && (
297
+ <div className="combobox-no-results" id={noResultsId} role="status">
298
+ {noResultsText}
299
+ </div>
300
+ )}
301
+ </div>,
302
+ portalContainer ?? document.body
303
+ )}
304
+ {name && (
305
+ <input type="hidden" name={name} value={state.value ?? ''} disabled={disabled} />
306
+ )}
307
+ </>
308
+ ),
309
+ help,
310
+ ids: { feedback: feedbackId, help: helpId, input: inputId, label: labelId },
311
+ invalid,
312
+ invalidFeedback,
313
+ label,
314
+ valid,
315
+ validFeedback
316
+ })
317
+ }
318
+ )
319
+
320
+ Combobox.displayName = 'Combobox'
@@ -0,0 +1,27 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export interface ComboboxGroupProps {
4
+ /**
5
+ * Items belonging to this group.
6
+ */
7
+ children: ReactNode
8
+ /**
9
+ * Header content for the group (`.menu-header`).
10
+ */
11
+ label: ReactNode
12
+ }
13
+
14
+ // `ComboboxGroup` is never actually mounted — like `ComboboxItem`, it's read as data by
15
+ // `Combobox`, which splits its top-level children into groups (wrapping react-stately
16
+ // `Section` nodes) and bare items. Groups and bare items can be mixed at the top level:
17
+ //
18
+ // <Combobox aria-label="Language">
19
+ // <ComboboxGroup label="Frontend">
20
+ // <ComboboxItem id="html">HTML</ComboboxItem>
21
+ // <ComboboxItem id="css">CSS</ComboboxItem>
22
+ // </ComboboxGroup>
23
+ // <ComboboxItem id="python">Python</ComboboxItem>
24
+ // </Combobox>
25
+ export const ComboboxGroup = (_props: ComboboxGroupProps): null => null
26
+
27
+ ComboboxGroup.displayName = 'ComboboxGroup'
@@ -0,0 +1,44 @@
1
+ import { ReactNode } from 'react'
2
+ import { Key } from 'react-stately'
3
+
4
+ export interface ComboboxItemProps {
5
+ /**
6
+ * Content of the option. Used as the filter/typeahead text when `textValue` isn't set — pass
7
+ * `textValue` explicitly whenever `children` isn't a plain string (e.g. it wraps an icon or
8
+ * other rich markup), since it can't otherwise be derived from rich content.
9
+ */
10
+ children: ReactNode
11
+ /**
12
+ * Secondary line of text rendered below `children` (`.menu-item-description`).
13
+ */
14
+ description?: ReactNode
15
+ /**
16
+ * Prevents the option from being selected, focused, or otherwise interacted with.
17
+ */
18
+ disabled?: boolean
19
+ /**
20
+ * Icon rendered at the option's leading edge (`.menu-item-icon`).
21
+ */
22
+ icon?: ReactNode
23
+ /**
24
+ * Identifies this option. Submitted as the value when this option is selected.
25
+ */
26
+ id: Key
27
+ /**
28
+ * Text used for filtering and typeahead. Required when `children` isn't a plain string —
29
+ * falls back to `children` itself when omitted and `children` is a string.
30
+ */
31
+ textValue?: string
32
+ }
33
+
34
+ // `ComboboxItem` is never actually mounted — like `Tab`, it's read as data by `Combobox`,
35
+ // which builds react-aria's listbox collection from each item's `id` and `children`. This keeps
36
+ // the public authoring shape as plain composed JSX:
37
+ //
38
+ // <Combobox aria-label="Fruit">
39
+ // <ComboboxItem id="apple">Apple</ComboboxItem>
40
+ // <ComboboxItem id="banana">Banana</ComboboxItem>
41
+ // </Combobox>
42
+ export const ComboboxItem = (_props: ComboboxItemProps): null => null
43
+
44
+ ComboboxItem.displayName = 'ComboboxItem'