@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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/index.d.ts +5909 -0
- package/dist/index.js +2 -0
- package/dist/style.css +1 -0
- package/package.json +114 -0
- package/src/components/accordion/Accordion.tsx +190 -0
- package/src/components/accordion/AccordionBody.tsx +46 -0
- package/src/components/accordion/AccordionHeader.tsx +21 -0
- package/src/components/accordion/AccordionItem.tsx +59 -0
- package/src/components/accordion/context.ts +8 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/autocomplete/Autocomplete.tsx +464 -0
- package/src/components/autocomplete/AutocompleteGroup.tsx +20 -0
- package/src/components/autocomplete/AutocompleteItem.tsx +18 -0
- package/src/components/autocomplete/index.ts +6 -0
- package/src/components/avatar/Avatar.tsx +134 -0
- package/src/components/avatar/AvatarImage.tsx +19 -0
- package/src/components/avatar/AvatarStack.tsx +98 -0
- package/src/components/avatar/index.ts +6 -0
- package/src/components/badge/Badge.tsx +94 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/breadcrumb/Breadcrumb.tsx +59 -0
- package/src/components/breadcrumb/BreadcrumbItem.tsx +38 -0
- package/src/components/breadcrumb/index.ts +4 -0
- package/src/components/button/Button.tsx +229 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/button-group/ButtonGroup.tsx +55 -0
- package/src/components/button-group/ButtonToolbar.tsx +47 -0
- package/src/components/button-group/index.ts +4 -0
- package/src/components/calendar/Calendar.scss +57 -0
- package/src/components/calendar/Calendar.tsx +324 -0
- package/src/components/calendar/CalendarMonthBlock.tsx +57 -0
- package/src/components/calendar/CalendarMonthGrid.tsx +92 -0
- package/src/components/calendar/CalendarMonthYearPicker.tsx +162 -0
- package/src/components/calendar/CalendarNavButton.tsx +24 -0
- package/src/components/calendar/CalendarWeekGrid.tsx +58 -0
- package/src/components/calendar/CalendarYearGrid.tsx +126 -0
- package/src/components/calendar/RangeCalendar.scss +95 -0
- package/src/components/calendar/RangeCalendar.tsx +361 -0
- package/src/components/calendar/index.ts +5 -0
- package/src/components/card/Card.tsx +173 -0
- package/src/components/card/CardBody.tsx +75 -0
- package/src/components/card/CardFooter.tsx +47 -0
- package/src/components/card/CardGroup.tsx +51 -0
- package/src/components/card/CardHeader.tsx +47 -0
- package/src/components/card/CardImage.tsx +70 -0
- package/src/components/card/CardImageOverlay.tsx +47 -0
- package/src/components/card/CardLink.tsx +40 -0
- package/src/components/card/CardSubtitle.tsx +47 -0
- package/src/components/card/CardText.tsx +47 -0
- package/src/components/card/CardTitle.tsx +47 -0
- package/src/components/card/index.ts +22 -0
- package/src/components/carousel/Carousel.tsx +742 -0
- package/src/components/carousel/CarouselControlButton.tsx +74 -0
- package/src/components/carousel/CarouselControlNext.tsx +22 -0
- package/src/components/carousel/CarouselControlPrev.tsx +22 -0
- package/src/components/carousel/CarouselIndicators.tsx +44 -0
- package/src/components/carousel/CarouselInner.tsx +56 -0
- package/src/components/carousel/CarouselItem.tsx +51 -0
- package/src/components/carousel/CarouselOverlay.tsx +51 -0
- package/src/components/carousel/CarouselPlayPause.tsx +61 -0
- package/src/components/carousel/carouselEngine.ts +170 -0
- package/src/components/carousel/context.ts +29 -0
- package/src/components/carousel/index.ts +21 -0
- package/src/components/checkbox/Checkbox.tsx +236 -0
- package/src/components/checkbox/CheckboxGroup.tsx +145 -0
- package/src/components/checkbox/context.ts +14 -0
- package/src/components/checkbox/index.ts +4 -0
- package/src/components/chip/Chip.tsx +183 -0
- package/src/components/chip/index.ts +2 -0
- package/src/components/chip-input/ChipInput.tsx +363 -0
- package/src/components/chip-input/ChipList.tsx +95 -0
- package/src/components/chip-input/index.ts +2 -0
- package/src/components/close-button/CloseButton.tsx +205 -0
- package/src/components/close-button/index.ts +2 -0
- package/src/components/collapse/Collapse.tsx +128 -0
- package/src/components/collapse/index.ts +2 -0
- package/src/components/color-input/ColorInput.tsx +106 -0
- package/src/components/color-input/index.ts +2 -0
- package/src/components/combobox/Combobox.tsx +320 -0
- package/src/components/combobox/ComboboxGroup.tsx +27 -0
- package/src/components/combobox/ComboboxItem.tsx +44 -0
- package/src/components/combobox/ComboboxListBox.tsx +98 -0
- package/src/components/combobox/index.ts +6 -0
- package/src/components/datepicker/CalendarToggleButton.tsx +58 -0
- package/src/components/datepicker/ClearButton.tsx +35 -0
- package/src/components/datepicker/DateField.tsx +54 -0
- package/src/components/datepicker/DatePicker.scss +51 -0
- package/src/components/datepicker/DatePicker.tsx +574 -0
- package/src/components/datepicker/DateRangePicker.scss +11 -0
- package/src/components/datepicker/DateRangePicker.tsx +343 -0
- package/src/components/datepicker/index.ts +4 -0
- package/src/components/datepicker/renderDatePickerShell.tsx +104 -0
- package/src/components/drawer/Drawer.tsx +211 -0
- package/src/components/drawer/DrawerBody.tsx +47 -0
- package/src/components/drawer/DrawerFooter.tsx +51 -0
- package/src/components/drawer/DrawerHeader.tsx +60 -0
- package/src/components/drawer/DrawerTitle.tsx +49 -0
- package/src/components/drawer/index.ts +10 -0
- package/src/components/file-input/FileInput.tsx +100 -0
- package/src/components/file-input/index.ts +2 -0
- package/src/components/flex/Flex.tsx +134 -0
- package/src/components/flex/index.ts +2 -0
- package/src/components/floating-input/FloatingInput.tsx +94 -0
- package/src/components/floating-input/index.ts +2 -0
- package/src/components/form/Form.tsx +26 -0
- package/src/components/form/FormFeedback.tsx +64 -0
- package/src/components/form/FormHelp.tsx +46 -0
- package/src/components/form/FormLabel.tsx +26 -0
- package/src/components/form/index.ts +8 -0
- package/src/components/form/renderFormCheck.tsx +86 -0
- package/src/components/form-field/FormField.tsx +84 -0
- package/src/components/form-field/index.ts +2 -0
- package/src/components/form-field/renderFormField.tsx +74 -0
- package/src/components/grid/Col.tsx +69 -0
- package/src/components/grid/Container.tsx +59 -0
- package/src/components/grid/Grid.tsx +86 -0
- package/src/components/grid/GridItem.tsx +123 -0
- package/src/components/grid/Row.tsx +71 -0
- package/src/components/grid/gap.ts +8 -0
- package/src/components/grid/index.ts +10 -0
- package/src/components/icon/Icon.tsx +74 -0
- package/src/components/icon/index.ts +2 -0
- package/src/components/input-adorn/InputAdorn.tsx +55 -0
- package/src/components/input-adorn/index.ts +2 -0
- package/src/components/input-group/InputGroup.tsx +50 -0
- package/src/components/input-group/InputGroupAddon.tsx +50 -0
- package/src/components/input-group/index.ts +4 -0
- package/src/components/link/Link.tsx +148 -0
- package/src/components/link/index.ts +2 -0
- package/src/components/list/List.tsx +193 -0
- package/src/components/list/ListItem.tsx +91 -0
- package/src/components/list/index.ts +4 -0
- package/src/components/menu/Menu.tsx +398 -0
- package/src/components/menu/MenuDivider.tsx +19 -0
- package/src/components/menu/MenuHeader.tsx +55 -0
- package/src/components/menu/MenuItem.tsx +85 -0
- package/src/components/menu/MenuItemDef.ts +83 -0
- package/src/components/menu/MenuList.tsx +142 -0
- package/src/components/menu/MenuSubmenu.tsx +284 -0
- package/src/components/menu/MenuSubmenuBack.tsx +39 -0
- package/src/components/menu/MenuText.tsx +47 -0
- package/src/components/menu/MenuToggle.tsx +148 -0
- package/src/components/menu/index.ts +19 -0
- package/src/components/menu/menuNavigation.ts +106 -0
- package/src/components/menu/submenuGroup.ts +43 -0
- package/src/components/modal/Modal.tsx +162 -0
- package/src/components/modal/ModalBody.tsx +47 -0
- package/src/components/modal/ModalFooter.tsx +51 -0
- package/src/components/modal/ModalHeader.tsx +60 -0
- package/src/components/modal/ModalTitle.tsx +49 -0
- package/src/components/modal/index.ts +10 -0
- package/src/components/nav/Nav.tsx +97 -0
- package/src/components/nav/NavItem.tsx +42 -0
- package/src/components/nav/NavLink.tsx +48 -0
- package/src/components/nav/NavTitle.tsx +22 -0
- package/src/components/nav/index.ts +7 -0
- package/src/components/navbar/Navbar.tsx +112 -0
- package/src/components/navbar/NavbarBrand.tsx +55 -0
- package/src/components/navbar/NavbarNav.tsx +47 -0
- package/src/components/navbar/NavbarText.tsx +47 -0
- package/src/components/navbar/NavbarToggler.tsx +34 -0
- package/src/components/navbar/index.ts +10 -0
- package/src/components/notification/Notification.scss +15 -0
- package/src/components/notification/Notification.tsx +187 -0
- package/src/components/notification/NotificationIcon.tsx +21 -0
- package/src/components/notification/NotificationStack.tsx +66 -0
- package/src/components/notification/NotificationText.tsx +47 -0
- package/src/components/notification/NotificationTitle.tsx +47 -0
- package/src/components/notification/context.ts +14 -0
- package/src/components/notification/index.ts +17 -0
- package/src/components/notification/notificationQueue.ts +48 -0
- package/src/components/otp-input/OtpBox.tsx +68 -0
- package/src/components/otp-input/OtpInput.tsx +341 -0
- package/src/components/otp-input/index.ts +2 -0
- package/src/components/pagination/Pagination.tsx +199 -0
- package/src/components/pagination/PaginationItem.tsx +133 -0
- package/src/components/pagination/index.ts +4 -0
- package/src/components/password-strength/PasswordStrength.tsx +155 -0
- package/src/components/password-strength/index.ts +2 -0
- package/src/components/password-strength/strengthScore.ts +79 -0
- package/src/components/placeholder/Placeholder.tsx +178 -0
- package/src/components/placeholder/index.ts +2 -0
- package/src/components/popover/Popover.tsx +243 -0
- package/src/components/popover/index.ts +2 -0
- package/src/components/progress/Progress.tsx +154 -0
- package/src/components/progress/ProgressBar.tsx +88 -0
- package/src/components/progress/index.ts +4 -0
- package/src/components/radio/Radio.tsx +88 -0
- package/src/components/radio/RadioGroup.tsx +142 -0
- package/src/components/radio/context.ts +14 -0
- package/src/components/radio/index.ts +4 -0
- package/src/components/range-input/RangeInput.tsx +109 -0
- package/src/components/range-input/index.ts +2 -0
- package/src/components/select/Select.tsx +277 -0
- package/src/components/select/index.ts +2 -0
- package/src/components/skeleton/Skeleton.tsx +89 -0
- package/src/components/skeleton/SkeletonLoader.tsx +91 -0
- package/src/components/skeleton/index.ts +4 -0
- package/src/components/spinner/Spinner.tsx +77 -0
- package/src/components/spinner/index.ts +2 -0
- package/src/components/stack/Stack.tsx +81 -0
- package/src/components/stack/index.ts +2 -0
- package/src/components/stepper/Stepper.tsx +172 -0
- package/src/components/stepper/StepperItem.tsx +66 -0
- package/src/components/stepper/index.ts +4 -0
- package/src/components/switch/Switch.tsx +246 -0
- package/src/components/switch/index.ts +2 -0
- package/src/components/table/Table.css +39 -0
- package/src/components/table/Table.tsx +390 -0
- package/src/components/table/TableBody.tsx +25 -0
- package/src/components/table/TableCell.tsx +27 -0
- package/src/components/table/TableColumn.tsx +31 -0
- package/src/components/table/TableHeader.tsx +26 -0
- package/src/components/table/TableRow.tsx +24 -0
- package/src/components/table/index.ts +12 -0
- package/src/components/tabs/Tab.tsx +36 -0
- package/src/components/tabs/TabList.tsx +76 -0
- package/src/components/tabs/TabPanel.tsx +68 -0
- package/src/components/tabs/Tabs.tsx +136 -0
- package/src/components/tabs/context.ts +24 -0
- package/src/components/tabs/index.ts +8 -0
- package/src/components/text-input/TextInput.tsx +188 -0
- package/src/components/text-input/index.ts +2 -0
- package/src/components/textarea/Textarea.tsx +145 -0
- package/src/components/textarea/index.ts +2 -0
- package/src/components/toast/Toast.tsx +219 -0
- package/src/components/toast/ToastBody.tsx +64 -0
- package/src/components/toast/ToastFooter.tsx +47 -0
- package/src/components/toast/ToastHeader.tsx +104 -0
- package/src/components/toast/ToastIcon.tsx +21 -0
- package/src/components/toast/Toaster.tsx +87 -0
- package/src/components/toast/context.ts +14 -0
- package/src/components/toast/index.ts +18 -0
- package/src/components/toast/toastQueue.ts +56 -0
- package/src/components/tooltip/Tooltip.tsx +155 -0
- package/src/components/tooltip/index.ts +2 -0
- package/src/global.d.ts +2 -0
- package/src/hooks/index.ts +57 -0
- package/src/hooks/useAutoDismiss.ts +88 -0
- package/src/hooks/useButtonSemantics.ts +33 -0
- package/src/hooks/useControllableState.ts +24 -0
- package/src/hooks/useDialogElement.ts +216 -0
- package/src/hooks/useDisabledAnchorGuard.ts +22 -0
- package/src/hooks/useDismissibleTransition.ts +126 -0
- package/src/hooks/useDrawer.ts +9 -0
- package/src/hooks/useFloatingOverlay.ts +115 -0
- package/src/hooks/useForkedRef.ts +50 -0
- package/src/hooks/useFormField.ts +89 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +9 -0
- package/src/hooks/useModal.ts +9 -0
- package/src/hooks/useNotification.ts +10 -0
- package/src/hooks/useOverlayPlacement.ts +93 -0
- package/src/hooks/usePagination.ts +85 -0
- package/src/hooks/useToast.ts +9 -0
- package/src/hooks/useToastRegionQueue.ts +27 -0
- package/src/index.ts +276 -0
- package/src/types.ts +133 -0
- package/src/utils/breakpoints.ts +43 -0
- package/src/utils/comboboxCollection.tsx +152 -0
- package/src/utils/dateRangePresets.ts +6 -0
- package/src/utils/dialogTransition.ts +39 -0
- package/src/utils/focusRedirect.ts +29 -0
- package/src/utils/isWholeUnitDisabled.ts +17 -0
- package/src/utils/mergeIsDateUnavailable.ts +15 -0
- package/src/utils/overlayPlacement.ts +93 -0
- package/src/utils/pointerInteraction.ts +21 -0
- package/src/utils/polymorphic.ts +36 -0
- package/src/utils/renderMenuItemContent.tsx +62 -0
- package/src/utils/setVisibleRangeStart.ts +64 -0
- package/src/utils/spacingClassName.ts +14 -0
- package/src/utils/suppressFocusRingGlobally.ts +69 -0
- package/src/utils/validationClassName.ts +8 -0
- 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
|
+
|