@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,199 @@
|
|
|
1
|
+
import React, { forwardRef, HTMLAttributes } from 'react'
|
|
2
|
+
import { Icon } from '../icon'
|
|
3
|
+
import classNames from 'classnames'
|
|
4
|
+
|
|
5
|
+
import { PaginationItem } from './PaginationItem'
|
|
6
|
+
import { usePagination } from '../../hooks'
|
|
7
|
+
|
|
8
|
+
export interface PaginationProps extends HTMLAttributes<HTMLElement> {
|
|
9
|
+
/**
|
|
10
|
+
* Current active page (1-indexed). Used with `pages` for data-driven mode.
|
|
11
|
+
*/
|
|
12
|
+
activePage?: number
|
|
13
|
+
/**
|
|
14
|
+
* Set the alignment of pagination components.
|
|
15
|
+
*/
|
|
16
|
+
align?: 'start' | 'center' | 'end'
|
|
17
|
+
/**
|
|
18
|
+
* Accessible label for the pagination `<nav>` landmark. Override for non-English locales,
|
|
19
|
+
* or when multiple paginators appear on the same page.
|
|
20
|
+
*
|
|
21
|
+
* @default 'Pagination'
|
|
22
|
+
*/
|
|
23
|
+
'aria-label'?: string
|
|
24
|
+
/**
|
|
25
|
+
* A string of all className you want applied to the base component.
|
|
26
|
+
*/
|
|
27
|
+
className?: string
|
|
28
|
+
/**
|
|
29
|
+
* Maximum number of visible page buttons (default: 5). Flanking pages are collapsed to ellipsis.
|
|
30
|
+
*/
|
|
31
|
+
maxVisiblePages?: number
|
|
32
|
+
/**
|
|
33
|
+
* Accessible label for the "next page" control, used in smart pagination mode. Override for
|
|
34
|
+
* non-English locales.
|
|
35
|
+
*
|
|
36
|
+
* @default 'Next'
|
|
37
|
+
*/
|
|
38
|
+
nextLabel?: string
|
|
39
|
+
/**
|
|
40
|
+
* Callback fired when the active page changes.
|
|
41
|
+
*/
|
|
42
|
+
onActivePageChange?: (page: number) => void
|
|
43
|
+
/**
|
|
44
|
+
* Total number of pages. When provided alongside `activePage` and `onActivePageChange`,
|
|
45
|
+
* the component renders a fully-controlled smart paginator with Prev/Next and ellipsis.
|
|
46
|
+
*/
|
|
47
|
+
pages?: number
|
|
48
|
+
/**
|
|
49
|
+
* Accessible label for the "previous page" control, used in smart pagination mode. Override
|
|
50
|
+
* for non-English locales.
|
|
51
|
+
*
|
|
52
|
+
* @default 'Previous'
|
|
53
|
+
*/
|
|
54
|
+
previousLabel?: string
|
|
55
|
+
/**
|
|
56
|
+
* Show the numbered page buttons in smart pagination mode. Set `false` alongside
|
|
57
|
+
* `showPrevNext` to build a Prev/Next-only paginator.
|
|
58
|
+
*
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
showPageNumbers?: boolean
|
|
62
|
+
/**
|
|
63
|
+
* Show the Prev/Next controls in smart pagination mode. Set `false` alongside
|
|
64
|
+
* `showPageNumbers` to build a page-numbers-only paginator.
|
|
65
|
+
*
|
|
66
|
+
* @default true
|
|
67
|
+
*/
|
|
68
|
+
showPrevNext?: boolean
|
|
69
|
+
/**
|
|
70
|
+
* Size the component small or large.
|
|
71
|
+
*/
|
|
72
|
+
size?: 'small' | 'large'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getPageRange(activePage: number, pages: number, maxVisible: number): (number | '...')[] {
|
|
76
|
+
const safeMaxVisible = Math.max(1, maxVisible)
|
|
77
|
+
|
|
78
|
+
if (pages <= safeMaxVisible) {
|
|
79
|
+
return Array.from({ length: pages }, (_, i) => i + 1)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const half = Math.floor(safeMaxVisible / 2)
|
|
83
|
+
let start = Math.max(1, activePage - half)
|
|
84
|
+
const end = Math.min(pages, start + safeMaxVisible - 1)
|
|
85
|
+
|
|
86
|
+
if (end - start < safeMaxVisible - 1) {
|
|
87
|
+
start = Math.max(1, end - safeMaxVisible + 1)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result: (number | '...')[] = []
|
|
91
|
+
|
|
92
|
+
if (start > 1) {
|
|
93
|
+
result.push(1)
|
|
94
|
+
if (start > 2) result.push('...')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (let i = start; i <= end; i++) {
|
|
98
|
+
result.push(i)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (end < pages) {
|
|
102
|
+
if (end < pages - 1) result.push('...')
|
|
103
|
+
result.push(pages)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const Pagination = forwardRef<HTMLElement, PaginationProps>(
|
|
110
|
+
(
|
|
111
|
+
{
|
|
112
|
+
activePage = 1,
|
|
113
|
+
align,
|
|
114
|
+
'aria-label': ariaLabel = 'Pagination',
|
|
115
|
+
children,
|
|
116
|
+
className,
|
|
117
|
+
maxVisiblePages = 5,
|
|
118
|
+
nextLabel = 'Next',
|
|
119
|
+
onActivePageChange,
|
|
120
|
+
pages,
|
|
121
|
+
previousLabel = 'Previous',
|
|
122
|
+
showPageNumbers = true,
|
|
123
|
+
showPrevNext = true,
|
|
124
|
+
size,
|
|
125
|
+
...rest
|
|
126
|
+
},
|
|
127
|
+
ref
|
|
128
|
+
) => {
|
|
129
|
+
const _className = classNames(
|
|
130
|
+
'pagination',
|
|
131
|
+
size,
|
|
132
|
+
{
|
|
133
|
+
[`justify-content-${align}`]: align
|
|
134
|
+
},
|
|
135
|
+
className
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const clampedActivePage = pages ? Math.min(Math.max(activePage, 1), pages) : activePage
|
|
139
|
+
const prevDisabled = pages ? clampedActivePage <= 1 : false
|
|
140
|
+
const nextDisabled = pages ? clampedActivePage >= pages : false
|
|
141
|
+
|
|
142
|
+
const { prevRef, nextRef, handlePrevClick, handleNextClick } = usePagination({
|
|
143
|
+
prevDisabled,
|
|
144
|
+
nextDisabled,
|
|
145
|
+
onPrev: () => onActivePageChange && onActivePageChange(clampedActivePage - 1),
|
|
146
|
+
onNext: () => onActivePageChange && onActivePageChange(clampedActivePage + 1)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const smartContent = pages ? (
|
|
150
|
+
<>
|
|
151
|
+
{showPrevNext && (
|
|
152
|
+
<PaginationItem
|
|
153
|
+
ref={prevRef}
|
|
154
|
+
disabled={prevDisabled}
|
|
155
|
+
onClick={handlePrevClick}
|
|
156
|
+
aria-label={previousLabel}
|
|
157
|
+
>
|
|
158
|
+
<Icon name="chevron-left-solid" className="directional-icon" />
|
|
159
|
+
</PaginationItem>
|
|
160
|
+
)}
|
|
161
|
+
{showPageNumbers &&
|
|
162
|
+
getPageRange(clampedActivePage, pages, maxVisiblePages).map((page, idx) =>
|
|
163
|
+
page === '...' ? (
|
|
164
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
165
|
+
<PaginationItem key={`ellipsis-${idx}`} disabled component="span" aria-hidden="true">
|
|
166
|
+
…
|
|
167
|
+
</PaginationItem>
|
|
168
|
+
) : (
|
|
169
|
+
<PaginationItem
|
|
170
|
+
key={page}
|
|
171
|
+
active={page === clampedActivePage}
|
|
172
|
+
onClick={() => onActivePageChange && onActivePageChange(page)}
|
|
173
|
+
>
|
|
174
|
+
{page}
|
|
175
|
+
</PaginationItem>
|
|
176
|
+
)
|
|
177
|
+
)}
|
|
178
|
+
{showPrevNext && (
|
|
179
|
+
<PaginationItem
|
|
180
|
+
ref={nextRef}
|
|
181
|
+
disabled={nextDisabled}
|
|
182
|
+
onClick={handleNextClick}
|
|
183
|
+
aria-label={nextLabel}
|
|
184
|
+
>
|
|
185
|
+
<Icon name="chevron-right-solid" className="directional-icon" />
|
|
186
|
+
</PaginationItem>
|
|
187
|
+
)}
|
|
188
|
+
</>
|
|
189
|
+
) : null
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<nav aria-label={ariaLabel} ref={ref} {...rest}>
|
|
193
|
+
<ul className={_className}>{smartContent ?? children}</ul>
|
|
194
|
+
</nav>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
Pagination.displayName = 'Pagination'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ElementType,
|
|
3
|
+
ForwardRefRenderFunction,
|
|
4
|
+
MouseEvent,
|
|
5
|
+
MouseEventHandler,
|
|
6
|
+
ReactElement,
|
|
7
|
+
Ref
|
|
8
|
+
} from 'react'
|
|
9
|
+
import classNames from 'classnames'
|
|
10
|
+
import { mergeProps } from 'react-aria'
|
|
11
|
+
|
|
12
|
+
import { useButtonSemantics } from '../../hooks'
|
|
13
|
+
import {
|
|
14
|
+
createPolymorphicComponent,
|
|
15
|
+
PolymorphicComponentProps,
|
|
16
|
+
PolymorphicRef
|
|
17
|
+
} from '../../utils/polymorphic'
|
|
18
|
+
|
|
19
|
+
type PaginationItemOwnProps<C extends ElementType> = {
|
|
20
|
+
/**
|
|
21
|
+
* Toggle the active state for the component.
|
|
22
|
+
*/
|
|
23
|
+
active?: boolean
|
|
24
|
+
/**
|
|
25
|
+
* Component used for the root node. Either a string to use a HTML element or a component.
|
|
26
|
+
*/
|
|
27
|
+
component?: C
|
|
28
|
+
/**
|
|
29
|
+
* Toggle the disabled state for the component.
|
|
30
|
+
*/
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
/**
|
|
33
|
+
* The href attribute. When provided the item renders as an `<a>` tag; otherwise as a `<button>`.
|
|
34
|
+
*/
|
|
35
|
+
href?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type PaginationItemProps<C extends ElementType = 'button'> = PolymorphicComponentProps<
|
|
39
|
+
C,
|
|
40
|
+
PaginationItemOwnProps<C>
|
|
41
|
+
>
|
|
42
|
+
|
|
43
|
+
type PaginationItemComponent = (<C extends ElementType = 'button'>(
|
|
44
|
+
props: PaginationItemProps<C> & { ref?: PolymorphicRef<C> }
|
|
45
|
+
) => ReactElement | null) & { displayName?: string }
|
|
46
|
+
|
|
47
|
+
function PaginationItemRender<C extends ElementType = 'button'>(
|
|
48
|
+
{
|
|
49
|
+
active,
|
|
50
|
+
children,
|
|
51
|
+
className,
|
|
52
|
+
component,
|
|
53
|
+
disabled,
|
|
54
|
+
href,
|
|
55
|
+
onClick,
|
|
56
|
+
...rest
|
|
57
|
+
}: PaginationItemProps<C>,
|
|
58
|
+
ref: PolymorphicRef<C>
|
|
59
|
+
) {
|
|
60
|
+
const _className = classNames(
|
|
61
|
+
'pagination-item',
|
|
62
|
+
{
|
|
63
|
+
active,
|
|
64
|
+
disabled
|
|
65
|
+
},
|
|
66
|
+
className
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const Component = (component ?? (active ? 'span' : href ? 'a' : 'button')) as ElementType
|
|
70
|
+
|
|
71
|
+
// `<a>` has no real `disabled` attribute, so a disabled anchor pagination item still fires
|
|
72
|
+
// click (and still navigates) unless it's blocked here, same guard `Button` applies.
|
|
73
|
+
const handleClick = (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
|
|
74
|
+
if (Component === 'a' && disabled) {
|
|
75
|
+
event.preventDefault()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
onClick?.(event as never)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A custom `component`/default (non-`button`/`a`) branch gets a raw `onClick` with no keyboard
|
|
82
|
+
// semantics otherwise — the same gap `Link` fills via `useButtonSemantics`.
|
|
83
|
+
const needsButtonSemantics = Component !== 'button' && Component !== 'a' && !!onClick
|
|
84
|
+
const { buttonProps, forkedRef } = useButtonSemantics<HTMLElement>(ref as Ref<HTMLElement>, {
|
|
85
|
+
disabled,
|
|
86
|
+
onClick: onClick as MouseEventHandler<HTMLElement> | undefined
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<li className={_className} {...(active && { 'aria-current': 'page' })}>
|
|
91
|
+
{Component === 'button' ? (
|
|
92
|
+
<button
|
|
93
|
+
{...(rest as Record<string, unknown>)}
|
|
94
|
+
className="pagination-link"
|
|
95
|
+
type="button"
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
onClick={handleClick}
|
|
98
|
+
ref={ref as Ref<HTMLButtonElement>}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</button>
|
|
102
|
+
) : Component === 'a' ? (
|
|
103
|
+
<a
|
|
104
|
+
{...(rest as Record<string, unknown>)}
|
|
105
|
+
className="pagination-link"
|
|
106
|
+
href={href}
|
|
107
|
+
onClick={handleClick}
|
|
108
|
+
{...(disabled && { 'aria-disabled': true, tabIndex: -1 })}
|
|
109
|
+
ref={ref as Ref<HTMLAnchorElement>}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</a>
|
|
113
|
+
) : (
|
|
114
|
+
<Component
|
|
115
|
+
className="pagination-link"
|
|
116
|
+
{...(mergeProps(rest, needsButtonSemantics ? buttonProps : {}) as Record<
|
|
117
|
+
string,
|
|
118
|
+
unknown
|
|
119
|
+
>)}
|
|
120
|
+
{...(!needsButtonSemantics && { onClick: handleClick })}
|
|
121
|
+
ref={needsButtonSemantics ? forkedRef : ref}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
</Component>
|
|
125
|
+
)}
|
|
126
|
+
</li>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const PaginationItem = createPolymorphicComponent<PaginationItemComponent>(
|
|
131
|
+
PaginationItemRender as ForwardRefRenderFunction<Element, PaginationItemProps<ElementType>>,
|
|
132
|
+
'PaginationItem'
|
|
133
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React, { Fragment, HTMLAttributes, useEffect, useMemo, useRef } from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import { useMeter } from 'react-aria'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
calculateScore,
|
|
7
|
+
defaultMessages,
|
|
8
|
+
defaultThresholds,
|
|
9
|
+
defaultWeights,
|
|
10
|
+
scoreToStrength,
|
|
11
|
+
STRENGTH_LEVELS,
|
|
12
|
+
StrengthLevel,
|
|
13
|
+
StrengthWeights
|
|
14
|
+
} from './strengthScore'
|
|
15
|
+
|
|
16
|
+
export interface PasswordStrengthProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
17
|
+
/**
|
|
18
|
+
* An accessible label for the meter. Defaults to `"Password strength"`.
|
|
19
|
+
*/
|
|
20
|
+
'aria-label'?: string
|
|
21
|
+
/**
|
|
22
|
+
* A string of all className you want applied to the base component.
|
|
23
|
+
*/
|
|
24
|
+
className?: string
|
|
25
|
+
/**
|
|
26
|
+
* `id` forwarded to the meter element.
|
|
27
|
+
*/
|
|
28
|
+
id?: string
|
|
29
|
+
/**
|
|
30
|
+
* The maximum possible score, used to compute the meter's `aria-valuemax`. Defaults to the sum
|
|
31
|
+
* of `weights`' values — set this explicitly when pairing a custom `scorer` whose scale doesn't
|
|
32
|
+
* match the built-in criteria weights, otherwise `aria-valuenow`/`aria-valuemax` get silently
|
|
33
|
+
* clamped to the built-in max.
|
|
34
|
+
*/
|
|
35
|
+
maxScore?: number
|
|
36
|
+
/**
|
|
37
|
+
* Minimum password length required to earn the first strength point. Defaults to `8`.
|
|
38
|
+
*/
|
|
39
|
+
minLength?: number
|
|
40
|
+
/**
|
|
41
|
+
* Feedback message shown for each strength level, when `showText` is `true`.
|
|
42
|
+
*/
|
|
43
|
+
messages?: Partial<Record<StrengthLevel, string>>
|
|
44
|
+
/**
|
|
45
|
+
* Callback fired whenever the strength level changes, including once on mount with the
|
|
46
|
+
* initial `value`'s strength.
|
|
47
|
+
*/
|
|
48
|
+
onStrengthChange?: (result: { score: number; strength: StrengthLevel | null }) => void
|
|
49
|
+
/**
|
|
50
|
+
* Custom scoring function overriding the built-in criteria. Receives the password and returns
|
|
51
|
+
* a numeric score.
|
|
52
|
+
*/
|
|
53
|
+
scorer?: (password: string) => number
|
|
54
|
+
/**
|
|
55
|
+
* Renders a `.strength-text` element below the meter with the current level's message.
|
|
56
|
+
* Defaults to `true`.
|
|
57
|
+
*/
|
|
58
|
+
showText?: boolean
|
|
59
|
+
/**
|
|
60
|
+
* Score boundaries `[weak, fair, good]` — scores above the last value are `"strong"`. Defaults
|
|
61
|
+
* to `[2, 4, 6]`.
|
|
62
|
+
*/
|
|
63
|
+
thresholds?: [number, number, number]
|
|
64
|
+
/**
|
|
65
|
+
* The current password value to evaluate.
|
|
66
|
+
*/
|
|
67
|
+
value: string
|
|
68
|
+
/**
|
|
69
|
+
* Render as four discrete segments, or a single growing bar. Defaults to `"segmented"`.
|
|
70
|
+
*/
|
|
71
|
+
variant?: 'bar' | 'segmented'
|
|
72
|
+
/**
|
|
73
|
+
* Point values for each scoring criterion. Set a criterion to `0` to disable it. Merged with
|
|
74
|
+
* the built-in defaults.
|
|
75
|
+
*/
|
|
76
|
+
weights?: Partial<StrengthWeights>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const PasswordStrength = ({
|
|
80
|
+
'aria-label': ariaLabel = 'Password strength',
|
|
81
|
+
className,
|
|
82
|
+
id,
|
|
83
|
+
maxScore,
|
|
84
|
+
messages,
|
|
85
|
+
minLength = 8,
|
|
86
|
+
onStrengthChange,
|
|
87
|
+
scorer,
|
|
88
|
+
showText = true,
|
|
89
|
+
thresholds = defaultThresholds,
|
|
90
|
+
value,
|
|
91
|
+
variant = 'segmented',
|
|
92
|
+
weights,
|
|
93
|
+
...rest
|
|
94
|
+
}: PasswordStrengthProps) => {
|
|
95
|
+
const mergedWeights = useMemo(() => ({ ...defaultWeights, ...weights }), [weights])
|
|
96
|
+
const mergedMessages = useMemo(() => ({ ...defaultMessages, ...messages }), [messages])
|
|
97
|
+
|
|
98
|
+
const score = useMemo(
|
|
99
|
+
() => calculateScore(value, { minLength, scorer, weights: mergedWeights }),
|
|
100
|
+
[value, minLength, scorer, mergedWeights]
|
|
101
|
+
)
|
|
102
|
+
const strength = useMemo(() => scoreToStrength(score, thresholds), [score, thresholds])
|
|
103
|
+
const resolvedMaxScore = useMemo(
|
|
104
|
+
() => maxScore ?? Object.values(mergedWeights).reduce((total, weight) => total + weight, 0),
|
|
105
|
+
[maxScore, mergedWeights]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// Deliberately not initialized to `strength` — that would make the effect below skip firing
|
|
109
|
+
// for the mount-time value, and a consumer using this to gate e.g. a submit button needs the
|
|
110
|
+
// real initial state, not just subsequent changes.
|
|
111
|
+
const previousStrength = useRef<StrengthLevel | null | undefined>(undefined)
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (previousStrength.current !== strength) {
|
|
114
|
+
previousStrength.current = strength
|
|
115
|
+
onStrengthChange?.({ score, strength })
|
|
116
|
+
}
|
|
117
|
+
}, [strength, score, onStrengthChange])
|
|
118
|
+
|
|
119
|
+
const { meterProps } = useMeter({
|
|
120
|
+
'aria-label': ariaLabel,
|
|
121
|
+
maxValue: resolvedMaxScore,
|
|
122
|
+
minValue: 0,
|
|
123
|
+
value: score,
|
|
124
|
+
valueLabel: strength ? mergedMessages[strength] : undefined
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Fragment>
|
|
131
|
+
<div
|
|
132
|
+
{...meterProps}
|
|
133
|
+
className={classNames(variant === 'bar' ? 'strength-bar' : 'strength', className)}
|
|
134
|
+
data-cx-strength={strength ?? undefined}
|
|
135
|
+
id={id}
|
|
136
|
+
{...rest}
|
|
137
|
+
>
|
|
138
|
+
{variant === 'segmented' &&
|
|
139
|
+
STRENGTH_LEVELS.map((level, index) => (
|
|
140
|
+
<div
|
|
141
|
+
className={classNames('strength-segment', { active: index <= strengthIndex })}
|
|
142
|
+
key={level}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
{showText && (
|
|
147
|
+
<span aria-live="polite" className="strength-text" data-cx-strength={strength ?? undefined}>
|
|
148
|
+
{strength ? mergedMessages[strength] : ''}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</Fragment>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
PasswordStrength.displayName = 'PasswordStrength'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type StrengthLevel = 'weak' | 'fair' | 'good' | 'strong'
|
|
2
|
+
|
|
3
|
+
export const STRENGTH_LEVELS: StrengthLevel[] = ['weak', 'fair', 'good', 'strong']
|
|
4
|
+
|
|
5
|
+
export interface StrengthWeights {
|
|
6
|
+
extraLength: number
|
|
7
|
+
lowercase: number
|
|
8
|
+
longPassword: number
|
|
9
|
+
minLength: number
|
|
10
|
+
multipleSpecial: number
|
|
11
|
+
numbers: number
|
|
12
|
+
special: number
|
|
13
|
+
uppercase: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const defaultWeights: StrengthWeights = {
|
|
17
|
+
extraLength: 1,
|
|
18
|
+
lowercase: 1,
|
|
19
|
+
longPassword: 1,
|
|
20
|
+
minLength: 1,
|
|
21
|
+
multipleSpecial: 1,
|
|
22
|
+
numbers: 1,
|
|
23
|
+
special: 1,
|
|
24
|
+
uppercase: 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const defaultMessages: Record<StrengthLevel, string> = {
|
|
28
|
+
fair: 'Fair',
|
|
29
|
+
good: 'Good',
|
|
30
|
+
strong: 'Strong',
|
|
31
|
+
weak: 'Weak'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const defaultThresholds: [number, number, number] = [2, 4, 6]
|
|
35
|
+
|
|
36
|
+
const SPECIAL_CHARS = /[!"#$%&()*,.:<>?@^{|}]/
|
|
37
|
+
const MULTIPLE_SPECIAL_CHARS = /[!"#$%&()*,.:<>?@^{|}].*[!"#$%&()*,.:<>?@^{|}]/
|
|
38
|
+
|
|
39
|
+
interface CalculateScoreOptions {
|
|
40
|
+
minLength: number
|
|
41
|
+
scorer?: (password: string) => number
|
|
42
|
+
weights: StrengthWeights
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Ported from chassis-css's vanilla strength.js `_calculateScore` — kept in lockstep with it so
|
|
46
|
+
// a password scores identically whether evaluated by the vanilla JS or this component.
|
|
47
|
+
export const calculateScore = (
|
|
48
|
+
password: string,
|
|
49
|
+
{ minLength, scorer, weights }: CalculateScoreOptions
|
|
50
|
+
): number => {
|
|
51
|
+
if (!password) return 0
|
|
52
|
+
if (scorer) return scorer(password)
|
|
53
|
+
|
|
54
|
+
let score = 0
|
|
55
|
+
|
|
56
|
+
if (password.length >= minLength) score += weights.minLength
|
|
57
|
+
if (password.length >= minLength + 4) score += weights.extraLength
|
|
58
|
+
if (/[a-z]/.test(password)) score += weights.lowercase
|
|
59
|
+
if (/[A-Z]/.test(password)) score += weights.uppercase
|
|
60
|
+
if (/\d/.test(password)) score += weights.numbers
|
|
61
|
+
if (SPECIAL_CHARS.test(password)) score += weights.special
|
|
62
|
+
if (MULTIPLE_SPECIAL_CHARS.test(password)) score += weights.multipleSpecial
|
|
63
|
+
if (password.length >= 16) score += weights.longPassword
|
|
64
|
+
|
|
65
|
+
return score
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const scoreToStrength = (
|
|
69
|
+
score: number,
|
|
70
|
+
thresholds: [number, number, number]
|
|
71
|
+
): StrengthLevel | null => {
|
|
72
|
+
if (score === 0) return null
|
|
73
|
+
|
|
74
|
+
const [weak, fair, good] = thresholds
|
|
75
|
+
if (score <= weak) return 'weak'
|
|
76
|
+
if (score <= fair) return 'fair'
|
|
77
|
+
if (score <= good) return 'good'
|
|
78
|
+
return 'strong'
|
|
79
|
+
}
|