@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,15 @@
|
|
|
1
|
+
import { DateValue } from 'react-stately'
|
|
2
|
+
|
|
3
|
+
// `date.toString()` on a `CalendarDate` (the only `DateValue` variant `Calendar`/`DatePicker`
|
|
4
|
+
// ever produce — both are date-only, no time segments) yields the same ISO 8601 `YYYY-MM-DD`
|
|
5
|
+
// string already used elsewhere for form submission (see `DatePicker`'s hidden input), so
|
|
6
|
+
// `unavailableDates` entries are expected in that same format.
|
|
7
|
+
export const mergeIsDateUnavailable = (
|
|
8
|
+
unavailableDates: string[] | undefined,
|
|
9
|
+
isDateUnavailable: ((date: DateValue) => boolean) | undefined
|
|
10
|
+
): ((date: DateValue) => boolean) | undefined => {
|
|
11
|
+
if (!unavailableDates?.length && !isDateUnavailable) return undefined
|
|
12
|
+
|
|
13
|
+
const set = unavailableDates?.length ? new Set(unavailableDates) : null
|
|
14
|
+
return (date) => Boolean(set?.has(date.toString())) || Boolean(isDateUnavailable?.(date))
|
|
15
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react'
|
|
2
|
+
import type { Placement as AriaPlacement, PlacementAxis } from 'react-aria'
|
|
3
|
+
|
|
4
|
+
// Chassis CSS's floating components (`.menu`, `.tooltip`, `.popover`) read a `data-cx-placement`
|
|
5
|
+
// attribute in this hyphenated, compound form (`"bottom-end"`, `"top-start"`, ...) to pick the
|
|
6
|
+
// right transform-origin for the open/close animation. React Aria's `useOverlayPosition` speaks a
|
|
7
|
+
// different, space-separated placement vocabulary (`"bottom end"`) and only reports back the
|
|
8
|
+
// resolved main axis after flipping, not the full compound value — this module bridges the two.
|
|
9
|
+
export type Placement =
|
|
10
|
+
| 'top'
|
|
11
|
+
| 'top-start'
|
|
12
|
+
| 'top-end'
|
|
13
|
+
| 'bottom'
|
|
14
|
+
| 'bottom-start'
|
|
15
|
+
| 'bottom-end'
|
|
16
|
+
| 'left'
|
|
17
|
+
| 'left-start'
|
|
18
|
+
| 'left-end'
|
|
19
|
+
| 'right'
|
|
20
|
+
| 'right-start'
|
|
21
|
+
| 'right-end'
|
|
22
|
+
|
|
23
|
+
const ARIA_PLACEMENT: Record<Placement, AriaPlacement> = {
|
|
24
|
+
top: 'top',
|
|
25
|
+
'top-start': 'top start',
|
|
26
|
+
'top-end': 'top end',
|
|
27
|
+
bottom: 'bottom',
|
|
28
|
+
'bottom-start': 'bottom start',
|
|
29
|
+
'bottom-end': 'bottom end',
|
|
30
|
+
left: 'left',
|
|
31
|
+
'left-start': 'left top',
|
|
32
|
+
'left-end': 'left bottom',
|
|
33
|
+
right: 'right',
|
|
34
|
+
'right-start': 'right top',
|
|
35
|
+
'right-end': 'right bottom'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Translates chassis-react's public `placement` prop values into the strings
|
|
39
|
+
// `useOverlayPosition` expects.
|
|
40
|
+
export const toAriaPlacement = (placement: Placement): AriaPlacement => ARIA_PLACEMENT[placement]
|
|
41
|
+
|
|
42
|
+
// `useOverlayPosition`'s own `shouldFlip` only ever swaps the main axis (top<->bottom or
|
|
43
|
+
// left<->right) — it never changes the requested cross-axis alignment — so the resolved compound
|
|
44
|
+
// value is always the requested cross-axis suffix reattached to the (possibly flipped) main axis
|
|
45
|
+
// `useOverlayPosition` reports back.
|
|
46
|
+
export const resolveDataPlacement = (
|
|
47
|
+
requested: Placement,
|
|
48
|
+
resolvedMain: PlacementAxis | null
|
|
49
|
+
): string => {
|
|
50
|
+
if (!resolvedMain || resolvedMain === 'center') return requested
|
|
51
|
+
const cross = requested.split('-')[1]
|
|
52
|
+
return cross ? `${resolvedMain}-${cross}` : resolvedMain
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Shared between `Menu`/`MenuSubmenu` and `Combobox`/`Autocomplete`, whose floating panels all
|
|
56
|
+
// position via `useOverlayPosition` the same way: only `position`/`top`/`left` are taken from the
|
|
57
|
+
// hook's computed style (`zIndex`/`maxHeight` stay owned by chassis-css's own `--zindex`/
|
|
58
|
+
// `--max-height` tokens, see `_menu.scss`), and `data-cx-placement` needs the resolved main axis
|
|
59
|
+
// reattached to the originally-requested cross-axis alignment. Lives here (rather than
|
|
60
|
+
// `components/menu/`) for the same reason `renderMenuItemContent` does — `Combobox.tsx`/
|
|
61
|
+
// `Autocomplete.tsx` need it too, and importing across that folder boundary from three call sites
|
|
62
|
+
// is worse than housing it where none of the four has to reach into a sibling's folder.
|
|
63
|
+
export interface MenuOverlayPositioning {
|
|
64
|
+
menuStyle: CSSProperties
|
|
65
|
+
placementAttr: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const resolveMenuOverlayPositioning = (
|
|
69
|
+
overlayStyle: CSSProperties | undefined,
|
|
70
|
+
requestedPlacement: Placement,
|
|
71
|
+
resolvedPlacement: PlacementAxis | null
|
|
72
|
+
): MenuOverlayPositioning => ({
|
|
73
|
+
menuStyle: {
|
|
74
|
+
position: overlayStyle?.position,
|
|
75
|
+
top: overlayStyle?.top,
|
|
76
|
+
left: overlayStyle?.left
|
|
77
|
+
},
|
|
78
|
+
placementAttr: resolveDataPlacement(requestedPlacement, resolvedPlacement)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// chassis-css's `.combobox + .menu` sibling-selector rule (`_combobox.scss`) sets
|
|
82
|
+
// `--menu-max-height`/`--menu-overflow-y` from `$combobox-menu-max-height`/
|
|
83
|
+
// `$combobox-menu-overflow-y` (320px/auto by default) — but only while the panel is an adjacent
|
|
84
|
+
// DOM sibling of `.combobox`. `Autocomplete`/`Combobox` portal their panel to `document.body` (or
|
|
85
|
+
// an enclosing open `<dialog>`, via `useFloatingOverlay`), breaking that adjacency, so both inline
|
|
86
|
+
// this as a fallback matching the shipped Sass defaults instead of the generic, uncapped `.menu`
|
|
87
|
+
// fallback (`max-height: none`). A consumer who customized either Sass variable needs to
|
|
88
|
+
// re-override `--menu-max-height`/`--menu-overflow-y` on `.menu` directly, since the
|
|
89
|
+
// sibling-selector auto-application can no longer reach a portaled panel.
|
|
90
|
+
export const COMBOBOX_MENU_OVERLAY_STYLE: CSSProperties = {
|
|
91
|
+
'--menu-max-height': '320px',
|
|
92
|
+
'--menu-overflow-y': 'auto'
|
|
93
|
+
} as CSSProperties
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Tracks, per-element, whether its most recent activation came from a real pointer click rather
|
|
2
|
+
// than a keyboard one (a `click` fired by pointer activation has `detail >= 1`; one fired by
|
|
3
|
+
// keyboard activation, e.g. Enter/Space on a focused button, has `detail === 0`) — for callers
|
|
4
|
+
// that need that information later, asynchronously, once React has committed the state update the
|
|
5
|
+
// click triggered (see `Carousel.tsx`'s disabled-end focus redirect, which needs it to decide
|
|
6
|
+
// whether `focusRedirect` should suppress Safari's script-focus-after-pointer-click ring). A
|
|
7
|
+
// `WeakSet` rather than a DOM attribute so it doesn't leak into markup/snapshots, and consuming it
|
|
8
|
+
// clears the entry so a later, unrelated keyboard activation on the same element isn't
|
|
9
|
+
// misattributed as pointer-triggered.
|
|
10
|
+
const pointerClicks = new WeakSet<HTMLElement>()
|
|
11
|
+
|
|
12
|
+
export function markPointerClick(el: HTMLElement): void {
|
|
13
|
+
pointerClicks.add(el)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function consumePointerClick(el: HTMLElement | null): boolean {
|
|
17
|
+
if (!el) return false
|
|
18
|
+
const wasPointer = pointerClicks.has(el)
|
|
19
|
+
pointerClicks.delete(el)
|
|
20
|
+
return wasPointer
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComponentPropsWithoutRef,
|
|
3
|
+
ComponentPropsWithRef,
|
|
4
|
+
ElementType,
|
|
5
|
+
ForwardRefRenderFunction,
|
|
6
|
+
forwardRef
|
|
7
|
+
} from 'react'
|
|
8
|
+
|
|
9
|
+
// Ref type inferred from a polymorphic component's currently-selected `component` element type.
|
|
10
|
+
export type PolymorphicRef<C extends ElementType> = ComponentPropsWithRef<C>['ref']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Props for a polymorphic component: `OwnProps` (which must declare `component?: C`) plus
|
|
14
|
+
* whatever props `C` itself accepts, minus any name already claimed by `OwnProps` so the two
|
|
15
|
+
* don't conflict. Lets consumers swap `component` for e.g. a framework's `Image` and get full
|
|
16
|
+
* type-checking/autocomplete for that component's own props at the call site.
|
|
17
|
+
*/
|
|
18
|
+
export type PolymorphicComponentProps<C extends ElementType, OwnProps extends object> = OwnProps &
|
|
19
|
+
Omit<ComponentPropsWithoutRef<C>, keyof OwnProps>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wraps a polymorphic component's render function in `forwardRef` and stamps `displayName` — the
|
|
23
|
+
* two-step wiring every polymorphic component in this library repeats identically. `T` is the
|
|
24
|
+
* fully generic component type (e.g. `ButtonComponent`) that `forwardRef`'s own return type can't
|
|
25
|
+
* express, since `forwardRef` only accepts a non-generic render function — the caller still casts
|
|
26
|
+
* the render function to a concrete `Element`-typed instance before passing it in here.
|
|
27
|
+
*/
|
|
28
|
+
export function createPolymorphicComponent<T>(
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
render: ForwardRefRenderFunction<Element, any>,
|
|
31
|
+
displayName: string
|
|
32
|
+
): T {
|
|
33
|
+
const Component = forwardRef(render) as unknown as T & { displayName?: string }
|
|
34
|
+
Component.displayName = displayName
|
|
35
|
+
return Component
|
|
36
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Icon } from '../components/icon/Icon'
|
|
4
|
+
|
|
5
|
+
export interface MenuItemContentDef {
|
|
6
|
+
/**
|
|
7
|
+
* Icon rendered at the item's leading edge (`.menu-item-icon`).
|
|
8
|
+
*/
|
|
9
|
+
icon?: ReactNode
|
|
10
|
+
/**
|
|
11
|
+
* Label content.
|
|
12
|
+
*/
|
|
13
|
+
label: ReactNode
|
|
14
|
+
/**
|
|
15
|
+
* Secondary line of text rendered below the label (`.menu-item-description`).
|
|
16
|
+
*/
|
|
17
|
+
description?: ReactNode
|
|
18
|
+
/**
|
|
19
|
+
* Renders a `.menu-item-check` at the trailing edge when true.
|
|
20
|
+
*/
|
|
21
|
+
selected?: boolean
|
|
22
|
+
/**
|
|
23
|
+
* Icon name for the selected-state check. Defaults to chassis-css's own doc example.
|
|
24
|
+
*/
|
|
25
|
+
checkIcon?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Chassis-css defines identical markup for rich item content in both Menu and Combobox items
|
|
29
|
+
// (and, by extension, Autocomplete) — `.menu-item-icon` / `.menu-item-content` (label +
|
|
30
|
+
// `.menu-item-description`) / `.menu-item-check`. Shared here so every consumer produces
|
|
31
|
+
// byte-identical DOM instead of three near-duplicate implementations. Lives in `utils/` rather
|
|
32
|
+
// than `components/menu/` because it crosses that folder boundary — Combobox.tsx and
|
|
33
|
+
// Autocomplete.tsx both import it too. Renders bare `label` when there's nothing else to show,
|
|
34
|
+
// keeping simple items' markup as minimal as chassis-css's own simplest example.
|
|
35
|
+
export const renderMenuItemContent = ({
|
|
36
|
+
icon,
|
|
37
|
+
label,
|
|
38
|
+
description,
|
|
39
|
+
selected,
|
|
40
|
+
checkIcon = 'check-solid'
|
|
41
|
+
}: MenuItemContentDef): ReactNode => {
|
|
42
|
+
if (icon == null && description == null && !selected) return label
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
{icon != null && (
|
|
47
|
+
<span className="menu-item-icon" aria-hidden="true">
|
|
48
|
+
{icon}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
{description != null ? (
|
|
52
|
+
<span className="menu-item-content">
|
|
53
|
+
<span>{label}</span>
|
|
54
|
+
<small className="menu-item-description">{description}</small>
|
|
55
|
+
</span>
|
|
56
|
+
) : (
|
|
57
|
+
label
|
|
58
|
+
)}
|
|
59
|
+
{selected && <Icon name={checkIcon} className="menu-item-check" />}
|
|
60
|
+
</>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { flushSync } from 'react-dom'
|
|
2
|
+
import { CalendarState, RangeCalendarState } from 'react-stately'
|
|
3
|
+
import { CalendarDate } from '@internationalized/date'
|
|
4
|
+
|
|
5
|
+
// Moves a (possibly multi-month) calendar's visible range so it starts at `desiredStart` — used by
|
|
6
|
+
// `CalendarMonthYearPicker` to jump straight to a picked month/year, in any visible block.
|
|
7
|
+
//
|
|
8
|
+
// `state.setFocusedDate` is the only way to move `visibleRange` from outside react-stately, but it
|
|
9
|
+
// doesn't just set `visibleRange.start` to wherever focus goes: react-stately keeps the visible
|
|
10
|
+
// range as separate internal state, only repositioning it when the newly focused date falls
|
|
11
|
+
// entirely *before* the current range (realigning so the range *ends* at that date's month) or
|
|
12
|
+
// entirely *after* it (realigning so it *starts* there) — a focused date that lands inside the
|
|
13
|
+
// current range moves focus without paging the range at all. That means a single `setFocusedDate`
|
|
14
|
+
// call can only land exactly on an arbitrary `desiredStart` when it's at least a full
|
|
15
|
+
// `visibleDuration` away from the current range. Anything closer — the common case here, since
|
|
16
|
+
// adjacent blocks in a multi-month calendar are only one month apart — either no-ops (desired
|
|
17
|
+
// start still inside the current range) or overshoots by up to a full `visibleDuration` (desired
|
|
18
|
+
// start just outside it, but not far enough for the realignment to land exactly).
|
|
19
|
+
//
|
|
20
|
+
// Worked around with two `setFocusedDate` hops when a single one can't reach exactly: first past
|
|
21
|
+
// `desiredStart` by one extra `visibleDuration` in the same direction (always far enough to land
|
|
22
|
+
// exactly, by the same rule), flushed synchronously so `visibleRange` actually updates before the
|
|
23
|
+
// second call runs, then back onto `desiredStart` itself — which, relative to that intermediate
|
|
24
|
+
// range, is now exactly one `visibleDuration` away and so lands exactly too.
|
|
25
|
+
export const setVisibleRangeStart = (
|
|
26
|
+
state: CalendarState<'single' | 'multiple'> | RangeCalendarState,
|
|
27
|
+
desiredStart: CalendarDate
|
|
28
|
+
): void => {
|
|
29
|
+
const { start: currentStart, end: currentEnd } = state.visibleRange
|
|
30
|
+
if (desiredStart.compare(currentStart) === 0) return
|
|
31
|
+
|
|
32
|
+
const durationMonths = state.visibleDuration.months || 1
|
|
33
|
+
const isForward = desiredStart.compare(currentStart) > 0
|
|
34
|
+
|
|
35
|
+
const reachableInOneHop = isForward
|
|
36
|
+
? desiredStart.compare(currentEnd) > 0
|
|
37
|
+
: desiredStart.add({ months: durationMonths - 1 }).compare(currentStart) < 0
|
|
38
|
+
|
|
39
|
+
if (reachableInOneHop) {
|
|
40
|
+
state.setFocusedDate(focusedDateForStart(desiredStart, isForward, durationMonths))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const overshootStart = isForward
|
|
45
|
+
? desiredStart.add({ months: durationMonths })
|
|
46
|
+
: desiredStart.subtract({ months: durationMonths })
|
|
47
|
+
|
|
48
|
+
flushSync(() => {
|
|
49
|
+
state.setFocusedDate(focusedDateForStart(overshootStart, isForward, durationMonths))
|
|
50
|
+
})
|
|
51
|
+
// Relative to the now-updated range, `desiredStart` sits exactly one `visibleDuration` back the
|
|
52
|
+
// way we came — always far enough for a single hop to land exactly.
|
|
53
|
+
state.setFocusedDate(focusedDateForStart(desiredStart, !isForward, durationMonths))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// react-stately's forward-paging realignment anchors the new range to whatever month
|
|
57
|
+
// `focusedDate` itself falls in; its backward-paging one anchors it `visibleDuration - 1` months
|
|
58
|
+
// *before* `focusedDate`'s month instead — so landing on a specific desired start takes a
|
|
59
|
+
// different `focusedDate` depending on which of the two is going to fire.
|
|
60
|
+
const focusedDateForStart = (
|
|
61
|
+
desiredStart: CalendarDate,
|
|
62
|
+
isForward: boolean,
|
|
63
|
+
durationMonths: number
|
|
64
|
+
): CalendarDate => (isForward ? desiredStart : desiredStart.add({ months: durationMonths - 1 }))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Spacing } from '../types'
|
|
2
|
+
|
|
3
|
+
// A spacing prop (`gap`/`rowGap`/`gutter`/...) is typed `Spacing | 0`, but `0` is meaningful
|
|
4
|
+
// (chassis-css's `gap-0` etc. exist) while `undefined`/`false`-ish values mean "not set" — a
|
|
5
|
+
// plain truthy check would drop `0`, so every caller needs this same `typeof` guard instead.
|
|
6
|
+
// Shared by `Flex`, `Stack`, `Row`, and `CardBody`'s own `layoutClassNames`/`_className` builders,
|
|
7
|
+
// which otherwise duplicated it once per spacing prop (`gap`, `rowGap`, `columnGap`, `gutter`,
|
|
8
|
+
// `gutterX`, `gutterY`, ...).
|
|
9
|
+
export const spacingClassName = (
|
|
10
|
+
base: string,
|
|
11
|
+
value: Spacing | 0 | undefined,
|
|
12
|
+
prefix = ''
|
|
13
|
+
): string | null =>
|
|
14
|
+
typeof value === 'string' || typeof value === 'number' ? `${prefix}${base}-${value}` : null
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Chromium/WebKit's `:focus-visible` heuristic has no interaction history to consult on the very
|
|
2
|
+
// first focus event of a page load, so a genuine pointer press can still paint a focus ring as if
|
|
3
|
+
// it came from the keyboard — most visible on whatever element happens to be the first thing a
|
|
4
|
+
// visitor clicks. A single capture-phase `pointerdown` listener on `document`, installed once (see
|
|
5
|
+
// `install` below, called for its side effect from `index.ts`), preemptively suppresses the
|
|
6
|
+
// outline on whatever focusable element the press is about to focus — before the browser's own
|
|
7
|
+
// default action grants focus and paints the ring for it — then restores it on blur. Safe to run
|
|
8
|
+
// on every press, not just the page's first: if the browser was already going to correctly
|
|
9
|
+
// withhold the ring, this suppress/restore cycle is a no-op within the same frame.
|
|
10
|
+
//
|
|
11
|
+
// Mirrors react-aria's own `@react-aria/interactions` global modality tracking (one document-level
|
|
12
|
+
// listener rather than one per component/instance) without switching this library's focus rings
|
|
13
|
+
// from chassis-css's native `:focus-visible` CSS to JS-driven state — react-aria's `isFocusVisible`
|
|
14
|
+
// is consumed by applying a class/attribute, which would fork focus-ring styling from the vanilla,
|
|
15
|
+
// non-React product chassis-css also ships. Deliberately unscoped to this library's own markup, for
|
|
16
|
+
// the same reason react-aria's own fix is unscoped: the browser bug isn't specific to any one
|
|
17
|
+
// component library, and a page generally wants one consistent focus-ring policy for every
|
|
18
|
+
// focusable element on it, not just the subset a component library happens to own.
|
|
19
|
+
const FOCUSABLE_SELECTOR = 'a[href], button, input, select, textarea, [tabindex]'
|
|
20
|
+
|
|
21
|
+
const suppressed = new WeakSet<HTMLElement>()
|
|
22
|
+
|
|
23
|
+
// Shared by the global `pointerdown` listener below and by call sites that move focus themselves
|
|
24
|
+
// via script (e.g. `RangeCalendar`'s hover-to-preview-a-range, which calls `element.focus()` from
|
|
25
|
+
// a `pointerenter` handler — outside the `pointerdown` this module otherwise listens for).
|
|
26
|
+
export function suppressFocusRing(el: HTMLElement): void {
|
|
27
|
+
// chassis-css's `.form-input` placeholder deliberately grants its focus ring on `:focus-within`
|
|
28
|
+
// as well as `:focus-visible` (see `_form.scss`), unlike the `:focus-visible`-only policy this
|
|
29
|
+
// module otherwise mirrors for buttons/links/checks/etc. — a form field is meant to show its
|
|
30
|
+
// ring regardless of whether the focus came from a click or the keyboard, so the very
|
|
31
|
+
// first-press-of-page-load quirk this module works around isn't a bug for `.form-input` in the
|
|
32
|
+
// first place: forcing its outline off here would fight, then correctly-but-uselessly restore,
|
|
33
|
+
// an outline chassis-css wants visible the whole time the field is focused.
|
|
34
|
+
if (el.classList.contains('form-input')) return
|
|
35
|
+
// `suppressed` guards re-entrancy: a second call for the same element before it blurs (e.g. a
|
|
36
|
+
// rapid re-press, or the pointer re-entering a cell) would otherwise stack a second blur
|
|
37
|
+
// listener that clobbers the first restoration and leaves the ring permanently suppressed.
|
|
38
|
+
if (suppressed.has(el)) return
|
|
39
|
+
|
|
40
|
+
suppressed.add(el)
|
|
41
|
+
const prevOutline = el.style.outline
|
|
42
|
+
el.style.outline = 'none'
|
|
43
|
+
el.addEventListener(
|
|
44
|
+
'blur',
|
|
45
|
+
() => {
|
|
46
|
+
el.style.outline = prevOutline
|
|
47
|
+
suppressed.delete(el)
|
|
48
|
+
},
|
|
49
|
+
{ once: true }
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handlePointerDown(event: PointerEvent) {
|
|
54
|
+
const target = event.target
|
|
55
|
+
if (!(target instanceof Element)) return
|
|
56
|
+
const el = target.closest<HTMLElement>(FOCUSABLE_SELECTOR)
|
|
57
|
+
if (!el) return
|
|
58
|
+
suppressFocusRing(el)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let installed = false
|
|
62
|
+
|
|
63
|
+
export function install(): void {
|
|
64
|
+
if (installed || typeof document === 'undefined') return
|
|
65
|
+
installed = true
|
|
66
|
+
document.addEventListener('pointerdown', handlePointerDown, true)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
install()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Chassis-css's validation-state classes, shared by every form control that supports
|
|
2
|
+
// `invalid`/`valid` (Radio, Switch, RangeInput, Select, TextInput, Textarea). Returns a
|
|
3
|
+
// `classNames`-compatible object rather than a resolved string so callers can still combine it
|
|
4
|
+
// with their own base/size classes and `className` in one `classNames(...)` call.
|
|
5
|
+
export const validationClassName = (invalid?: boolean, valid?: boolean) => ({
|
|
6
|
+
'is-invalid': invalid,
|
|
7
|
+
'is-valid': valid
|
|
8
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CSSProperties } from 'react'
|
|
2
|
+
|
|
3
|
+
// Options in a virtually-focused listbox (react-aria's combobox/listbox patterns) never receive
|
|
4
|
+
// real DOM focus — focus stays on the trigger (a text input or button) while the highlighted
|
|
5
|
+
// option is tracked virtually, so `:hover`/`:focus-visible` can't style it. To indicate the
|
|
6
|
+
// highlight without touching chassis-css, we reuse its own hover color tokens (already defined
|
|
7
|
+
// on the ancestor `.menu`, prefixed `--cx-` by chassis-css's build) as inline overrides instead
|
|
8
|
+
// of relying on `.active`, which chassis-css reserves for the pressed/`:active` look.
|
|
9
|
+
export const virtualFocusStyle: CSSProperties = {
|
|
10
|
+
'--cx-icon-color': 'var(--cx-item-hover-icon-color)',
|
|
11
|
+
'--cx-item-fg-color': 'var(--cx-item-hover-fg-color)',
|
|
12
|
+
'--cx-item-bg-color': 'var(--cx-item-hover-bg-color)'
|
|
13
|
+
} as CSSProperties
|
|
14
|
+
|
|
15
|
+
export const getVirtualFocusStyle = (isFocused: boolean): CSSProperties | undefined =>
|
|
16
|
+
isFocused ? virtualFocusStyle : undefined
|