@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,88 @@
1
+ import React, { forwardRef, InputHTMLAttributes, ReactNode, useContext, useRef } from 'react'
2
+ import classNames from 'classnames'
3
+ import { AriaRadioProps, useRadio } from 'react-aria'
4
+
5
+ import { useForkedRef } from '../../hooks'
6
+ import { ContextColor } from '../../types'
7
+ import { validationClassName } from '../../utils/validationClassName'
8
+
9
+ import { RadioGroupContext } from './context'
10
+ import { ButtonObject, renderFormCheck } from '../form/renderFormCheck'
11
+
12
+ export interface RadioProps extends Omit<
13
+ InputHTMLAttributes<HTMLInputElement>,
14
+ 'checked' | 'defaultChecked' | 'onChange' | 'size'
15
+ > {
16
+ /**
17
+ * Create button-like radios. Combine with `<RadioGroup>` to build radio toggle-button groups.
18
+ */
19
+ button?: ButtonObject
20
+ /**
21
+ * A string of all className you want applied to the component.
22
+ */
23
+ className?: string
24
+ /**
25
+ * Sets the color of the radio indicator to one of Chassis context colors. Ignored when `button` is set.
26
+ */
27
+ color?: ContextColor
28
+ /**
29
+ * The id global attribute defines an identifier (ID) that must be unique in the whole document.
30
+ */
31
+ id?: string
32
+ /**
33
+ * The element represents a caption for a component.
34
+ */
35
+ label?: ReactNode
36
+ /**
37
+ * Size the component small or large.
38
+ */
39
+ size?: 'small' | 'large'
40
+ /**
41
+ * The value of the radio button, used to identify it within its `<RadioGroup>`.
42
+ */
43
+ value: string
44
+ }
45
+
46
+ // `<Radio>` must be rendered inside a `<RadioGroup>` — react-aria has no standalone
47
+ // radio hook, only useRadio(props, RadioGroupState, ref), because a lone radio with no group is
48
+ // not a meaningful accessible control (see https://chassis-ui.com/css/docs/forms/checkbox-radio).
49
+ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
50
+ ({ button, className, color, disabled, id, label, size, ...rest }, ref) => {
51
+ const group = useContext(RadioGroupContext)
52
+
53
+ if (!group) {
54
+ throw new Error('Radio must be rendered inside a RadioGroup.')
55
+ }
56
+
57
+ const { state: groupState, valid } = group
58
+ const invalid = groupState.isInvalid
59
+
60
+ const inputRef = useRef<HTMLInputElement>(null)
61
+ const forkedRef = useForkedRef(ref, inputRef)
62
+
63
+ const { inputProps } = useRadio(
64
+ {
65
+ ...rest,
66
+ children: label,
67
+ isDisabled: disabled
68
+ } as AriaRadioProps,
69
+ groupState,
70
+ inputRef
71
+ )
72
+
73
+ const inputClassName = classNames(validationClassName(invalid, valid))
74
+
75
+ return renderFormCheck({
76
+ button,
77
+ className,
78
+ color,
79
+ input: <input {...inputProps} className={inputClassName} id={id} ref={forkedRef} />,
80
+ invalid,
81
+ label,
82
+ size,
83
+ valid
84
+ })
85
+ }
86
+ )
87
+
88
+ Radio.displayName = 'Radio'
@@ -0,0 +1,142 @@
1
+ import React, { forwardRef, HTMLAttributes, ReactNode } from 'react'
2
+ import classNames from 'classnames'
3
+ import { AriaRadioGroupProps, useRadioGroup } from 'react-aria'
4
+ import { RadioGroupProps as StatelyRadioGroupProps, useRadioGroupState } from 'react-stately'
5
+
6
+ import { validationClassName } from '../../utils/validationClassName'
7
+
8
+ import { RadioGroupContext } from './context'
9
+ import { FormFeedback } from '../form/FormFeedback'
10
+ import { FormHelp } from '../form/FormHelp'
11
+ import { Flex } from '../flex'
12
+
13
+ export interface RadioGroupProps extends Omit<
14
+ HTMLAttributes<HTMLFieldSetElement>,
15
+ 'defaultValue' | 'onChange'
16
+ > {
17
+ /**
18
+ * One or more `<Radio>` elements.
19
+ */
20
+ children: ReactNode
21
+ /**
22
+ * A string of all className you want applied to the component.
23
+ */
24
+ className?: string
25
+ /**
26
+ * The selected value, uncontrolled.
27
+ */
28
+ defaultValue?: string
29
+ /**
30
+ * A description for the group, rendered below the options.
31
+ */
32
+ description?: ReactNode
33
+ /**
34
+ * Disables every radio in the group.
35
+ */
36
+ disabled?: boolean
37
+ /**
38
+ * An error message for the group, rendered below the options when `invalid` is set.
39
+ */
40
+ errorMessage?: ReactNode
41
+ /**
42
+ * Set group validation state to invalid.
43
+ */
44
+ invalid?: boolean
45
+ /**
46
+ * The group's caption, rendered as a `<legend>`.
47
+ */
48
+ label?: ReactNode
49
+ /**
50
+ * The name for the group, used for native form submission.
51
+ */
52
+ name?: string
53
+ /**
54
+ * Callback fired when the selected value changes.
55
+ */
56
+ onChange?: (value: string) => void
57
+ /**
58
+ * Lay the group's radios out on the same horizontal row instead of stacking them.
59
+ */
60
+ orientation?: 'horizontal' | 'vertical'
61
+ /**
62
+ * Marks the group as required.
63
+ */
64
+ required?: boolean
65
+ /**
66
+ * Set group validation state to valid.
67
+ */
68
+ valid?: boolean
69
+ /**
70
+ * The selected value, controlled.
71
+ */
72
+ value?: string
73
+ }
74
+
75
+ export const RadioGroup = forwardRef<HTMLFieldSetElement, RadioGroupProps>(
76
+ (
77
+ {
78
+ children,
79
+ className,
80
+ defaultValue,
81
+ description,
82
+ disabled,
83
+ errorMessage,
84
+ invalid,
85
+ label,
86
+ name,
87
+ onChange,
88
+ orientation,
89
+ required,
90
+ valid,
91
+ value,
92
+ ...rest
93
+ },
94
+ ref
95
+ ) => {
96
+ const groupProps = {
97
+ ...rest,
98
+ defaultValue,
99
+ description,
100
+ errorMessage,
101
+ isDisabled: disabled,
102
+ isInvalid: invalid,
103
+ isRequired: required,
104
+ label,
105
+ name,
106
+ onChange,
107
+ orientation,
108
+ value
109
+ }
110
+
111
+ const state = useRadioGroupState(groupProps as StatelyRadioGroupProps)
112
+ const { radioGroupProps, labelProps, descriptionProps, errorMessageProps } = useRadioGroup(
113
+ groupProps as AriaRadioGroupProps,
114
+ state
115
+ )
116
+
117
+ const _className = classNames('form-field', validationClassName(invalid, valid), className)
118
+
119
+ const items = (
120
+ <RadioGroupContext.Provider value={{ state, valid }}>{children}</RadioGroupContext.Provider>
121
+ )
122
+
123
+ return (
124
+ <fieldset {...rest} {...radioGroupProps} className={_className} ref={ref}>
125
+ {label && (
126
+ <legend className="form-label" {...labelProps}>
127
+ {label}
128
+ </legend>
129
+ )}
130
+ {orientation === 'horizontal' ? <Flex gap="medium">{items}</Flex> : items}
131
+ {description && <FormHelp {...descriptionProps}>{description}</FormHelp>}
132
+ {invalid && errorMessage && (
133
+ <FormFeedback invalid {...errorMessageProps}>
134
+ {errorMessage}
135
+ </FormFeedback>
136
+ )}
137
+ </fieldset>
138
+ )
139
+ }
140
+ )
141
+
142
+ RadioGroup.displayName = 'RadioGroup'
@@ -0,0 +1,14 @@
1
+ import { createContext } from 'react'
2
+ import { RadioGroupState } from 'react-stately'
3
+
4
+ export interface RadioGroupContextValue {
5
+ state: RadioGroupState
6
+ /**
7
+ * `RadioGroup`'s own `valid` prop, mirrored here since it has no react-stately equivalent to
8
+ * read off `state` the way `isInvalid` does — each `Radio` needs it to apply its own
9
+ * `is-valid` class.
10
+ */
11
+ valid?: boolean
12
+ }
13
+
14
+ export const RadioGroupContext = createContext<RadioGroupContextValue | null>(null)
@@ -0,0 +1,4 @@
1
+ export { Radio } from './Radio'
2
+ export type { RadioProps } from './Radio'
3
+ export { RadioGroup } from './RadioGroup'
4
+ export type { RadioGroupProps } from './RadioGroup'
@@ -0,0 +1,109 @@
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 RangeInputProps extends InputHTMLAttributes<HTMLInputElement> {
9
+ /**
10
+ * A string of all className you want applied to the component.
11
+ */
12
+ className?: string
13
+ /**
14
+ * Toggle the disabled state for the component.
15
+ */
16
+ disabled?: boolean
17
+ /**
18
+ * A description for the field, rendered below the range input.
19
+ */
20
+ help?: ReactNode
21
+ /**
22
+ * Set component validation state to invalid.
23
+ */
24
+ invalid?: boolean
25
+ /**
26
+ * An error message for the field, rendered below the range input when `invalid` is set.
27
+ */
28
+ invalidFeedback?: ReactNode
29
+ /**
30
+ * The field's caption, rendered as a `FormLabel` associated with this range input.
31
+ */
32
+ label?: ReactNode
33
+ /**
34
+ * Specifies the maximum value for the component.
35
+ */
36
+ max?: number
37
+ /**
38
+ * Specifies the minimum value for the component.
39
+ */
40
+ min?: number
41
+ /**
42
+ * Method called immediately after the `value` prop changes.
43
+ */
44
+ onChange?: ChangeEventHandler<HTMLInputElement>
45
+ /**
46
+ * Toggle the readonly state for the component.
47
+ */
48
+ readOnly?: boolean
49
+ /**
50
+ * Specifies the interval between legal numbers in the component.
51
+ */
52
+ step?: number
53
+ /**
54
+ * Set component validation state to valid.
55
+ */
56
+ valid?: boolean
57
+ /**
58
+ * A success message for the field, rendered below the range input when `valid` is set.
59
+ */
60
+ validFeedback?: ReactNode
61
+ /**
62
+ * The `value` attribute of component.
63
+ *
64
+ * @controllable onChange
65
+ * */
66
+ value?: string | number
67
+ }
68
+
69
+ export const RangeInput = forwardRef<HTMLInputElement, RangeInputProps>(
70
+ (
71
+ { className, help, id, invalid, invalidFeedback, label, valid, validFeedback, ...rest },
72
+ ref
73
+ ) => {
74
+ const { describedBy, feedbackId, helpId, inputId } = useFormField({
75
+ ariaDescribedBy: rest['aria-describedby'],
76
+ help,
77
+ id,
78
+ invalid,
79
+ invalidFeedback,
80
+ valid,
81
+ validFeedback
82
+ })
83
+
84
+ const _className = classNames('form-range', validationClassName(invalid, valid), className)
85
+
86
+ return renderFormField({
87
+ children: (
88
+ <input
89
+ {...rest}
90
+ aria-describedby={describedBy}
91
+ aria-invalid={invalid || undefined}
92
+ className={_className}
93
+ id={inputId}
94
+ ref={ref}
95
+ type="range"
96
+ />
97
+ ),
98
+ help,
99
+ ids: { feedback: feedbackId, help: helpId, input: inputId },
100
+ invalid,
101
+ invalidFeedback,
102
+ label,
103
+ valid,
104
+ validFeedback
105
+ })
106
+ }
107
+ )
108
+
109
+ RangeInput.displayName = 'RangeInput'
@@ -0,0 +1,2 @@
1
+ export { RangeInput } from './RangeInput'
2
+ export type { RangeInputProps } from './RangeInput'
@@ -0,0 +1,277 @@
1
+ import React, {
2
+ ChangeEventHandler,
3
+ forwardRef,
4
+ InputHTMLAttributes,
5
+ MouseEventHandler,
6
+ ReactNode,
7
+ useRef
8
+ } from 'react'
9
+ import classNames from 'classnames'
10
+
11
+ import { useForkedRef, useFormField } from '../../hooks'
12
+ import { validationClassName } from '../../utils/validationClassName'
13
+ import { renderFormField } from '../form-field/renderFormField'
14
+
15
+ export interface SelectOptionDef {
16
+ /**
17
+ * Marks the option as disabled — it can't be clicked or reached via the keyboard.
18
+ */
19
+ disabled?: boolean
20
+ /**
21
+ * Label text rendered inside the option. Falls back to the browser's default (the `value`,
22
+ * stringified) when omitted.
23
+ */
24
+ label?: string
25
+ /**
26
+ * Marks the option as selected by default (uncontrolled) — resolved into the select's own
27
+ * `defaultValue`, and ignored when the select's `value`/`defaultValue` is set directly. Set on
28
+ * more than one option only when `multiple` is also set on `Select` — otherwise, matching
29
+ * native `<select>` behavior, only the last option with `selected` set wins.
30
+ */
31
+ selected?: boolean
32
+ /**
33
+ * The option's value attribute.
34
+ */
35
+ value?: string | number
36
+ }
37
+ export interface SelectProps extends Omit<InputHTMLAttributes<HTMLSelectElement>, 'size'> {
38
+ /**
39
+ * Content rendered at the select's trailing edge, e.g. a `InputAdorn` icon, text, or button.
40
+ * Setting either `adornStart` or `adornEnd` renders a `.form-input` wrapper around a
41
+ * `.ghost-input`, matching chassis-css's [input help](https://chassis-ui.com/css/docs/forms/input-adorn) pattern.
42
+ * Clicking anywhere in the wrapper (other than an actionable adorn) opens the select, since the
43
+ * native element itself no longer fills the wrapper's full width.
44
+ */
45
+ adornEnd?: ReactNode
46
+ /**
47
+ * Content rendered at the select's leading edge, e.g. a `InputAdorn` icon, text, or button.
48
+ * Setting either `adornStart` or `adornEnd` renders a `.form-input` wrapper around a
49
+ * `.ghost-input`, matching chassis-css's [input help](https://chassis-ui.com/css/docs/forms/input-adorn) pattern.
50
+ * Clicking anywhere in the wrapper (other than an actionable adorn) opens the select, since the
51
+ * native element itself no longer fills the wrapper's full width.
52
+ */
53
+ adornStart?: ReactNode
54
+ /**
55
+ * A string of all className you want applied to the component.
56
+ */
57
+ className?: string
58
+ /**
59
+ * A description for the field, rendered below the select.
60
+ */
61
+ help?: ReactNode
62
+ /**
63
+ * Specifies the number of visible options in a drop-down list.
64
+ */
65
+ htmlSize?: number
66
+ /**
67
+ * Set component validation state to invalid.
68
+ */
69
+ invalid?: boolean
70
+ /**
71
+ * An error message for the field, rendered below the select when `invalid` is set.
72
+ */
73
+ invalidFeedback?: ReactNode
74
+ /**
75
+ * The field's caption, rendered as a `FormLabel` associated with this select.
76
+ */
77
+ label?: ReactNode
78
+ /**
79
+ * Allows more than one option to be selected at once. Pass `value` as a string array (or set
80
+ * `selected` on more than one `options` entry) to control the selection.
81
+ */
82
+ multiple?: boolean
83
+ /**
84
+ * Method called immediately after the `value` prop changes.
85
+ */
86
+ onChange?: ChangeEventHandler<HTMLSelectElement>
87
+ /**
88
+ * Options list of the select component. Available keys: `label`, `value`, `disabled`,
89
+ * `selected`.
90
+ * Examples:
91
+ * - `options={[{ value: 'js', label: 'JavaScript' }, { value: 'html', label: 'HTML', disabled: true }]}`
92
+ * - `options={['js', 'html']}`
93
+ */
94
+ options?: SelectOptionDef[] | string[]
95
+ /**
96
+ * Renders a disabled placeholder option as the first item (e.g. `"Select a country…"`).
97
+ * The option has an empty value so it is not selectable once another option is chosen.
98
+ */
99
+ placeholder?: string
100
+ /**
101
+ * Size the component small or large.
102
+ */
103
+ size?: 'small' | 'large'
104
+ /**
105
+ * Set component validation state to valid.
106
+ */
107
+ valid?: boolean
108
+ /**
109
+ * A success message for the field, rendered below the select when `valid` is set.
110
+ */
111
+ validFeedback?: ReactNode
112
+ /**
113
+ * The `value` attribute of component.
114
+ *
115
+ * @controllable onChange
116
+ * */
117
+ value?: string | string[] | number
118
+ }
119
+
120
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
121
+ (
122
+ {
123
+ adornEnd,
124
+ adornStart,
125
+ children,
126
+ className,
127
+ help,
128
+ htmlSize,
129
+ id,
130
+ invalid,
131
+ invalidFeedback,
132
+ label,
133
+ multiple,
134
+ options,
135
+ placeholder,
136
+ size,
137
+ valid,
138
+ validFeedback,
139
+ ...rest
140
+ },
141
+ ref
142
+ ) => {
143
+ const selectRef = useRef<HTMLSelectElement>(null)
144
+ const forkedRef = useForkedRef(ref, selectRef)
145
+
146
+ const selectedValues = Array.isArray(options)
147
+ ? options
148
+ .filter(
149
+ (option): option is SelectOptionDef => typeof option === 'object' && !!option.selected
150
+ )
151
+ .map((option) => String(option.value ?? ''))
152
+ : []
153
+
154
+ if (!multiple && selectedValues.length > 1) {
155
+ console.warn(
156
+ 'Select: more than one option has `selected: true` but `multiple` is not set — only ' +
157
+ 'the last one will be selected, matching native <select> behavior.'
158
+ )
159
+ }
160
+
161
+ // React warns against setting `selected` directly on <option>, recommending `defaultValue`/
162
+ // `value` on <select> instead — so `options[].selected` is resolved into a `defaultValue`
163
+ // here rather than rendered as an attribute. Left alone when the caller already controls
164
+ // `value`/`defaultValue` themselves.
165
+ const inferredDefaultValue =
166
+ selectedValues.length && rest.value === undefined && rest.defaultValue === undefined
167
+ ? multiple
168
+ ? selectedValues
169
+ : selectedValues[selectedValues.length - 1]
170
+ : undefined
171
+
172
+ const { describedBy, feedbackId, helpId, inputId } = useFormField({
173
+ ariaDescribedBy: rest['aria-describedby'],
174
+ help,
175
+ id,
176
+ invalid,
177
+ invalidFeedback,
178
+ valid,
179
+ validFeedback
180
+ })
181
+
182
+ const hasAdorn = adornStart != null || adornEnd != null
183
+ // A caret can only pop the dropdown open when the select actually renders as one - an inline
184
+ // listbox (`multiple`, or a `size` greater than 1) has no picker to show.
185
+ const isDropdown = !multiple && (!htmlSize || htmlSize <= 1)
186
+
187
+ // chassis-css's `.form-input:has(.ghost-input.is-valid)` selector reads validation state off
188
+ // the inner select, not the wrapper, once adorns turn `.form-input` into a flex container -
189
+ // see https://chassis-ui.com/css/docs/forms/input-adorn.
190
+ const selectClassName = classNames(
191
+ hasAdorn ? 'ghost-input' : 'form-input',
192
+ !hasAdorn && size,
193
+ validationClassName(invalid, valid),
194
+ !hasAdorn && className
195
+ )
196
+
197
+ const handleWrapperClick: MouseEventHandler<HTMLDivElement> = (event) => {
198
+ const node = selectRef.current
199
+ const target = event.target as HTMLElement
200
+ if (!node || node.disabled || !isDropdown || target === node || target.closest('button, a'))
201
+ return
202
+ node.focus()
203
+ try {
204
+ // showPicker() opens the native dropdown from a proxy click; unsupported browsers fall
205
+ // back to the focus() above, same as tabbing to a bare select and pressing Space/Down.
206
+ node.showPicker?.()
207
+ } catch {
208
+ // Can throw without a user activation or under a policy-restricted embed - focus() still
209
+ // leaves the select reachable via the keyboard.
210
+ }
211
+ }
212
+
213
+ const select = (
214
+ <select
215
+ {...rest}
216
+ aria-describedby={describedBy}
217
+ aria-invalid={invalid || undefined}
218
+ className={selectClassName}
219
+ id={inputId}
220
+ multiple={multiple}
221
+ ref={forkedRef}
222
+ size={htmlSize}
223
+ {...(inferredDefaultValue !== undefined && { defaultValue: inferredDefaultValue })}
224
+ >
225
+ {placeholder && (
226
+ <option value="" disabled>
227
+ {placeholder}
228
+ </option>
229
+ )}
230
+ {options
231
+ ? options.map((option, index) => {
232
+ return (
233
+ <option
234
+ {...(typeof option === 'object' &&
235
+ option.disabled && { disabled: option.disabled })}
236
+ {...(typeof option === 'object' &&
237
+ option.value != null && { value: option.value })}
238
+ // eslint-disable-next-line react/no-array-index-key
239
+ key={index}
240
+ >
241
+ {typeof option === 'string'
242
+ ? option
243
+ : (option.label ?? String(option.value ?? ''))}
244
+ </option>
245
+ )
246
+ })
247
+ : children}
248
+ </select>
249
+ )
250
+
251
+ const rendered = hasAdorn ? (
252
+ <div
253
+ className={classNames('form-input', isDropdown && 'form-caret', size, className)}
254
+ onClick={handleWrapperClick}
255
+ >
256
+ {adornStart}
257
+ {select}
258
+ {adornEnd}
259
+ </div>
260
+ ) : (
261
+ select
262
+ )
263
+
264
+ return renderFormField({
265
+ children: rendered,
266
+ help,
267
+ ids: { feedback: feedbackId, help: helpId, input: inputId },
268
+ invalid,
269
+ invalidFeedback,
270
+ label,
271
+ valid,
272
+ validFeedback
273
+ })
274
+ }
275
+ )
276
+
277
+ Select.displayName = 'Select'
@@ -0,0 +1,2 @@
1
+ export { Select } from './Select'
2
+ export type { SelectProps, SelectOptionDef } from './Select'