@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,246 @@
1
+ import React, { forwardRef, InputHTMLAttributes, ReactNode, useRef } from 'react'
2
+ import classNames from 'classnames'
3
+ import { AriaSwitchProps, useSwitch } from 'react-aria'
4
+ import { useToggleState } from 'react-stately'
5
+
6
+ import { useForkedRef } from '../../hooks'
7
+ import { ContextColor } from '../../types'
8
+ import { validationClassName } from '../../utils/validationClassName'
9
+
10
+ import { FormLabel } from '../form/FormLabel'
11
+
12
+ export interface SwitchProps extends Omit<
13
+ InputHTMLAttributes<HTMLInputElement>,
14
+ 'checked' | 'defaultChecked' | 'onChange' | 'size'
15
+ > {
16
+ /**
17
+ * A string of all className you want applied to the component.
18
+ */
19
+ className?: string
20
+ /**
21
+ * Sets the color of the switch indicator to one of Chassis context colors.
22
+ */
23
+ color?: ContextColor
24
+ /**
25
+ * Whether the switch is selected, uncontrolled.
26
+ */
27
+ defaultSelected?: boolean
28
+ /**
29
+ * The id global attribute defines an identifier (ID) that must be unique in the whole document.
30
+ */
31
+ id?: string
32
+ /**
33
+ * Set component validation state to invalid.
34
+ */
35
+ invalid?: boolean
36
+ /**
37
+ * Whether the switch is selected, controlled.
38
+ */
39
+ isSelected?: boolean
40
+ /**
41
+ * The element represents a caption for a component.
42
+ */
43
+ label?: ReactNode
44
+ /**
45
+ * Callback fired when the selected state changes.
46
+ */
47
+ onChange?: (isSelected: boolean) => void
48
+ /**
49
+ * Size the component small or large.
50
+ */
51
+ size?: 'small' | 'large'
52
+ /**
53
+ * Specifies the type of component.
54
+ */
55
+ type?: 'checkbox' | 'radio'
56
+ /**
57
+ * Set component validation state to valid.
58
+ */
59
+ valid?: boolean
60
+ }
61
+
62
+ type SwitchVariantProps = Omit<SwitchProps, 'type'>
63
+
64
+ interface RenderSwitchOptions {
65
+ checkInputClassName: string
66
+ className: string
67
+ input: ReactNode
68
+ label?: ReactNode
69
+ }
70
+
71
+ // Mirrors renderFormCheck's nested-label/bare-span shape — Switch inlines its own markup (see
72
+ // FORMS.md) rather than sharing that helper directly, since `role="switch"` placement didn't fit
73
+ // it cleanly. Keep this in sync with renderFormCheck by eye if either changes.
74
+ const renderSwitch = ({ checkInputClassName, className, input, label }: RenderSwitchOptions) => {
75
+ if (!label) {
76
+ return <span className={checkInputClassName}>{input}</span>
77
+ }
78
+ return (
79
+ <FormLabel customClassName={className}>
80
+ <span className={checkInputClassName}>{input}</span>
81
+ {label}
82
+ </FormLabel>
83
+ )
84
+ }
85
+
86
+ interface RenderSwitchInputOptions {
87
+ className?: string
88
+ color?: ContextColor
89
+ forkedRef: React.Ref<HTMLInputElement>
90
+ id?: string
91
+ inputProps: InputHTMLAttributes<HTMLInputElement>
92
+ invalid?: boolean
93
+ label?: ReactNode
94
+ size?: 'small' | 'large'
95
+ valid?: boolean
96
+ }
97
+
98
+ // Shared className-building/markup for `SwitchCheckbox`/`SwitchRadio` — the two variants only
99
+ // differ in how `inputProps` gets built (a react-aria hook vs. a hand-wired radio), everything
100
+ // after that is identical.
101
+ const renderSwitchInput = ({
102
+ className,
103
+ color,
104
+ forkedRef,
105
+ id,
106
+ inputProps,
107
+ invalid,
108
+ label,
109
+ size,
110
+ valid
111
+ }: RenderSwitchInputOptions) => {
112
+ const inputClassName = classNames(validationClassName(invalid, valid))
113
+ const checkInputClassName = classNames('check-input', color, validationClassName(invalid, valid))
114
+ const _className = classNames(
115
+ 'form-check form-switch',
116
+ size,
117
+ validationClassName(invalid, valid),
118
+ className
119
+ )
120
+
121
+ return renderSwitch({
122
+ checkInputClassName,
123
+ className: _className,
124
+ input: <input {...inputProps} className={inputClassName} id={id} ref={forkedRef} />,
125
+ label
126
+ })
127
+ }
128
+
129
+ // Checkbox-backed switch: a real toggle, so it goes through react-aria's useSwitch/useToggleState
130
+ // like Checkbox does. Split into its own component (rather than branching inside one Switch on
131
+ // `type`) so SwitchRadio below never has to call these hooks just to discard their result —
132
+ // calling them unconditionally and ignoring the output risked react-aria's own dev-mode warnings
133
+ // (e.g. its missing-accessible-name check) firing for state that was never actually rendered.
134
+ const SwitchCheckbox = forwardRef<HTMLInputElement, SwitchVariantProps>(
135
+ (
136
+ {
137
+ className,
138
+ color,
139
+ defaultSelected,
140
+ disabled,
141
+ id,
142
+ invalid,
143
+ isSelected,
144
+ label,
145
+ onChange,
146
+ size,
147
+ valid,
148
+ ...rest
149
+ },
150
+ ref
151
+ ) => {
152
+ const inputRef = useRef<HTMLInputElement>(null)
153
+ const forkedRef = useForkedRef(ref, inputRef)
154
+
155
+ const toggleState = useToggleState({
156
+ defaultSelected,
157
+ isDisabled: disabled,
158
+ isSelected,
159
+ onChange
160
+ })
161
+
162
+ const { inputProps: switchProps } = useSwitch(
163
+ {
164
+ ...rest,
165
+ children: label,
166
+ isDisabled: disabled,
167
+ isInvalid: invalid,
168
+ value: rest.value as string | undefined
169
+ } as AriaSwitchProps,
170
+ toggleState,
171
+ inputRef
172
+ )
173
+
174
+ return renderSwitchInput({
175
+ className,
176
+ color,
177
+ forkedRef,
178
+ id,
179
+ inputProps: switchProps,
180
+ invalid,
181
+ label,
182
+ size,
183
+ valid
184
+ })
185
+ }
186
+ )
187
+ SwitchCheckbox.displayName = 'SwitchCheckbox'
188
+
189
+ // Radio-backed switch: react-aria has no ungrouped radio hook (see Checkbox's own split for the
190
+ // same reason), so this stays a native, hand-wired `<input type="radio" role="switch">` — no
191
+ // react-aria hook to call here at all.
192
+ const SwitchRadio = forwardRef<HTMLInputElement, SwitchVariantProps>(
193
+ (
194
+ {
195
+ className,
196
+ color,
197
+ defaultSelected,
198
+ disabled,
199
+ id,
200
+ invalid,
201
+ isSelected,
202
+ label,
203
+ onChange,
204
+ size,
205
+ valid,
206
+ ...rest
207
+ },
208
+ ref
209
+ ) => {
210
+ const inputRef = useRef<HTMLInputElement>(null)
211
+ const forkedRef = useForkedRef(ref, inputRef)
212
+
213
+ const radioProps = {
214
+ ...rest,
215
+ 'aria-invalid': invalid,
216
+ checked: isSelected,
217
+ defaultChecked: defaultSelected,
218
+ disabled,
219
+ onChange: onChange
220
+ ? (event: React.ChangeEvent<HTMLInputElement>) => onChange(event.target.checked)
221
+ : undefined,
222
+ role: 'switch' as const,
223
+ type: 'radio' as const
224
+ }
225
+
226
+ return renderSwitchInput({
227
+ className,
228
+ color,
229
+ forkedRef,
230
+ id,
231
+ inputProps: radioProps,
232
+ invalid,
233
+ label,
234
+ size,
235
+ valid
236
+ })
237
+ }
238
+ )
239
+ SwitchRadio.displayName = 'SwitchRadio'
240
+
241
+ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
242
+ ({ type = 'checkbox', ...rest }, ref) =>
243
+ type === 'radio' ? <SwitchRadio {...rest} ref={ref} /> : <SwitchCheckbox {...rest} ref={ref} />
244
+ )
245
+
246
+ Switch.displayName = 'Switch'
@@ -0,0 +1,2 @@
1
+ export { Switch } from './Switch'
2
+ export type { SwitchProps } from './Switch'
@@ -0,0 +1,39 @@
1
+ /*
2
+ * Table's `<table>`/`<thead>`/`<tbody>`/`<tr>`/`<th>`/`<td>` chrome reuses chassis-css's
3
+ * existing `.table` class as-is (colors, borders, striping, hover, the `.active` row highlight —
4
+ * now driven by real selection state instead of a manual prop). Only the pieces with no
5
+ * chassis-css equivalent are new here: the sort indicator and the selection-checkbox column,
6
+ * both built on `@chassis-ui/tokens`' public `--cx-*` tokens for the same reason as
7
+ * DatePicker's calendar grid.
8
+ */
9
+
10
+ .table-sort-icon {
11
+ display: inline-block;
12
+ margin-inline-start: var(--cx-space-2xsmall);
13
+ font-size: .75em;
14
+ opacity: .6;
15
+ }
16
+
17
+ [aria-sort="ascending"] .table-sort-icon,
18
+ [aria-sort="descending"] .table-sort-icon {
19
+ opacity: 1;
20
+ }
21
+
22
+ [role="columnheader"][aria-sort] {
23
+ cursor: pointer;
24
+ user-select: none;
25
+ }
26
+
27
+ .table-selection-cell {
28
+ width: 1%;
29
+ padding-inline-end: 0;
30
+ text-align: center;
31
+ }
32
+
33
+ .table [role="row"]:focus-visible,
34
+ .table [role="gridcell"]:focus-visible,
35
+ .table [role="columnheader"]:focus-visible,
36
+ .table [role="rowheader"]:focus-visible {
37
+ outline: var(--cx-border-width-xlarge) solid var(--cx-primary);
38
+ outline-offset: -2px;
39
+ }
@@ -0,0 +1,390 @@
1
+ import React, { ReactElement, ReactNode, Ref, forwardRef, useRef } from 'react'
2
+ import classNames from 'classnames'
3
+ import {
4
+ AriaCheckboxProps,
5
+ Key,
6
+ useCheckbox,
7
+ useTable,
8
+ useTableCell,
9
+ useTableColumnHeader,
10
+ useTableHeaderRow,
11
+ useTableRow,
12
+ useTableRowGroup,
13
+ useTableSelectAllCheckbox,
14
+ useTableSelectionCheckbox
15
+ } from 'react-aria'
16
+ import {
17
+ Selection,
18
+ SortDescriptor,
19
+ TableBodyProps,
20
+ TableHeaderProps,
21
+ TableState,
22
+ useTableState,
23
+ useToggleState
24
+ } from 'react-stately'
25
+
26
+ import { useForkedRef } from '../../hooks/useForkedRef'
27
+ import { ContextColor } from '../../types'
28
+ import './Table.css'
29
+
30
+ // `GridNode<T>` (from `@react-types/shared`) isn't re-exported by either package's public
31
+ // surface — derived from the hooks' own parameter types instead of adding an undeclared
32
+ // dependency on react-aria/react-stately's shared internals package.
33
+ type ColumnNode<T> = Parameters<typeof useTableColumnHeader<T>>[0]['node']
34
+ type RowNode<T> = Parameters<typeof useTableRow<T>>[0]['node']
35
+ type CellNode<T> = Parameters<typeof useTableCell<T>>[0]['node']
36
+
37
+ export interface TableProps<T extends object> {
38
+ /**
39
+ * An accessible label for the table, used when there's no visible heading.
40
+ */
41
+ 'aria-label'?: string
42
+ /**
43
+ * Identifies a visible heading element for the table.
44
+ */
45
+ 'aria-labelledby'?: string
46
+ /**
47
+ * Set the vertical alignment of cell content.
48
+ */
49
+ align?: 'bottom' | 'middle' | 'top'
50
+ /**
51
+ * Add borders on all sides of the table and cells.
52
+ */
53
+ bordered?: boolean
54
+ /**
55
+ * Remove borders on all sides of the table and cells.
56
+ */
57
+ borderless?: boolean
58
+ /**
59
+ * Content shown above the table, functioning as a heading for it.
60
+ */
61
+ caption?: ReactNode
62
+ /**
63
+ * A `TableHeader` and a `TableBody`, each built from `TableColumn`/`TableRow`/
64
+ * `TableCell` — read as data to build the table's collection. Not rendered directly.
65
+ */
66
+ children: [
67
+ ReactElement<TableHeaderProps<T> & { className?: string }>,
68
+ ReactElement<TableBodyProps<T> & { className?: string }>
69
+ ]
70
+ /**
71
+ * A string of all className you want applied to the component.
72
+ */
73
+ className?: string
74
+ /**
75
+ * Sets the color of the component.
76
+ */
77
+ color?: ContextColor
78
+ /**
79
+ * A list of row keys to disable. Disabled rows cannot be selected, focused, or interacted with.
80
+ */
81
+ disabledKeys?: Iterable<Key>
82
+ /**
83
+ * Content shown below the table in a `<tfoot>`, e.g. a totals row. Rendered as plain markup —
84
+ * not part of the keyboard-navigable grid.
85
+ */
86
+ footer?: ReactNode
87
+ /**
88
+ * Enable a hover state on table rows.
89
+ */
90
+ hover?: boolean
91
+ /**
92
+ * `id` forwarded to the table element.
93
+ */
94
+ id?: string
95
+ /**
96
+ * Handler that is called when the selection changes.
97
+ */
98
+ onSelectionChange?: (keys: Selection) => void
99
+ /**
100
+ * Handler that is called when a column is sorted.
101
+ */
102
+ onSortChange?: (descriptor: SortDescriptor) => void
103
+ /**
104
+ * Make any table responsive across all viewports or pick a maximum breakpoint.
105
+ */
106
+ responsive?: boolean | 'small' | 'medium' | 'large' | 'xlarge' | '2xlarge'
107
+ /**
108
+ * The currently selected row keys (controlled).
109
+ */
110
+ selectedKeys?: Selection
111
+ /**
112
+ * The type of selection that is allowed.
113
+ */
114
+ selectionMode?: 'none' | 'single' | 'multiple'
115
+ /**
116
+ * Make table more compact by cutting all cell padding.
117
+ */
118
+ small?: boolean
119
+ /**
120
+ * The current sort column and direction.
121
+ */
122
+ sortDescriptor?: SortDescriptor
123
+ /**
124
+ * Convert rows into stacked label/value blocks below a container width, for tables with too
125
+ * many columns to read comfortably even with horizontal scrolling. `true` always stacks; a
126
+ * breakpoint name stacks only below it. Implies `responsive` when `responsive` isn't set
127
+ * separately, since stacking needs the same `.table-responsive` container-query ancestor.
128
+ * Labels come from each `TableColumn`'s text (or its `textValue`, for non-text headers).
129
+ */
130
+ stacked?: boolean | 'small' | 'medium' | 'large' | 'xlarge' | '2xlarge'
131
+ /**
132
+ * Add zebra-striping to table rows.
133
+ */
134
+ striped?: boolean
135
+ }
136
+
137
+ const TableInner = <T extends object>(
138
+ {
139
+ align,
140
+ bordered,
141
+ borderless,
142
+ caption,
143
+ children,
144
+ className,
145
+ color,
146
+ disabledKeys,
147
+ footer,
148
+ hover,
149
+ id,
150
+ onSelectionChange,
151
+ onSortChange,
152
+ responsive,
153
+ selectedKeys,
154
+ selectionMode = 'none',
155
+ small,
156
+ sortDescriptor,
157
+ stacked,
158
+ striped,
159
+ ...rest
160
+ }: TableProps<T>,
161
+ forwardedRef: Ref<HTMLTableElement>
162
+ ) => {
163
+ const state = useTableState<T>({
164
+ children,
165
+ disabledKeys,
166
+ onSelectionChange,
167
+ onSortChange,
168
+ selectedKeys,
169
+ selectionMode,
170
+ showSelectionCheckboxes: selectionMode === 'multiple',
171
+ sortDescriptor
172
+ })
173
+
174
+ const internalRef = useRef<HTMLTableElement>(null)
175
+ const ref = useForkedRef(internalRef, forwardedRef)
176
+ const { gridProps } = useTable(
177
+ { 'aria-label': rest['aria-label'], 'aria-labelledby': rest['aria-labelledby'], id },
178
+ state,
179
+ internalRef
180
+ )
181
+
182
+ const _className = classNames(
183
+ 'table',
184
+ color,
185
+ {
186
+ [`align-${align}`]: align,
187
+ bordered,
188
+ borderless,
189
+ hoverable: hover,
190
+ small,
191
+ stacked: stacked === true,
192
+ striped
193
+ },
194
+ typeof stacked === 'string' ? `max-${stacked}:stacked` : undefined,
195
+ className
196
+ )
197
+
198
+ const tableEl = (
199
+ <table {...gridProps} className={_className || undefined} ref={ref}>
200
+ {caption && <caption>{caption}</caption>}
201
+ <TableRowGroup className={children[0].props.className} type="thead">
202
+ {state.collection.headerRows.map((headerRow) => (
203
+ <TableHeaderRow key={headerRow.key} item={headerRow} state={state}>
204
+ {[...(state.collection.getChildren?.(headerRow.key) ?? [])].map((column) =>
205
+ column.props?.isSelectionCell ? (
206
+ <TableSelectAllCell key={column.key} column={column} state={state} />
207
+ ) : (
208
+ <TableColumnHeader key={column.key} column={column} state={state} />
209
+ )
210
+ )}
211
+ </TableHeaderRow>
212
+ ))}
213
+ </TableRowGroup>
214
+ <TableRowGroup className={children[1].props.className} type="tbody">
215
+ {[...state.collection.body.childNodes].map((row) => (
216
+ <TableRow key={row.key} item={row} state={state}>
217
+ {[...(state.collection.getChildren?.(row.key) ?? [])].map((cell) =>
218
+ cell.props?.isSelectionCell ? (
219
+ <TableSelectionCell key={cell.key} cell={cell} state={state} />
220
+ ) : (
221
+ <TableCell key={cell.key} cell={cell} stacked={!!stacked} state={state} />
222
+ )
223
+ )}
224
+ </TableRow>
225
+ ))}
226
+ </TableRowGroup>
227
+ {footer && <tfoot>{footer}</tfoot>}
228
+ </table>
229
+ )
230
+
231
+ if (!responsive && !stacked) return tableEl
232
+
233
+ const wrapperClassName = responsive
234
+ ? typeof responsive === 'boolean'
235
+ ? 'table-responsive'
236
+ : `max-${responsive}:table-responsive`
237
+ : 'table-responsive'
238
+
239
+ return <div className={wrapperClassName}>{tableEl}</div>
240
+ }
241
+
242
+ // `forwardRef` erases type parameters, so the generic component is cast back to a generic
243
+ // signature for callers — same reasoning as `TableHeader`/`TableBody`'s casts above.
244
+ export const Table = forwardRef(TableInner) as <T extends object>(
245
+ props: TableProps<T> & { ref?: Ref<HTMLTableElement> }
246
+ ) => ReactElement
247
+
248
+ ;(Table as { displayName?: string }).displayName = 'Table'
249
+
250
+ const TableRowGroup = ({
251
+ className,
252
+ type: Element,
253
+ children
254
+ }: {
255
+ children: ReactNode
256
+ className?: string
257
+ type: 'thead' | 'tbody'
258
+ }) => {
259
+ const { rowGroupProps } = useTableRowGroup()
260
+ return (
261
+ <Element {...rowGroupProps} className={className || undefined}>
262
+ {children}
263
+ </Element>
264
+ )
265
+ }
266
+
267
+ interface TableHeaderRowProps<T> {
268
+ children: ReactNode
269
+ item: RowNode<T>
270
+ state: TableState<T>
271
+ }
272
+
273
+ const TableHeaderRow = <T extends object>({ children, item, state }: TableHeaderRowProps<T>) => {
274
+ const ref = useRef<HTMLTableRowElement>(null)
275
+ const { rowProps } = useTableHeaderRow({ node: item }, state, ref)
276
+ return (
277
+ <tr {...rowProps} ref={ref}>
278
+ {children}
279
+ </tr>
280
+ )
281
+ }
282
+
283
+ interface TableColumnHeaderProps<T> {
284
+ column: ColumnNode<T>
285
+ state: TableState<T>
286
+ }
287
+
288
+ const TableColumnHeader = <T extends object>({ column, state }: TableColumnHeaderProps<T>) => {
289
+ const ref = useRef<HTMLTableCellElement>(null)
290
+ const { columnHeaderProps } = useTableColumnHeader({ node: column }, state, ref)
291
+ const sortActive = state.sortDescriptor?.column === column.key
292
+ const sortIcon = state.sortDescriptor?.direction === 'ascending' ? '▲' : '▼'
293
+
294
+ return (
295
+ <th
296
+ {...columnHeaderProps}
297
+ className={column.props?.className || undefined}
298
+ colSpan={column.colSpan ?? undefined}
299
+ ref={ref}
300
+ >
301
+ {column.rendered}
302
+ {column.props?.allowsSorting && (
303
+ <span aria-hidden="true" className="table-sort-icon">
304
+ {sortActive ? sortIcon : '⇅'}
305
+ </span>
306
+ )}
307
+ </th>
308
+ )
309
+ }
310
+
311
+ interface TableSelectAllCellProps<T> {
312
+ column: ColumnNode<T>
313
+ state: TableState<T>
314
+ }
315
+
316
+ const TableSelectAllCell = <T extends object>({ column, state }: TableSelectAllCellProps<T>) => {
317
+ const ref = useRef<HTMLTableCellElement>(null)
318
+ const { columnHeaderProps } = useTableColumnHeader({ node: column }, state, ref)
319
+ const { checkboxProps } = useTableSelectAllCheckbox(state)
320
+
321
+ return (
322
+ <th {...columnHeaderProps} className="table-selection-cell" ref={ref}>
323
+ <TableCheckbox checkboxProps={checkboxProps} />
324
+ </th>
325
+ )
326
+ }
327
+
328
+ interface TableRowProps<T> {
329
+ children: ReactNode
330
+ item: RowNode<T>
331
+ state: TableState<T>
332
+ }
333
+
334
+ const TableRow = <T extends object>({ children, item, state }: TableRowProps<T>) => {
335
+ const ref = useRef<HTMLTableRowElement>(null)
336
+ const { rowProps } = useTableRow({ node: item }, state, ref)
337
+ const isSelected = state.selectionManager.isSelected(item.key)
338
+
339
+ return (
340
+ <tr
341
+ {...rowProps}
342
+ className={classNames({ active: isSelected }, item.props?.className)}
343
+ ref={ref}
344
+ >
345
+ {children}
346
+ </tr>
347
+ )
348
+ }
349
+
350
+ interface TableCellProps<T> {
351
+ cell: CellNode<T>
352
+ stacked?: boolean
353
+ state: TableState<T>
354
+ }
355
+
356
+ const TableCell = <T extends object>({ cell, stacked, state }: TableCellProps<T>) => {
357
+ const ref = useRef<HTMLTableCellElement>(null)
358
+ const { gridCellProps } = useTableCell({ node: cell }, state, ref)
359
+
360
+ return (
361
+ <td
362
+ {...gridCellProps}
363
+ className={cell.props?.className || undefined}
364
+ data-cell={stacked ? cell.column?.textValue || undefined : undefined}
365
+ ref={ref}
366
+ >
367
+ {cell.rendered}
368
+ </td>
369
+ )
370
+ }
371
+
372
+ const TableSelectionCell = <T extends object>({ cell, state }: TableCellProps<T>) => {
373
+ const ref = useRef<HTMLTableCellElement>(null)
374
+ const { gridCellProps } = useTableCell({ node: cell }, state, ref)
375
+ const { checkboxProps } = useTableSelectionCheckbox({ key: cell.parentKey! }, state)
376
+
377
+ return (
378
+ <td {...gridCellProps} className="table-selection-cell" ref={ref}>
379
+ <TableCheckbox checkboxProps={checkboxProps} />
380
+ </td>
381
+ )
382
+ }
383
+
384
+ const TableCheckbox = ({ checkboxProps }: { checkboxProps: AriaCheckboxProps }) => {
385
+ const toggleState = useToggleState(checkboxProps)
386
+ const ref = useRef<HTMLInputElement>(null)
387
+ const { inputProps } = useCheckbox(checkboxProps, toggleState, ref)
388
+
389
+ return <input {...inputProps} className="check-input" ref={ref} />
390
+ }
@@ -0,0 +1,25 @@
1
+ import { ReactElement } from 'react'
2
+ import { TableBody as StatelyTableBody } from 'react-stately'
3
+
4
+ export interface TableBodyProps<T> {
5
+ /**
6
+ * `TableRow` elements, or a render function paired with `items` for dynamic row generation.
7
+ */
8
+ children: ReactElement | ReactElement[] | ((item: T) => ReactElement)
9
+ /**
10
+ * A string of all className you want applied to the `<tbody>` element.
11
+ */
12
+ className?: string
13
+ /**
14
+ * A list of row data objects, rendered via the function form of `children`.
15
+ */
16
+ items?: Iterable<T>
17
+ }
18
+
19
+ /**
20
+ * Collection node, data-only — read by `Table` to build the table's row collection. Never
21
+ * rendered directly.
22
+ */
23
+ export const TableBody = StatelyTableBody as unknown as <T>(
24
+ props: TableBodyProps<T>
25
+ ) => ReactElement