@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,47 @@
|
|
|
1
|
+
import React, { ElementType, ForwardRefRenderFunction, ReactElement } from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createPolymorphicComponent,
|
|
6
|
+
PolymorphicComponentProps,
|
|
7
|
+
PolymorphicRef
|
|
8
|
+
} from '../../utils/polymorphic'
|
|
9
|
+
|
|
10
|
+
type NotificationTextOwnProps<C extends ElementType> = {
|
|
11
|
+
/**
|
|
12
|
+
* A string of all className you want applied to the component.
|
|
13
|
+
*/
|
|
14
|
+
className?: string
|
|
15
|
+
/**
|
|
16
|
+
* Component used for the root node. Either a string to use a HTML element or a component.
|
|
17
|
+
*/
|
|
18
|
+
component?: C
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type NotificationTextProps<C extends ElementType = 'div'> = PolymorphicComponentProps<
|
|
22
|
+
C,
|
|
23
|
+
NotificationTextOwnProps<C>
|
|
24
|
+
>
|
|
25
|
+
|
|
26
|
+
type NotificationTextComponent = (<C extends ElementType = 'div'>(
|
|
27
|
+
props: NotificationTextProps<C> & { ref?: PolymorphicRef<C> }
|
|
28
|
+
) => ReactElement | null) & { displayName?: string }
|
|
29
|
+
|
|
30
|
+
function NotificationTextRender<C extends ElementType = 'div'>(
|
|
31
|
+
{ children, component, className, ...rest }: NotificationTextProps<C>,
|
|
32
|
+
ref: PolymorphicRef<C>
|
|
33
|
+
) {
|
|
34
|
+
const Component = component ?? 'div'
|
|
35
|
+
const _className = classNames('notification-text', className)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Component className={_className} {...rest} ref={ref}>
|
|
39
|
+
{children}
|
|
40
|
+
</Component>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const NotificationText = createPolymorphicComponent<NotificationTextComponent>(
|
|
45
|
+
NotificationTextRender as ForwardRefRenderFunction<Element, NotificationTextProps<ElementType>>,
|
|
46
|
+
'NotificationText'
|
|
47
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { ElementType, ForwardRefRenderFunction, ReactElement } from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createPolymorphicComponent,
|
|
6
|
+
PolymorphicComponentProps,
|
|
7
|
+
PolymorphicRef
|
|
8
|
+
} from '../../utils/polymorphic'
|
|
9
|
+
|
|
10
|
+
type NotificationTitleOwnProps<C extends ElementType> = {
|
|
11
|
+
/**
|
|
12
|
+
* A string of all className you want applied to the base component.
|
|
13
|
+
*/
|
|
14
|
+
className?: string
|
|
15
|
+
/**
|
|
16
|
+
* Component used for the root node. Either a string to use a HTML element or a component.
|
|
17
|
+
*/
|
|
18
|
+
component?: C
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type NotificationTitleProps<C extends ElementType = 'h4'> = PolymorphicComponentProps<
|
|
22
|
+
C,
|
|
23
|
+
NotificationTitleOwnProps<C>
|
|
24
|
+
>
|
|
25
|
+
|
|
26
|
+
type NotificationTitleComponent = (<C extends ElementType = 'h4'>(
|
|
27
|
+
props: NotificationTitleProps<C> & { ref?: PolymorphicRef<C> }
|
|
28
|
+
) => ReactElement | null) & { displayName?: string }
|
|
29
|
+
|
|
30
|
+
function NotificationTitleRender<C extends ElementType = 'h4'>(
|
|
31
|
+
{ children, className, component, ...rest }: NotificationTitleProps<C>,
|
|
32
|
+
ref: PolymorphicRef<C>
|
|
33
|
+
) {
|
|
34
|
+
const Component = component ?? 'h4'
|
|
35
|
+
const _className = classNames('notification-title', className)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Component className={_className} {...rest} ref={ref}>
|
|
39
|
+
{children}
|
|
40
|
+
</Component>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const NotificationTitle = createPolymorphicComponent<NotificationTitleComponent>(
|
|
45
|
+
NotificationTitleRender as ForwardRefRenderFunction<Element, NotificationTitleProps<ElementType>>,
|
|
46
|
+
'NotificationTitle'
|
|
47
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface NotificationContextProps {
|
|
4
|
+
/**
|
|
5
|
+
* Whether the notification is currently visible.
|
|
6
|
+
*/
|
|
7
|
+
visible?: boolean
|
|
8
|
+
/**
|
|
9
|
+
* Dismisses the notification. Wire this to any element's `onClick` — see `useNotification`.
|
|
10
|
+
*/
|
|
11
|
+
close: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NotificationContext = createContext<NotificationContextProps>({ close: () => {} })
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Notification } from './Notification'
|
|
2
|
+
export type { NotificationProps } from './Notification'
|
|
3
|
+
export { NotificationTitle } from './NotificationTitle'
|
|
4
|
+
export type { NotificationTitleProps } from './NotificationTitle'
|
|
5
|
+
export { NotificationIcon } from './NotificationIcon'
|
|
6
|
+
export type { NotificationIconProps } from './NotificationIcon'
|
|
7
|
+
export { NotificationText } from './NotificationText'
|
|
8
|
+
export type { NotificationTextProps } from './NotificationText'
|
|
9
|
+
|
|
10
|
+
// NotificationStack is an independent manager/container component (react-hot-toast-style), not
|
|
11
|
+
// a Notification sub-part — it subscribes to the shared notificationQueue and renders
|
|
12
|
+
// Notification instances from it, rather than being composed as a child of Notification. Stays
|
|
13
|
+
// a separate top-level export, mirroring Toast/Toaster.
|
|
14
|
+
export { NotificationStack } from './NotificationStack'
|
|
15
|
+
export type { NotificationStackProps } from './NotificationStack'
|
|
16
|
+
export { addNotification, closeNotification, notificationQueue } from './notificationQueue'
|
|
17
|
+
export type { NotificationContent } from './notificationQueue'
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
import { ToastQueue } from 'react-stately'
|
|
3
|
+
|
|
4
|
+
import { NotificationProps } from './Notification'
|
|
5
|
+
|
|
6
|
+
export interface NotificationContent extends Pick<
|
|
7
|
+
NotificationProps,
|
|
8
|
+
| 'actions'
|
|
9
|
+
| 'autohide'
|
|
10
|
+
| 'closeLabel'
|
|
11
|
+
| 'color'
|
|
12
|
+
| 'delay'
|
|
13
|
+
| 'dismissible'
|
|
14
|
+
| 'icon'
|
|
15
|
+
| 'role'
|
|
16
|
+
| 'solid'
|
|
17
|
+
| 'text'
|
|
18
|
+
| 'title'
|
|
19
|
+
| 'titleComponent'
|
|
20
|
+
> {
|
|
21
|
+
/**
|
|
22
|
+
* Content of the notification. Compose manually (typically a `NotificationIcon`/
|
|
23
|
+
* `NotificationTitle`/`NotificationText`), or leave empty and use the `icon`/`title`/`text`
|
|
24
|
+
* shorthand options instead.
|
|
25
|
+
*/
|
|
26
|
+
children?: ReactNode
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The queue backing `NotificationStack` — a module-level singleton so `addNotification()` is
|
|
30
|
+
// callable from anywhere (an event handler, an async callback) without threading a `push` prop
|
|
31
|
+
// through render. `NotificationStack` subscribes to it via `useToastQueue`; nothing renders
|
|
32
|
+
// until a `<NotificationStack />` is actually mounted somewhere to display the queue's contents.
|
|
33
|
+
export const notificationQueue = new ToastQueue<NotificationContent>()
|
|
34
|
+
|
|
35
|
+
// Adds a notification to the queue. Returns the notification's key, which can be passed to
|
|
36
|
+
// `closeNotification` to dismiss it programmatically. `children` is optional — omit it for a
|
|
37
|
+
// notification composed entirely from the `icon`/`title`/`text` shorthand options.
|
|
38
|
+
export function addNotification(
|
|
39
|
+
children?: ReactNode,
|
|
40
|
+
options: Omit<NotificationContent, 'children'> = {}
|
|
41
|
+
): string {
|
|
42
|
+
return notificationQueue.add({ children, ...options })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Removes a queued notification immediately, bypassing its own exit animation.
|
|
46
|
+
export function closeNotification(key: string): void {
|
|
47
|
+
notificationQueue.close(key)
|
|
48
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { ClipboardEvent, ForwardedRef, forwardRef, KeyboardEvent } from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import { mergeProps, useFocusRing, useObjectRef, useTextField } from 'react-aria'
|
|
4
|
+
|
|
5
|
+
import { validationClassName } from '../../utils/validationClassName'
|
|
6
|
+
|
|
7
|
+
interface OtpBoxProps {
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
index: number
|
|
10
|
+
invalid?: boolean
|
|
11
|
+
mask?: boolean
|
|
12
|
+
onChangeValue: (value: string) => void
|
|
13
|
+
onKeyDownBox: (event: KeyboardEvent<HTMLInputElement>) => void
|
|
14
|
+
onPasteBox: (event: ClipboardEvent<HTMLInputElement>) => void
|
|
15
|
+
size?: 'small' | 'large'
|
|
16
|
+
valid?: boolean
|
|
17
|
+
value: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const OtpBox = forwardRef(
|
|
21
|
+
(
|
|
22
|
+
{
|
|
23
|
+
disabled,
|
|
24
|
+
index,
|
|
25
|
+
invalid,
|
|
26
|
+
mask,
|
|
27
|
+
onChangeValue,
|
|
28
|
+
onKeyDownBox,
|
|
29
|
+
onPasteBox,
|
|
30
|
+
size,
|
|
31
|
+
valid,
|
|
32
|
+
value
|
|
33
|
+
}: OtpBoxProps,
|
|
34
|
+
forwardedRef: ForwardedRef<HTMLInputElement>
|
|
35
|
+
) => {
|
|
36
|
+
const ref = useObjectRef(forwardedRef)
|
|
37
|
+
const { focusProps, isFocusVisible } = useFocusRing()
|
|
38
|
+
const { inputProps } = useTextField(
|
|
39
|
+
{
|
|
40
|
+
'aria-label': `Digit ${index + 1}`,
|
|
41
|
+
autoComplete: index === 0 ? 'one-time-code' : 'off',
|
|
42
|
+
isDisabled: disabled,
|
|
43
|
+
isInvalid: invalid,
|
|
44
|
+
maxLength: 1,
|
|
45
|
+
onChange: onChangeValue,
|
|
46
|
+
onFocus: (event) => (event.target as HTMLInputElement).select(),
|
|
47
|
+
onKeyDown: onKeyDownBox,
|
|
48
|
+
type: mask ? 'password' : 'text',
|
|
49
|
+
value
|
|
50
|
+
},
|
|
51
|
+
ref
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<input
|
|
56
|
+
{...mergeProps(inputProps, focusProps)}
|
|
57
|
+
className={classNames('form-input', size, validationClassName(invalid, valid))}
|
|
58
|
+
data-focus-visible={isFocusVisible || undefined}
|
|
59
|
+
inputMode="numeric"
|
|
60
|
+
onPaste={onPasteBox}
|
|
61
|
+
pattern="\d*"
|
|
62
|
+
ref={ref}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
OtpBox.displayName = 'OtpBox'
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ClipboardEvent,
|
|
3
|
+
forwardRef,
|
|
4
|
+
Fragment,
|
|
5
|
+
HTMLAttributes,
|
|
6
|
+
KeyboardEvent,
|
|
7
|
+
ReactNode,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef
|
|
10
|
+
} from 'react'
|
|
11
|
+
import classNames from 'classnames'
|
|
12
|
+
|
|
13
|
+
import { useControllableState, useForkedRef, useFormField } from '../../hooks'
|
|
14
|
+
import { validationClassName } from '../../utils/validationClassName'
|
|
15
|
+
import { renderFormField } from '../form-field/renderFormField'
|
|
16
|
+
import { OtpBox } from './OtpBox'
|
|
17
|
+
|
|
18
|
+
export interface OtpInputProps extends Omit<
|
|
19
|
+
HTMLAttributes<HTMLDivElement>,
|
|
20
|
+
'onChange' | 'defaultValue'
|
|
21
|
+
> {
|
|
22
|
+
/**
|
|
23
|
+
* Identifies the element that describes the group, e.g. a `FormHelp` help element.
|
|
24
|
+
*/
|
|
25
|
+
'aria-describedby'?: string
|
|
26
|
+
/**
|
|
27
|
+
* An accessible label for the group, used when there's no visible `<label>`.
|
|
28
|
+
*/
|
|
29
|
+
'aria-label'?: string
|
|
30
|
+
/**
|
|
31
|
+
* Identifies a visible `<label>` element for the group.
|
|
32
|
+
*/
|
|
33
|
+
'aria-labelledby'?: string
|
|
34
|
+
/**
|
|
35
|
+
* A string of all className you want applied to the base component.
|
|
36
|
+
*/
|
|
37
|
+
className?: string
|
|
38
|
+
/**
|
|
39
|
+
* The initial code (uncontrolled), e.g. `"123"` for a partially filled code.
|
|
40
|
+
*/
|
|
41
|
+
defaultValue?: string
|
|
42
|
+
/**
|
|
43
|
+
* Prevents input and makes every box non-interactive.
|
|
44
|
+
*/
|
|
45
|
+
disabled?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* Splits the boxes into groups (e.g. `[3, 3]` for a "123-456" layout), rendering a
|
|
48
|
+
* `.form-otp-separator` between each group. The total number of boxes becomes the sum of
|
|
49
|
+
* `groupSizes`, overriding `length`.
|
|
50
|
+
*/
|
|
51
|
+
groupSizes?: number[]
|
|
52
|
+
/**
|
|
53
|
+
* A description for the field, rendered below the boxes.
|
|
54
|
+
*/
|
|
55
|
+
help?: ReactNode
|
|
56
|
+
/**
|
|
57
|
+
* `id` forwarded to the group container.
|
|
58
|
+
*/
|
|
59
|
+
id?: string
|
|
60
|
+
/**
|
|
61
|
+
* Visually connects the boxes into a single bordered control. When `groupSizes` is set, each
|
|
62
|
+
* group is connected separately.
|
|
63
|
+
*/
|
|
64
|
+
inputGroup?: boolean
|
|
65
|
+
/**
|
|
66
|
+
* Set component validation state to invalid.
|
|
67
|
+
*/
|
|
68
|
+
invalid?: boolean
|
|
69
|
+
/**
|
|
70
|
+
* An error message for the field, rendered below the boxes when `invalid` is set.
|
|
71
|
+
*/
|
|
72
|
+
invalidFeedback?: ReactNode
|
|
73
|
+
/**
|
|
74
|
+
* The field's caption, rendered as a `FormLabel` associated with the group via
|
|
75
|
+
* `aria-labelledby` (there's no single input to target with `htmlFor`).
|
|
76
|
+
*/
|
|
77
|
+
label?: ReactNode
|
|
78
|
+
/**
|
|
79
|
+
* Number of digit boxes. Ignored when `groupSizes` is set. Defaults to `6`.
|
|
80
|
+
*/
|
|
81
|
+
length?: number
|
|
82
|
+
/**
|
|
83
|
+
* Mask entered digits using `type="password"` boxes.
|
|
84
|
+
*/
|
|
85
|
+
mask?: boolean
|
|
86
|
+
/**
|
|
87
|
+
* `name` of an auto-created hidden input kept in sync with the code, for native form
|
|
88
|
+
* submission. Omit to skip creating it.
|
|
89
|
+
*/
|
|
90
|
+
name?: string
|
|
91
|
+
/**
|
|
92
|
+
* Callback fired whenever the code changes.
|
|
93
|
+
*/
|
|
94
|
+
onChange?: (value: string) => void
|
|
95
|
+
/**
|
|
96
|
+
* Callback fired once every box is filled.
|
|
97
|
+
*/
|
|
98
|
+
onComplete?: (value: string) => void
|
|
99
|
+
/**
|
|
100
|
+
* Content of the separator rendered between groups when `groupSizes` is set. Defaults to `–`.
|
|
101
|
+
*/
|
|
102
|
+
separator?: ReactNode
|
|
103
|
+
/**
|
|
104
|
+
* Size the boxes small or large.
|
|
105
|
+
*/
|
|
106
|
+
size?: 'small' | 'large'
|
|
107
|
+
/**
|
|
108
|
+
* Set component validation state to valid.
|
|
109
|
+
*/
|
|
110
|
+
valid?: boolean
|
|
111
|
+
/**
|
|
112
|
+
* A success message for the field, rendered below the boxes when `valid` is set.
|
|
113
|
+
*/
|
|
114
|
+
validFeedback?: ReactNode
|
|
115
|
+
/**
|
|
116
|
+
* The code (controlled), digits only, e.g. `"123456"`.
|
|
117
|
+
*/
|
|
118
|
+
value?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const onlyDigits = (raw: string) => raw.replace(/\D/g, '')
|
|
122
|
+
|
|
123
|
+
const toBoxes = (raw: string, total: number): string[] => {
|
|
124
|
+
const digits = [...onlyDigits(raw)].slice(0, total)
|
|
125
|
+
return Array.from({ length: total }, (_, i) => digits[i] ?? '')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const OtpInput = forwardRef<HTMLDivElement, OtpInputProps>(
|
|
129
|
+
(
|
|
130
|
+
{
|
|
131
|
+
className,
|
|
132
|
+
defaultValue,
|
|
133
|
+
disabled,
|
|
134
|
+
groupSizes,
|
|
135
|
+
help,
|
|
136
|
+
id,
|
|
137
|
+
inputGroup,
|
|
138
|
+
invalid,
|
|
139
|
+
invalidFeedback,
|
|
140
|
+
label,
|
|
141
|
+
length = 6,
|
|
142
|
+
mask,
|
|
143
|
+
name,
|
|
144
|
+
onChange,
|
|
145
|
+
onComplete,
|
|
146
|
+
separator = '–',
|
|
147
|
+
size,
|
|
148
|
+
valid,
|
|
149
|
+
validFeedback,
|
|
150
|
+
value,
|
|
151
|
+
...rest
|
|
152
|
+
}: OtpInputProps,
|
|
153
|
+
ref
|
|
154
|
+
): ReactNode => {
|
|
155
|
+
const total =
|
|
156
|
+
groupSizes && groupSizes.length > 0 ? groupSizes.reduce((a, b) => a + b, 0) : length
|
|
157
|
+
|
|
158
|
+
const [rawValue, setRawValue] = useControllableState(value, defaultValue ?? '', onChange)
|
|
159
|
+
const boxes = useMemo(() => toBoxes(rawValue, total), [rawValue, total])
|
|
160
|
+
|
|
161
|
+
const boxRefs = useRef<Array<HTMLInputElement | null>>([])
|
|
162
|
+
const focusBox = (index: number) => boxRefs.current[index]?.focus()
|
|
163
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
164
|
+
const forkedRef = useForkedRef(ref, containerRef)
|
|
165
|
+
|
|
166
|
+
const commit = (nextBoxes: string[]) => {
|
|
167
|
+
const next = nextBoxes.join('')
|
|
168
|
+
setRawValue(next)
|
|
169
|
+
if (nextBoxes.length === total && nextBoxes.every((box) => box !== '')) {
|
|
170
|
+
onComplete?.(next)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handleChange = (index: number, raw: string) => {
|
|
175
|
+
const digits = onlyDigits(raw)
|
|
176
|
+
|
|
177
|
+
if (digits.length > 1) {
|
|
178
|
+
// Multi-character value landing in a single box (autofill, or a fast typist outrunning
|
|
179
|
+
// the auto-advance) — distribute across this and subsequent boxes, matching chassis-css's
|
|
180
|
+
// vanilla otp-input.js `_handleInput`.
|
|
181
|
+
const chars = [...digits]
|
|
182
|
+
const nextBoxes = [...boxes]
|
|
183
|
+
for (let i = 0; i < chars.length && index + i < total; i++) {
|
|
184
|
+
nextBoxes[index + i] = chars[i]!
|
|
185
|
+
}
|
|
186
|
+
commit(nextBoxes)
|
|
187
|
+
focusBox(Math.min(index + chars.length, total - 1))
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const nextBoxes = [...boxes]
|
|
192
|
+
nextBoxes[index] = digits
|
|
193
|
+
commit(nextBoxes)
|
|
194
|
+
if (digits && index < total - 1) focusBox(index + 1)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const handleKeyDown = (index: number, event: KeyboardEvent<HTMLInputElement>) => {
|
|
198
|
+
if (disabled) return
|
|
199
|
+
|
|
200
|
+
switch (event.key) {
|
|
201
|
+
case 'Backspace': {
|
|
202
|
+
if (!boxes[index] && index > 0) {
|
|
203
|
+
event.preventDefault()
|
|
204
|
+
const nextBoxes = [...boxes]
|
|
205
|
+
nextBoxes[index - 1] = ''
|
|
206
|
+
commit(nextBoxes)
|
|
207
|
+
focusBox(index - 1)
|
|
208
|
+
}
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
case 'Delete': {
|
|
212
|
+
event.preventDefault()
|
|
213
|
+
// `boxes` (and every copy derived from it) is always exactly `total` entries long — see
|
|
214
|
+
// `toBoxes` — so `i + 1` (bounded by `i < total - 1`) never reads past the end.
|
|
215
|
+
const nextBoxes = [...boxes]
|
|
216
|
+
for (let i = index; i < total - 1; i++) nextBoxes[i] = nextBoxes[i + 1]!
|
|
217
|
+
nextBoxes[total - 1] = ''
|
|
218
|
+
commit(nextBoxes)
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
case 'ArrowLeft': {
|
|
222
|
+
if (index > 0) {
|
|
223
|
+
event.preventDefault()
|
|
224
|
+
focusBox(index - 1)
|
|
225
|
+
}
|
|
226
|
+
break
|
|
227
|
+
}
|
|
228
|
+
case 'ArrowRight': {
|
|
229
|
+
if (index < total - 1) {
|
|
230
|
+
event.preventDefault()
|
|
231
|
+
focusBox(index + 1)
|
|
232
|
+
}
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
default:
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {
|
|
241
|
+
event.preventDefault()
|
|
242
|
+
if (disabled) return
|
|
243
|
+
const digits = onlyDigits(event.clipboardData.getData('text')).slice(0, total)
|
|
244
|
+
if (!digits) return
|
|
245
|
+
commit(toBoxes(digits, total))
|
|
246
|
+
focusBox(Math.min(digits.length, total) - 1)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const renderBoxes = (start: number, count: number) =>
|
|
250
|
+
Array.from({ length: count }, (_, i) => start + i).map((i) => (
|
|
251
|
+
<OtpBox
|
|
252
|
+
disabled={disabled}
|
|
253
|
+
index={i}
|
|
254
|
+
invalid={invalid}
|
|
255
|
+
key={i}
|
|
256
|
+
mask={mask}
|
|
257
|
+
onChangeValue={(next) => handleChange(i, next)}
|
|
258
|
+
onKeyDownBox={(event) => handleKeyDown(i, event)}
|
|
259
|
+
onPasteBox={handlePaste}
|
|
260
|
+
ref={(el) => {
|
|
261
|
+
boxRefs.current[i] = el
|
|
262
|
+
}}
|
|
263
|
+
size={size}
|
|
264
|
+
valid={valid}
|
|
265
|
+
value={boxes[i] ?? ''}
|
|
266
|
+
/>
|
|
267
|
+
))
|
|
268
|
+
|
|
269
|
+
let content: ReactNode
|
|
270
|
+
if (groupSizes && groupSizes.length > 0) {
|
|
271
|
+
let cursor = 0
|
|
272
|
+
content = groupSizes.map((groupSize, groupIndex) => {
|
|
273
|
+
const start = cursor
|
|
274
|
+
cursor += groupSize
|
|
275
|
+
return (
|
|
276
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
277
|
+
<Fragment key={groupIndex}>
|
|
278
|
+
{groupIndex > 0 && <span className="form-otp-separator">{separator}</span>}
|
|
279
|
+
{inputGroup ? (
|
|
280
|
+
<div className="input-group">{renderBoxes(start, groupSize)}</div>
|
|
281
|
+
) : (
|
|
282
|
+
renderBoxes(start, groupSize)
|
|
283
|
+
)}
|
|
284
|
+
</Fragment>
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
} else {
|
|
288
|
+
content = renderBoxes(0, total)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const {
|
|
292
|
+
describedBy,
|
|
293
|
+
feedbackId,
|
|
294
|
+
helpId,
|
|
295
|
+
inputId: groupId,
|
|
296
|
+
labelId,
|
|
297
|
+
labelledBy
|
|
298
|
+
} = useFormField({
|
|
299
|
+
ariaDescribedBy: rest['aria-describedby'],
|
|
300
|
+
ariaLabelledBy: rest['aria-labelledby'],
|
|
301
|
+
help,
|
|
302
|
+
id,
|
|
303
|
+
invalid,
|
|
304
|
+
invalidFeedback,
|
|
305
|
+
label,
|
|
306
|
+
valid,
|
|
307
|
+
validFeedback
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
return renderFormField({
|
|
311
|
+
children: (
|
|
312
|
+
<div
|
|
313
|
+
{...rest}
|
|
314
|
+
aria-describedby={describedBy}
|
|
315
|
+
aria-labelledby={labelledBy}
|
|
316
|
+
className={classNames(
|
|
317
|
+
'form-otp',
|
|
318
|
+
{ 'input-group': inputGroup && !(groupSizes && groupSizes.length > 0) },
|
|
319
|
+
validationClassName(invalid, valid),
|
|
320
|
+
className
|
|
321
|
+
)}
|
|
322
|
+
id={groupId}
|
|
323
|
+
ref={forkedRef}
|
|
324
|
+
role="group"
|
|
325
|
+
>
|
|
326
|
+
{content}
|
|
327
|
+
{name && <input disabled={disabled} name={name} type="hidden" value={boxes.join('')} />}
|
|
328
|
+
</div>
|
|
329
|
+
),
|
|
330
|
+
help,
|
|
331
|
+
ids: { feedback: feedbackId, help: helpId, label: labelId },
|
|
332
|
+
invalid,
|
|
333
|
+
invalidFeedback,
|
|
334
|
+
label,
|
|
335
|
+
valid,
|
|
336
|
+
validFeedback
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
OtpInput.displayName = 'OtpInput'
|