@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,55 @@
1
+ import React, { ElementType, ForwardRefRenderFunction, ReactElement } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import {
5
+ createPolymorphicComponent,
6
+ PolymorphicComponentProps,
7
+ PolymorphicRef
8
+ } from '../../utils/polymorphic'
9
+
10
+ type ButtonGroupOwnProps<C extends ElementType> = {
11
+ /**
12
+ * A string of all className you want applied to the base component.
13
+ */
14
+ className?: string
15
+ /**
16
+ * Component used for the root node. Either a string to use a HTML element or a component.
17
+ */
18
+ component?: C
19
+ /**
20
+ * Size the component small or large.
21
+ */
22
+ size?: 'small' | 'large'
23
+ /**
24
+ * Create a set of buttons that appear vertically stacked rather than horizontally. Split button dropdowns are not supported here.
25
+ */
26
+ vertical?: boolean
27
+ }
28
+
29
+ export type ButtonGroupProps<C extends ElementType = 'div'> = PolymorphicComponentProps<
30
+ C,
31
+ ButtonGroupOwnProps<C>
32
+ >
33
+
34
+ type ButtonGroupComponent = (<C extends ElementType = 'div'>(
35
+ props: ButtonGroupProps<C> & { ref?: PolymorphicRef<C> }
36
+ ) => ReactElement | null) & { displayName?: string }
37
+
38
+ function ButtonGroupRender<C extends ElementType = 'div'>(
39
+ { children, className, component, size, vertical, ...rest }: ButtonGroupProps<C>,
40
+ ref: PolymorphicRef<C>
41
+ ) {
42
+ const Component = component ?? 'div'
43
+ const _className = classNames('button-group', { vertical }, size, className)
44
+
45
+ return (
46
+ <Component className={_className} {...rest} ref={ref}>
47
+ {children}
48
+ </Component>
49
+ )
50
+ }
51
+
52
+ export const ButtonGroup = createPolymorphicComponent<ButtonGroupComponent>(
53
+ ButtonGroupRender as ForwardRefRenderFunction<Element, ButtonGroupProps<ElementType>>,
54
+ 'ButtonGroup'
55
+ )
@@ -0,0 +1,47 @@
1
+ import React, { ElementType, ForwardRefRenderFunction, ReactElement } from 'react'
2
+ import classNames from 'classnames'
3
+
4
+ import {
5
+ createPolymorphicComponent,
6
+ PolymorphicComponentProps,
7
+ PolymorphicRef
8
+ } from '../../utils/polymorphic'
9
+
10
+ type ButtonToolbarOwnProps<C extends ElementType> = {
11
+ /**
12
+ * A string of all className you want applied to the base component.
13
+ */
14
+ className?: string
15
+ /**
16
+ * Component used for the root node. Either a string to use a HTML element or a component.
17
+ */
18
+ component?: C
19
+ }
20
+
21
+ export type ButtonToolbarProps<C extends ElementType = 'div'> = PolymorphicComponentProps<
22
+ C,
23
+ ButtonToolbarOwnProps<C>
24
+ >
25
+
26
+ type ButtonToolbarComponent = (<C extends ElementType = 'div'>(
27
+ props: ButtonToolbarProps<C> & { ref?: PolymorphicRef<C> }
28
+ ) => ReactElement | null) & { displayName?: string }
29
+
30
+ function ButtonToolbarRender<C extends ElementType = 'div'>(
31
+ { children, className, component, ...rest }: ButtonToolbarProps<C>,
32
+ ref: PolymorphicRef<C>
33
+ ) {
34
+ const Component = component ?? 'div'
35
+ const _className = classNames('button-toolbar', className)
36
+
37
+ return (
38
+ <Component className={_className} {...rest} ref={ref}>
39
+ {children}
40
+ </Component>
41
+ )
42
+ }
43
+
44
+ export const ButtonToolbar = createPolymorphicComponent<ButtonToolbarComponent>(
45
+ ButtonToolbarRender as ForwardRefRenderFunction<Element, ButtonToolbarProps<ElementType>>,
46
+ 'ButtonToolbar'
47
+ )
@@ -0,0 +1,4 @@
1
+ export { ButtonGroup } from './ButtonGroup'
2
+ export type { ButtonGroupProps } from './ButtonGroup'
3
+ export { ButtonToolbar } from './ButtonToolbar'
4
+ export type { ButtonToolbarProps } from './ButtonToolbar'
@@ -0,0 +1,57 @@
1
+ @use "@chassis-ui/css/scss/config" as *;
2
+ @use "@chassis-ui/css/scss/mixins" as *;
3
+
4
+ /*
5
+ * `Calendar` reuses chassis-css's `.datepicker-*` classes, so layout/arrows/header/today/disabled/
6
+ * focus styling already comes from `@chassis-ui/css`. This file only covers the gaps: states
7
+ * vanilla-calendar-pro exposes via `data-vc-date-*` (no react-aria equivalent, re-expressed as
8
+ * `.datepicker-date-*` classes) and the month/year picker, which chassis-css styles via attribute
9
+ * selectors this component doesn't have.
10
+ */
11
+
12
+ @layer components {
13
+ .datepicker-body {
14
+ position: relative;
15
+ }
16
+
17
+ .datepicker-months-month.selected,
18
+ .datepicker-years-year.selected {
19
+ color: var(--primary-contrast);
20
+ background-color: var(--primary);
21
+ }
22
+
23
+ .datepicker-months-month.selected:hover,
24
+ .datepicker-years-year.selected:hover {
25
+ background-color: var(--primary);
26
+ }
27
+
28
+ /* Leading/trailing days from the adjacent month */
29
+ .datepicker-date-outside > .datepicker-date-btn {
30
+ color: var(--day-disabled-fg-color);
31
+ }
32
+
33
+ /* Weekend, muted unless selected/unavailable/disabled takes priority. `:where()` keeps these
34
+ guards from adding specificity over chassis-css's "today" rule. */
35
+ .datepicker-date-weekend:where(:not(.datepicker-date-selected):not(.datepicker-date-unavailable)) > .datepicker-date-btn:where(:not([aria-disabled="true"])) {
36
+ color: var(--fg-subtle);
37
+ }
38
+
39
+ /* Unavailable (`isDateUnavailable`) — focusable and announced, unlike native disabled. */
40
+ .datepicker-date-unavailable:where(:not(.datepicker-date-selected)) > .datepicker-date-btn {
41
+ color: var(--day-disabled-fg-color);
42
+ text-decoration: line-through;
43
+ }
44
+
45
+ /* Selected (single-date only; `RangeCalendar.scss` handles start/end/in-range). */
46
+ .datepicker-date-selected > .datepicker-date-btn {
47
+ color: var(--primary-contrast);
48
+ background-color: var(--primary);
49
+ }
50
+
51
+ /* Extra `.datepicker-date` qualifier wins the specificity tie with chassis-css's "today" rule
52
+ when a date is both selected and today. */
53
+ .datepicker-date-selected.datepicker-date[aria-current="date"] > .datepicker-date-btn {
54
+ color: var(--primary-contrast);
55
+ background-color: var(--primary);
56
+ }
57
+ }
@@ -0,0 +1,324 @@
1
+ import React, { forwardRef, HTMLAttributes, useMemo, useRef, useState } from 'react'
2
+ import classNames from 'classnames'
3
+ import { AriaCalendarProps, mergeProps, useCalendar, useCalendarCell, useLocale } from 'react-aria'
4
+ import { CalendarState as StatelyCalendarState, DateValue, useCalendarState } from 'react-stately'
5
+ import {
6
+ CalendarDate,
7
+ createCalendar,
8
+ getLocalTimeZone,
9
+ isToday,
10
+ isWeekend,
11
+ today
12
+ } from '@internationalized/date'
13
+
14
+ import { useForkedRef } from '../../hooks'
15
+ import { CalendarMonthBlock } from './CalendarMonthBlock'
16
+ import { CalendarMonthYearPicker } from './CalendarMonthYearPicker'
17
+ import { CalendarNavButton } from './CalendarNavButton'
18
+ import { CalendarWeekGrid } from './CalendarWeekGrid'
19
+ import { mergeIsDateUnavailable } from '../../utils/mergeIsDateUnavailable'
20
+ import './Calendar.scss'
21
+
22
+ interface CalendarBaseProps extends Omit<
23
+ HTMLAttributes<HTMLDivElement>,
24
+ 'onChange' | 'defaultValue'
25
+ > {
26
+ /**
27
+ * An accessible label for the calendar, used when there's no visible label. Required for
28
+ * standalone use — a calendar grid has no other accessible name of its own.
29
+ */
30
+ 'aria-label'?: string
31
+ /**
32
+ * Identifies a visible label element for the calendar.
33
+ */
34
+ 'aria-labelledby'?: string
35
+ /**
36
+ * Whether to automatically focus the calendar when it mounts.
37
+ */
38
+ autoFocus?: boolean
39
+ /**
40
+ * A string of all className you want applied to the base component.
41
+ */
42
+ className?: string
43
+ /**
44
+ * Prevents the calendar from being focused or interacted with.
45
+ */
46
+ disabled?: boolean
47
+ /**
48
+ * The day that starts the week, overriding the default set by the active locale.
49
+ *
50
+ * @default 'mon'
51
+ */
52
+ firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
53
+ /**
54
+ * Callback that is called for each date in the calendar. If it returns `true`, that date is
55
+ * shown but cannot be selected.
56
+ */
57
+ isDateUnavailable?: (date: DateValue) => boolean
58
+ /**
59
+ * The maximum allowed date that a user may select.
60
+ */
61
+ maxValue?: DateValue | null
62
+ /**
63
+ * The minimum allowed date that a user may select.
64
+ */
65
+ minValue?: DateValue | null
66
+ /**
67
+ * ISO 8601 dates (`YYYY-MM-DD`) to mark unselectable, as a convenience alternative to
68
+ * `isDateUnavailable` for data-driven cases (e.g. booked dates fetched from an API). Composed
69
+ * with `isDateUnavailable` when both are given — a date unavailable by either is unavailable.
70
+ */
71
+ unavailableDates?: string[]
72
+ /**
73
+ * Number of months to display side by side, sharing one selection. Wraps to multiple rows in
74
+ * a narrow container (e.g. a popover on a small screen) rather than overflowing — the wrap is
75
+ * driven by the calendar's own width, not the viewport, so it adapts correctly regardless of
76
+ * where the calendar is embedded.
77
+ *
78
+ * @default 1
79
+ */
80
+ visibleMonths?: number
81
+ }
82
+
83
+ export interface CalendarSingleProps extends CalendarBaseProps {
84
+ /**
85
+ * The initial selected date (uncontrolled).
86
+ */
87
+ defaultValue?: DateValue | null
88
+ /**
89
+ * Callback fired when the selected date changes. Unlike `DatePicker`'s `onChange` (whose
90
+ * segmented field can be cleared to `null`), a calendar selection is always a concrete date.
91
+ */
92
+ onChange?: (value: DateValue) => void
93
+ /**
94
+ * Whether a single date or multiple, independently toggled dates can be selected.
95
+ *
96
+ * @default 'single'
97
+ */
98
+ selectionMode?: 'single'
99
+ /**
100
+ * The selected date (controlled).
101
+ */
102
+ value?: DateValue | null
103
+ }
104
+
105
+ export interface CalendarMultipleProps extends CalendarBaseProps {
106
+ /**
107
+ * The initial selected dates (uncontrolled).
108
+ */
109
+ defaultValue?: DateValue[] | null
110
+ /**
111
+ * Callback fired when the set of selected dates changes.
112
+ */
113
+ onChange?: (value: DateValue[]) => void
114
+ /**
115
+ * Whether a single date or multiple, independently toggled dates can be selected.
116
+ *
117
+ * @default 'single'
118
+ */
119
+ selectionMode: 'multiple'
120
+ /**
121
+ * The selected dates (controlled).
122
+ */
123
+ value?: DateValue[] | null
124
+ }
125
+
126
+ export type CalendarProps = CalendarSingleProps | CalendarMultipleProps
127
+
128
+ // Public, standalone calendar grid — also used internally by `DatePicker` to render the grid
129
+ // inside its popover. Deliberately has no knowledge of dialog/popover semantics (role="dialog",
130
+ // focus trapping, etc.) — that's the caller's concern, layered on via the `...rest` passthrough
131
+ // below (see `DatePicker`, which merges its own `useDialog` output in this way).
132
+ //
133
+ // `selectionMode` is a discriminated union on the public props (single vs. multiple value/
134
+ // onChange shapes), bridged onto one underlying `useCalendarState`/`useCalendar` call instantiated
135
+ // over the union of both modes — react-stately/react-aria are natively selection-mode-aware at
136
+ // that layer (including `useCalendarCell`'s `isSelected`), so no custom multi-select logic is
137
+ // needed here, just narrow, provably-safe casts at the seam between the public union and the
138
+ // single generic call (react-stately always reports/expects the value shape matching whichever
139
+ // `selectionMode` was actually passed in).
140
+ export const Calendar = forwardRef<HTMLDivElement, CalendarProps>((props, forwardedRef) => {
141
+ const {
142
+ autoFocus,
143
+ className,
144
+ defaultValue,
145
+ disabled,
146
+ firstDayOfWeek = 'mon',
147
+ isDateUnavailable,
148
+ maxValue,
149
+ minValue,
150
+ onChange,
151
+ selectionMode = 'single',
152
+ unavailableDates,
153
+ value,
154
+ visibleMonths = 1,
155
+ ...rest
156
+ } = props
157
+
158
+ const { locale } = useLocale()
159
+ const internalRef = useRef<HTMLDivElement>(null)
160
+ const ref = useForkedRef(internalRef, forwardedRef)
161
+ const combinedIsDateUnavailable = useMemo(
162
+ () => mergeIsDateUnavailable(unavailableDates, isDateUnavailable),
163
+ [unavailableDates, isDateUnavailable]
164
+ )
165
+ // Tracks each visible month block's own view, keyed by `monthIndex` — same reasoning as
166
+ // `RangeCalendar`'s identical state: needed only to know whether *any* block has switched
167
+ // away from the day grid, so the global `.datepicker-controls` overlay can hide itself rather
168
+ // than sit on top of `CalendarYearGrid`'s own pager.
169
+ const [monthViews, setMonthViews] = useState<Record<number, 'days' | 'months' | 'years'>>({})
170
+ const hasPickerView = Object.values(monthViews).some((view) => view !== 'days')
171
+
172
+ const handleChange = (next: DateValue | readonly DateValue[]) => {
173
+ if (selectionMode === 'multiple') {
174
+ ;(onChange as ((value: DateValue[]) => void) | undefined)?.(next as DateValue[])
175
+ } else {
176
+ ;(onChange as ((value: DateValue) => void) | undefined)?.(next as DateValue)
177
+ }
178
+ }
179
+ const normalizedValue = value as DateValue | readonly DateValue[] | null | undefined
180
+ const normalizedDefaultValue = defaultValue as DateValue | readonly DateValue[] | null | undefined
181
+ // react-stately's `useCalendarState` crashes computing the initial focused date for
182
+ // `selectionMode: 'multiple'` when the (controlled or default) value is a genuinely empty
183
+ // array — its own fallback-to-today logic only runs when the value is nullish, but an empty
184
+ // array is truthy, so it instead indexes the (nonexistent) first element and calls `.subtract`
185
+ // on the resulting `undefined`. Supplying `defaultFocusedValue` ourselves in exactly that case
186
+ // short-circuits react-stately's logic before it ever reaches the buggy branch, without
187
+ // affecting the normal "focus follows the selection" behavior whenever there is one.
188
+ const resolvedValue = normalizedValue !== undefined ? normalizedValue : normalizedDefaultValue
189
+ const defaultFocusedValue =
190
+ selectionMode === 'multiple' && Array.isArray(resolvedValue) && resolvedValue.length === 0
191
+ ? today(getLocalTimeZone())
192
+ : undefined
193
+
194
+ const state = useCalendarState<DateValue, 'single' | 'multiple'>({
195
+ autoFocus,
196
+ createCalendar,
197
+ defaultFocusedValue,
198
+ defaultValue: normalizedDefaultValue,
199
+ firstDayOfWeek,
200
+ isDateUnavailable: combinedIsDateUnavailable,
201
+ isDisabled: disabled,
202
+ locale,
203
+ maxValue,
204
+ minValue,
205
+ onChange: handleChange,
206
+ // Prev/next always slide the visible window by one month, regardless of `visibleMonths` —
207
+ // same reasoning as `RangeCalendar`'s identical setting (see that component's own comment).
208
+ pageBehavior: 'single',
209
+ selectionMode,
210
+ value: normalizedValue,
211
+ visibleDuration: { months: visibleMonths }
212
+ })
213
+
214
+ const ariaProps: AriaCalendarProps<DateValue, 'single' | 'multiple'> = {
215
+ 'aria-label': rest['aria-label'],
216
+ 'aria-labelledby': rest['aria-labelledby'],
217
+ autoFocus,
218
+ defaultFocusedValue,
219
+ defaultValue: normalizedDefaultValue,
220
+ isDateUnavailable: combinedIsDateUnavailable,
221
+ isDisabled: disabled,
222
+ maxValue,
223
+ minValue,
224
+ onChange: handleChange,
225
+ selectionMode,
226
+ value: normalizedValue
227
+ }
228
+
229
+ const { calendarProps, prevButtonProps, nextButtonProps } = useCalendar(ariaProps, state)
230
+ const prevButton = <CalendarNavButton buttonProps={prevButtonProps} direction="prev" />
231
+ const nextButton = <CalendarNavButton buttonProps={nextButtonProps} direction="next" />
232
+
233
+ return (
234
+ <div
235
+ {...mergeProps(calendarProps, rest)}
236
+ className={classNames('datepicker', className)}
237
+ data-cx-inline="true"
238
+ ref={ref}
239
+ >
240
+ {visibleMonths === 1 ? (
241
+ <CalendarMonthYearPicker
242
+ monthStart={state.visibleRange.start}
243
+ nextArrow={nextButton}
244
+ prevArrow={prevButton}
245
+ state={state}
246
+ >
247
+ <CalendarWeekGrid
248
+ firstDayOfWeek={firstDayOfWeek}
249
+ renderCell={(date) => <CalendarCell date={date} locale={locale} state={state} />}
250
+ state={state}
251
+ />
252
+ </CalendarMonthYearPicker>
253
+ ) : (
254
+ <>
255
+ {/* `.datepicker[data-cx-inline]` (the root above) is already `position: relative` in
256
+ chassis-css, so this overlay needs no extra positioning wrapper of its own — unlike
257
+ `RangeCalendar`, which has a `presets` column sharing that root and so scopes its
258
+ own copy of this overlay to a nested `.datepicker-body` instead. */}
259
+ {!hasPickerView && (
260
+ <div className="datepicker-controls">
261
+ {prevButton}
262
+ {nextButton}
263
+ </div>
264
+ )}
265
+ <div className="datepicker-grid">
266
+ {[...new Array(visibleMonths).keys()].map((monthIndex) => (
267
+ <CalendarMonthBlock
268
+ firstDayOfWeek={firstDayOfWeek}
269
+ key={monthIndex}
270
+ monthIndex={monthIndex}
271
+ onViewChange={(view) => setMonthViews((prev) => ({ ...prev, [monthIndex]: view }))}
272
+ renderCell={(date) => <CalendarCell date={date} locale={locale} state={state} />}
273
+ state={state}
274
+ />
275
+ ))}
276
+ </div>
277
+ </>
278
+ )}
279
+ </div>
280
+ )
281
+ })
282
+
283
+ Calendar.displayName = 'Calendar'
284
+
285
+ type CalendarState = StatelyCalendarState<'single' | 'multiple'>
286
+
287
+ interface CalendarCellProps {
288
+ date: CalendarDate
289
+ locale: string
290
+ state: CalendarState
291
+ }
292
+
293
+ const CalendarCell = ({ date, locale, state }: CalendarCellProps) => {
294
+ const ref = useRef<HTMLButtonElement>(null)
295
+ const {
296
+ cellProps,
297
+ buttonProps,
298
+ isSelected,
299
+ isOutsideVisibleRange,
300
+ isDisabled,
301
+ isUnavailable,
302
+ formattedDate
303
+ } = useCalendarCell({ date }, state, ref)
304
+ const isCurrentDate = isToday(date, getLocalTimeZone())
305
+
306
+ return (
307
+ <div
308
+ {...cellProps}
309
+ aria-current={isCurrentDate ? 'date' : undefined}
310
+ className={classNames('datepicker-date', {
311
+ 'datepicker-date-selected': isSelected,
312
+ 'datepicker-date-today': isCurrentDate,
313
+ 'datepicker-date-outside': isOutsideVisibleRange,
314
+ 'datepicker-date-disabled': isDisabled,
315
+ 'datepicker-date-unavailable': isUnavailable,
316
+ 'datepicker-date-weekend': isWeekend(date, locale)
317
+ })}
318
+ >
319
+ <button {...buttonProps} type="button" className="datepicker-date-btn" ref={ref}>
320
+ {formattedDate}
321
+ </button>
322
+ </div>
323
+ )
324
+ }
@@ -0,0 +1,57 @@
1
+ import React, { ReactNode } from 'react'
2
+ import { CalendarState, RangeCalendarState } from 'react-stately'
3
+ import { CalendarDate } from '@internationalized/date'
4
+
5
+ import { CalendarMonthYearPicker } from './CalendarMonthYearPicker'
6
+ import { CalendarWeekGrid } from './CalendarWeekGrid'
7
+
8
+ interface CalendarMonthBlockProps {
9
+ // `null`/omitted when a global prev/next pair elsewhere is paging every visible month block at
10
+ // once (`RangeCalendar` with `visibleMonths > 1`) rather than this block having its own — see
11
+ // that component's own `arrows` prop.
12
+ arrows?: { next: ReactNode; prev: ReactNode } | null
13
+ firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
14
+ monthIndex: number
15
+ onViewChange: (view: 'days' | 'months' | 'years') => void
16
+ // The individual cell differs between `Calendar` and `RangeCalendar` (range selection needs
17
+ // start/end/in-between pill state a single-date cell has no equivalent for), so cell rendering
18
+ // is left to the caller — same shape `CalendarWeekGrid` itself already expects.
19
+ renderCell: (date: CalendarDate, index: number, week: (CalendarDate | null)[]) => ReactNode
20
+ state: CalendarState<'single' | 'multiple'> | RangeCalendarState
21
+ }
22
+
23
+ // One visible month within `visibleMonths` — shared by `Calendar` and `RangeCalendar`, which
24
+ // otherwise duplicated this wiring near-verbatim. `monthIndex` is the offset of this block's own
25
+ // month from `state.focusedDate` (`0` for a single-month calendar), letting `setVisibleRangeStart`
26
+ // (inside `CalendarMonthYearPicker`) land a pick correctly regardless of which visible block it
27
+ // came from.
28
+ export const CalendarMonthBlock = ({
29
+ arrows,
30
+ firstDayOfWeek,
31
+ monthIndex,
32
+ onViewChange,
33
+ renderCell,
34
+ state
35
+ }: CalendarMonthBlockProps) => {
36
+ const monthStart = state.visibleRange.start.add({ months: monthIndex })
37
+
38
+ return (
39
+ <>
40
+ <CalendarMonthYearPicker
41
+ monthIndex={monthIndex}
42
+ monthStart={monthStart}
43
+ nextArrow={arrows?.next}
44
+ onViewChange={onViewChange}
45
+ prevArrow={arrows?.prev}
46
+ state={state}
47
+ >
48
+ <CalendarWeekGrid
49
+ firstDayOfWeek={firstDayOfWeek}
50
+ renderCell={renderCell}
51
+ startDate={monthStart}
52
+ state={state}
53
+ />
54
+ </CalendarMonthYearPicker>
55
+ </>
56
+ )
57
+ }
@@ -0,0 +1,92 @@
1
+ import React from 'react'
2
+ import classNames from 'classnames'
3
+ import { useDateFormatter } from 'react-aria'
4
+ import { CalendarState, RangeCalendarState } from 'react-stately'
5
+ import { CalendarDate } from '@internationalized/date'
6
+
7
+ import { isWholeUnitDisabled } from '../../utils/isWholeUnitDisabled'
8
+
9
+ interface CalendarMonthGridProps {
10
+ // Whether to render the visually-hidden `aria-live` announcement of this view's current header
11
+ // (see `CalendarMonthYearPicker`'s own equivalent gate) — `false` for every visible month block
12
+ // but the first when `visibleMonths > 1`, so switching into this view doesn't fire one
13
+ // simultaneous announcement per block.
14
+ announce?: boolean
15
+ monthStart: CalendarDate
16
+ onBack: () => void
17
+ onSelect: (date: CalendarDate) => void
18
+ state: CalendarState<'single' | 'multiple'> | RangeCalendarState
19
+ }
20
+
21
+ // The month view of `CalendarMonthYearPicker` — every month in `monthStart`'s own year, laid out
22
+ // as a static 3-column grid (no paging; picking a different year is the year view's job).
23
+ export const CalendarMonthGrid = ({
24
+ announce = true,
25
+ monthStart,
26
+ onBack,
27
+ onSelect,
28
+ state
29
+ }: CalendarMonthGridProps) => {
30
+ const monthFormatter = useDateFormatter({
31
+ calendar: monthStart.calendar.identifier,
32
+ month: 'short',
33
+ timeZone: state.timeZone
34
+ })
35
+ const yearFormatter = useDateFormatter({
36
+ calendar: monthStart.calendar.identifier,
37
+ timeZone: state.timeZone,
38
+ year: 'numeric'
39
+ })
40
+
41
+ const numMonths = monthStart.calendar.getMonthsInYear(monthStart)
42
+ const months = [...new Array(numMonths).keys()].map((i) => {
43
+ const date = monthStart.set({ day: 1, month: i + 1 })
44
+ return { date, formatted: monthFormatter.format(date.toDate(state.timeZone)) }
45
+ })
46
+
47
+ return (
48
+ <>
49
+ <div className="datepicker-header">
50
+ <div className="datepicker-header-content">
51
+ <button className="datepicker-month" onClick={onBack} type="button">
52
+ {yearFormatter.format(monthStart.toDate(state.timeZone))}
53
+ </button>
54
+ </div>
55
+ </div>
56
+ {announce && (
57
+ <span role="status" className="visually-hidden">
58
+ {`Select month, ${yearFormatter.format(monthStart.toDate(state.timeZone))}`}
59
+ </span>
60
+ )}
61
+ <div className="datepicker-content">
62
+ <div
63
+ aria-label={yearFormatter.format(monthStart.toDate(state.timeZone))}
64
+ className="datepicker-months"
65
+ role="group"
66
+ >
67
+ {months.map((month) => {
68
+ const isSelected = month.date.month === monthStart.month
69
+ const isDisabled = isWholeUnitDisabled(
70
+ state,
71
+ month.date,
72
+ month.date.add({ months: 1 }).subtract({ days: 1 })
73
+ )
74
+
75
+ return (
76
+ <button
77
+ aria-current={isSelected ? 'true' : undefined}
78
+ className={classNames('datepicker-months-month', { selected: isSelected })}
79
+ disabled={isDisabled}
80
+ key={month.date.month}
81
+ onClick={() => onSelect(month.date)}
82
+ type="button"
83
+ >
84
+ {month.formatted}
85
+ </button>
86
+ )
87
+ })}
88
+ </div>
89
+ </div>
90
+ </>
91
+ )
92
+ }