@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,162 @@
1
+ import React, { ReactNode, useEffect, useRef, useState } from 'react'
2
+ import { useDateFormatter } from 'react-aria'
3
+ import { CalendarState, RangeCalendarState } from 'react-stately'
4
+ import { CalendarDate } from '@internationalized/date'
5
+
6
+ import { CalendarMonthGrid } from './CalendarMonthGrid'
7
+ import { CalendarYearGrid } from './CalendarYearGrid'
8
+ import { setVisibleRangeStart } from '../../utils/setVisibleRangeStart'
9
+
10
+ interface CalendarMonthYearPickerProps {
11
+ // The day grid, shown in the default 'days' view — owned by the caller (`Calendar` and
12
+ // `RangeCalendar` render genuinely different cells: single-date vs. range pill styling), so
13
+ // this only decides *whether* it's showing, never how it's built.
14
+ children: ReactNode
15
+ monthIndex?: number
16
+ monthStart: CalendarDate
17
+ // `null` when a global prev/next pair elsewhere is paging every visible month block at once
18
+ // (`RangeCalendar` with `visibleMonths > 1`) rather than this block having its own — see that
19
+ // component's own `arrows` prop. Only ever rendered in the 'days' view; the month/year views page
20
+ // themselves independently (`CalendarYearGrid`) or don't page at all (`CalendarMonthGrid`).
21
+ nextArrow?: ReactNode
22
+ // Reports this block's own view whenever it changes, so a parent rendering several blocks
23
+ // (`RangeCalendar` with `visibleMonths > 1`) can tell when one of them has switched to the
24
+ // year view — `CalendarYearGrid` owns its own prev/next pager in that view, which would
25
+ // otherwise sit right underneath the parent's global `.datepicker-controls` overlay.
26
+ onViewChange?: (view: 'days' | 'months' | 'years') => void
27
+ prevArrow?: ReactNode
28
+ state: CalendarState<'single' | 'multiple'> | RangeCalendarState
29
+ }
30
+
31
+ // Header + body for one visible month block: the day grid by default, switching in place to a
32
+ // month or year grid when the header's own month/year button is pressed (chassis-css's actual
33
+ // `_datepicker.scss` design for this — `[data-vc="months"]`/`[data-vc="years"]`, swapped in for
34
+ // `[data-vc="dates"]` — not a `<select>`, which is what this component used to render here. Native
35
+ // `<select>` turned out to be the whole problem: picking a value forced a synchronous re-render
36
+ // from inside the browser's own `change`-event dispatch, and in Safari that corrupted `usePress`'s
37
+ // pointerup-vs-click target check for whatever the user clicked next — a page bug specific to
38
+ // native form controls, not present here since a grid button is a plain, ordinary press.
39
+ //
40
+ // Shared by `Calendar` and `RangeCalendar` — `monthIndex` is the offset of this block's own
41
+ // month from `state.focusedDate` (`0` for a single-month calendar), letting `setVisibleRangeStart`
42
+ // land a pick correctly regardless of which visible block it came from.
43
+ export const CalendarMonthYearPicker = ({
44
+ children,
45
+ monthIndex = 0,
46
+ monthStart,
47
+ nextArrow,
48
+ onViewChange,
49
+ prevArrow,
50
+ state
51
+ }: CalendarMonthYearPickerProps) => {
52
+ const [view, setView] = useState<'days' | 'months' | 'years'>('days')
53
+ const containerRef = useRef<HTMLDivElement>(null)
54
+ // Skipped on mount — there's no prior view to restore focus from yet, and stealing focus
55
+ // as soon as the calendar renders would fight `autoFocus`/the caller's own focus management.
56
+ const isFirstRender = useRef(true)
57
+
58
+ const changeView = (next: 'days' | 'months' | 'years') => {
59
+ setView(next)
60
+ onViewChange?.(next)
61
+ }
62
+
63
+ // Switching `view` swaps in a whole new subtree, unmounting whatever was focused (the month/
64
+ // year trigger button, or the month/year grid button just picked) — React has no reason to move
65
+ // focus anywhere on its own, so without this it drops to `document.body`, breaking out of
66
+ // `FocusScope`'s containment in `DatePicker`/`DateRangePicker`'s popover entirely. Lands
67
+ // focus on the selected month/year button when entering the month/year view (falling back to
68
+ // the first option if nothing is selected on the currently visible page), or back onto the
69
+ // day grid's own roving-tabindex cell when returning to 'days'.
70
+ useEffect(() => {
71
+ if (isFirstRender.current) {
72
+ isFirstRender.current = false
73
+ return
74
+ }
75
+
76
+ const container = containerRef.current
77
+ if (!container) return
78
+
79
+ const target =
80
+ view === 'days'
81
+ ? container.querySelector<HTMLElement>('.datepicker-date-btn[tabindex="0"]')
82
+ : (container.querySelector<HTMLElement>(
83
+ '.datepicker-months-month.selected, .datepicker-years-year.selected'
84
+ ) ??
85
+ container.querySelector<HTMLElement>('.datepicker-months-month, .datepicker-years-year'))
86
+
87
+ target?.focus()
88
+ }, [view])
89
+
90
+ const monthFormatter = useDateFormatter({
91
+ calendar: monthStart.calendar.identifier,
92
+ month: 'long',
93
+ timeZone: state.timeZone
94
+ })
95
+ const yearFormatter = useDateFormatter({
96
+ calendar: monthStart.calendar.identifier,
97
+ timeZone: state.timeZone,
98
+ year: 'numeric'
99
+ })
100
+
101
+ const commitAndReturn = (date: CalendarDate) => {
102
+ setVisibleRangeStart(state, date.subtract({ months: monthIndex }))
103
+ changeView('days')
104
+ }
105
+
106
+ return (
107
+ <div className="datepicker-column" ref={containerRef}>
108
+ {view === 'months' && (
109
+ <CalendarMonthGrid
110
+ announce={monthIndex === 0}
111
+ monthStart={monthStart}
112
+ onBack={() => changeView('days')}
113
+ onSelect={commitAndReturn}
114
+ state={state}
115
+ />
116
+ )}
117
+ {view === 'years' && (
118
+ <CalendarYearGrid
119
+ announce={monthIndex === 0}
120
+ monthStart={monthStart}
121
+ onBack={() => changeView('days')}
122
+ onSelect={commitAndReturn}
123
+ state={state}
124
+ />
125
+ )}
126
+ {view === 'days' && (
127
+ <>
128
+ <div className="datepicker-header">
129
+ {prevArrow}
130
+ <div className="datepicker-header-content">
131
+ <button
132
+ aria-label={`Month: ${monthFormatter.format(monthStart.toDate(state.timeZone))}`}
133
+ className="datepicker-month"
134
+ disabled={state.isDisabled}
135
+ onClick={() => changeView('months')}
136
+ type="button"
137
+ >
138
+ {monthFormatter.format(monthStart.toDate(state.timeZone))}
139
+ </button>
140
+ <button
141
+ aria-label={`Year: ${yearFormatter.format(monthStart.toDate(state.timeZone))}`}
142
+ className="datepicker-year"
143
+ disabled={state.isDisabled}
144
+ onClick={() => changeView('years')}
145
+ type="button"
146
+ >
147
+ {yearFormatter.format(monthStart.toDate(state.timeZone))}
148
+ </button>
149
+ </div>
150
+ {nextArrow}
151
+ </div>
152
+ {monthIndex === 0 && (
153
+ <span role="status" className="visually-hidden">
154
+ {`${monthFormatter.format(monthStart.toDate(state.timeZone))} ${yearFormatter.format(monthStart.toDate(state.timeZone))}`}
155
+ </span>
156
+ )}
157
+ {children}
158
+ </>
159
+ )}
160
+ </div>
161
+ )
162
+ }
@@ -0,0 +1,24 @@
1
+ import React, { useRef } from 'react'
2
+ import { AriaButtonProps, useButton } from 'react-aria'
3
+
4
+ interface CalendarNavButtonProps {
5
+ buttonProps: AriaButtonProps
6
+ direction: 'prev' | 'next'
7
+ }
8
+
9
+ // Shared prev/next arrow, used twice by both `Calendar` and `RangeCalendar` — `buttonProps`
10
+ // comes from that caller's own `useCalendar`/`useRangeCalendar`, so this only owns the DOM ref and
11
+ // the `useButton` wiring plus the arrow markup itself.
12
+ export const CalendarNavButton = ({ buttonProps, direction }: CalendarNavButtonProps) => {
13
+ const ref = useRef<HTMLButtonElement>(null)
14
+ const { buttonProps: domButtonProps } = useButton(buttonProps, ref)
15
+
16
+ return (
17
+ <button
18
+ {...domButtonProps}
19
+ className={`datepicker-arrow datepicker-arrow-${direction}`}
20
+ ref={ref}
21
+ type="button"
22
+ ></button>
23
+ )
24
+ }
@@ -0,0 +1,58 @@
1
+ import React, { ReactNode } from 'react'
2
+ import { useCalendarGrid } from 'react-aria'
3
+ import { CalendarState, RangeCalendarState } from 'react-stately'
4
+ import { CalendarDate } from '@internationalized/date'
5
+
6
+ interface CalendarWeekGridProps {
7
+ firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
8
+ renderCell: (date: CalendarDate, index: number, week: (CalendarDate | null)[]) => ReactNode
9
+ startDate?: CalendarDate
10
+ state: CalendarState<'single' | 'multiple'> | RangeCalendarState
11
+ }
12
+
13
+ // Shared week-day header + date-rows grid, used by both `Calendar` and `RangeCalendar` —
14
+ // `startDate` lets a range calendar anchor this to one of several visible months (see
15
+ // `useCalendarGrid`'s own docs); omitted, it defaults to the calendar's own visible start. Only the
16
+ // individual cell differs between the two callers (range selection needs start/end/in-between pill
17
+ // state a single-date cell has no equivalent for), so cell rendering is left to `renderCell`.
18
+ export const CalendarWeekGrid = ({
19
+ firstDayOfWeek,
20
+ renderCell,
21
+ startDate,
22
+ state
23
+ }: CalendarWeekGridProps) => {
24
+ const { gridProps, headerProps, weekDays, weeksInMonth } = useCalendarGrid(
25
+ { firstDayOfWeek, startDate },
26
+ state
27
+ )
28
+
29
+ return (
30
+ <div {...gridProps} className="datepicker-content">
31
+ <div {...headerProps} className="datepicker-week" role="row">
32
+ {weekDays.map((day, index) => (
33
+ // eslint-disable-next-line react/no-array-index-key
34
+ <span className="datepicker-week-day" key={index} role="columnheader">
35
+ {day}
36
+ </span>
37
+ ))}
38
+ </div>
39
+ <div className="datepicker-dates">
40
+ {[...new Array(weeksInMonth).keys()].map((weekIndex) => {
41
+ const week = state.getDatesInWeek(weekIndex, startDate)
42
+ return (
43
+ <div className="datepicker-dates-row" key={weekIndex} role="row">
44
+ {week.map((date, i) =>
45
+ date ? (
46
+ <React.Fragment key={date.toString()}>{renderCell(date, i, week)}</React.Fragment>
47
+ ) : (
48
+ // eslint-disable-next-line react/no-array-index-key
49
+ <div className="datepicker-date" key={i} role="gridcell" />
50
+ )
51
+ )}
52
+ </div>
53
+ )
54
+ })}
55
+ </div>
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,126 @@
1
+ import React, { useState } from 'react'
2
+ import classNames from 'classnames'
3
+ import { useDateFormatter } from 'react-aria'
4
+ import { CalendarState, RangeCalendarState } from 'react-stately'
5
+ import { CalendarDate, toCalendarDate } from '@internationalized/date'
6
+
7
+ import { isWholeUnitDisabled } from '../../utils/isWholeUnitDisabled'
8
+
9
+ interface CalendarYearGridProps {
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 (or paging within it)
13
+ // doesn't fire one 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
+ const YEARS_PER_PAGE = 15
22
+
23
+ // The year view of `CalendarMonthYearPicker` — a paged 3-column grid, `YEARS_PER_PAGE` years at a
24
+ // time starting from the currently visible year. Unlike the month view, a year range doesn't fit
25
+ // on one screen, so this owns its own prev/next paging (independent of the day grid's own
26
+ // prev/next, which keep paging by month underneath whenever this view isn't showing).
27
+ export const CalendarYearGrid = ({
28
+ announce = true,
29
+ monthStart,
30
+ onBack,
31
+ onSelect,
32
+ state
33
+ }: CalendarYearGridProps) => {
34
+ const yearFormatter = useDateFormatter({
35
+ calendar: monthStart.calendar.identifier,
36
+ timeZone: state.timeZone,
37
+ year: 'numeric'
38
+ })
39
+ // Lazy initializer only — this view unmounts on every exit, so there's no case where `monthStart`
40
+ // changes while it's still mounted that this would need to react to.
41
+ const [pageStart, setPageStart] = useState(() => monthStart.year)
42
+
43
+ // `date` preserves `monthStart`'s own month/day, changing only the year — picking a year should
44
+ // land on the same month it started from, not reset to January (`yearStart`/`yearEnd` are the
45
+ // Jan 1–Dec 31 bounds used only for the disabled-range check below, kept separate so the actual
46
+ // selection target isn't affected by them).
47
+ const years = [...new Array(YEARS_PER_PAGE).keys()].map((i) => {
48
+ const date = monthStart.set({ year: pageStart + i })
49
+ const yearStart = date.set({ day: 1, month: 1 })
50
+ const yearEnd = yearStart.add({ years: 1 }).subtract({ days: 1 })
51
+ return {
52
+ date,
53
+ formatted: yearFormatter.format(date.toDate(state.timeZone)),
54
+ yearEnd,
55
+ yearStart
56
+ }
57
+ })
58
+ // `years` always has exactly `YEARS_PER_PAGE` (15) entries — a fixed, non-zero constant, never
59
+ // empty — so the first/last indices are always in range.
60
+ const firstYear = years[0]!
61
+ const lastYear = years[years.length - 1]!
62
+ const firstYearStart = firstYear.yearStart
63
+ const lastYearEnd = lastYear.yearEnd
64
+ const rangeLabel = `${firstYear.formatted} – ${lastYear.formatted}`
65
+
66
+ const isPrevDisabled =
67
+ state.isDisabled ||
68
+ (state.minValue != null &&
69
+ firstYearStart.subtract({ days: 1 }).compare(toCalendarDate(state.minValue)) < 0)
70
+ const isNextDisabled =
71
+ state.isDisabled ||
72
+ (state.maxValue != null &&
73
+ lastYearEnd.add({ days: 1 }).compare(toCalendarDate(state.maxValue)) > 0)
74
+
75
+ return (
76
+ <>
77
+ <div className="datepicker-header">
78
+ <button
79
+ aria-label="Previous years"
80
+ className="datepicker-arrow datepicker-arrow-prev"
81
+ disabled={isPrevDisabled}
82
+ onClick={() => setPageStart((start) => start - YEARS_PER_PAGE)}
83
+ type="button"
84
+ />
85
+ <div className="datepicker-header-content">
86
+ <button className="datepicker-year" onClick={onBack} type="button">
87
+ {rangeLabel}
88
+ </button>
89
+ </div>
90
+ <button
91
+ aria-label="Next years"
92
+ className="datepicker-arrow datepicker-arrow-next"
93
+ disabled={isNextDisabled}
94
+ onClick={() => setPageStart((start) => start + YEARS_PER_PAGE)}
95
+ type="button"
96
+ />
97
+ </div>
98
+ {announce && (
99
+ <span role="status" className="visually-hidden">
100
+ {`Select year, ${rangeLabel}`}
101
+ </span>
102
+ )}
103
+ <div className="datepicker-content">
104
+ <div aria-label={rangeLabel} className="datepicker-years" role="group">
105
+ {years.map((year) => {
106
+ const isSelected = year.date.year === monthStart.year
107
+ const isDisabled = isWholeUnitDisabled(state, year.yearStart, year.yearEnd)
108
+
109
+ return (
110
+ <button
111
+ aria-current={isSelected ? 'true' : undefined}
112
+ className={classNames('datepicker-years-year', { selected: isSelected })}
113
+ disabled={isDisabled}
114
+ key={year.date.year}
115
+ onClick={() => onSelect(year.date)}
116
+ type="button"
117
+ >
118
+ {year.formatted}
119
+ </button>
120
+ )
121
+ })}
122
+ </div>
123
+ </div>
124
+ </>
125
+ )
126
+ }
@@ -0,0 +1,95 @@
1
+ @use "@chassis-ui/css/scss/config" as *;
2
+ @use "@chassis-ui/css/scss/mixins" as *;
3
+
4
+ /*
5
+ * `RangeCalendar` reuses chassis-css's `.datepicker-*` classes as-is (see `Calendar.scss`).
6
+ * This file only covers the gaps: range-specific state (`.datepicker-date-in-range`,
7
+ * `-range-start`/`-end`) and the quick-select list beside the grid, which chassis-css
8
+ * doesn't style.
9
+ */
10
+
11
+ @layer components {
12
+ .datepicker-date-in-range {
13
+ color: var(--day-range-fg-color);
14
+ background-color: var(--day-range-bg-color);
15
+ }
16
+
17
+ .datepicker-date-in-range > .datepicker-date-btn:hover {
18
+ background-color: unset;
19
+ }
20
+
21
+ .datepicker-date-in-range.datepicker-date-range-start {
22
+ @include border-top-start-radius(var(--border-radius-medium));
23
+ @include border-bottom-start-radius(var(--border-radius-medium));
24
+ }
25
+
26
+ .datepicker-date-in-range.datepicker-date-range-end {
27
+ @include border-top-end-radius(var(--border-radius-medium));
28
+ @include border-bottom-end-radius(var(--border-radius-medium));
29
+ }
30
+
31
+ .datepicker-date-btn:is(.datepicker-date-range-start, .datepicker-date-range-end),
32
+ .datepicker-date-btn:is(.datepicker-date-range-start, .datepicker-date-range-end):hover {
33
+ color: var(--primary-contrast);
34
+ background-color: var(--primary);
35
+ }
36
+
37
+ .datepicker-date-btn.datepicker-date-range-start:not(.datepicker-date-range-end) {
38
+ @include border-top-end-radius(0);
39
+ @include border-bottom-end-radius(0);
40
+ }
41
+
42
+ .datepicker-date-btn.datepicker-date-range-end:not(.datepicker-date-range-start) {
43
+ @include border-top-start-radius(0);
44
+ @include border-bottom-start-radius(0);
45
+ }
46
+
47
+ /* Wins the specificity tie with chassis-css's "today" rule for a range endpoint that's also
48
+ today (same approach as `Calendar.scss`'s selected+today override). */
49
+ .datepicker-date[aria-current="date"] > .datepicker-date-btn.datepicker-date-range-start,
50
+ .datepicker-date[aria-current="date"] > .datepicker-date-btn.datepicker-date-range-end {
51
+ color: var(--primary-contrast);
52
+ background-color: var(--primary);
53
+ }
54
+
55
+ /* Quick-select list beside the grid; no chassis-css equivalent, so fully custom. */
56
+ .datepicker-presets {
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: var(--gap);
60
+ padding-inline: 0 var(--padding-x);
61
+ margin: 0;
62
+ list-style: none;
63
+ border-inline-end: var(--border-width-medium) solid var(--border-subtle);
64
+ }
65
+
66
+ .datepicker-preset {
67
+ padding: var(--space-2xsmall) var(--space-xsmall);
68
+ color: var(--fg-main);
69
+ text-align: start;
70
+ white-space: nowrap;
71
+ cursor: pointer;
72
+ background-color: transparent;
73
+ border: 0;
74
+ @include border-radius(var(--border-radius-medium));
75
+ }
76
+
77
+ .datepicker-preset:hover {
78
+ background-color: var(--bg-hover);
79
+ }
80
+
81
+ .datepicker-preset:focus-visible {
82
+ outline: var(--border-width-xlarge) solid var(--primary);
83
+ outline-offset: -1px;
84
+ }
85
+
86
+ .datepicker-preset.selected {
87
+ color: var(--primary-contrast);
88
+ background-color: var(--primary);
89
+ }
90
+
91
+ .datepicker-preset.selected:hover {
92
+ background-color: var(--primary);
93
+ }
94
+ }
95
+