@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,363 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
HTMLAttributes,
|
|
4
|
+
KeyboardEvent,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState
|
|
9
|
+
} from 'react'
|
|
10
|
+
import classNames from 'classnames'
|
|
11
|
+
import { useTextField } from 'react-aria'
|
|
12
|
+
import { Item, Key, useListState } from 'react-stately'
|
|
13
|
+
|
|
14
|
+
import { useControllableState, useForkedRef, useFormField } from '../../hooks'
|
|
15
|
+
import { validationClassName } from '../../utils/validationClassName'
|
|
16
|
+
import { renderFormField } from '../form-field/renderFormField'
|
|
17
|
+
import { ChipList, ChipItem } from './ChipList'
|
|
18
|
+
|
|
19
|
+
// `allowDuplicates` means two tags can share a value, but react-stately's collection needs a
|
|
20
|
+
// unique key per item — keying on the value alone collapses duplicates into the same node, so
|
|
21
|
+
// selecting or removing one affects all of them. Key on (value, occurrence-within-the-array)
|
|
22
|
+
// instead, recomputed fresh from `tags` on every call rather than cached, so it stays correct
|
|
23
|
+
// whether the array changed via our own add/remove or via an externally-controlled `value` prop.
|
|
24
|
+
const buildTagIds = (list: string[]): string[] => {
|
|
25
|
+
const seen = new Map<string, number>()
|
|
26
|
+
return list.map((tag) => {
|
|
27
|
+
const occurrence = seen.get(tag) ?? 0
|
|
28
|
+
seen.set(tag, occurrence + 1)
|
|
29
|
+
return occurrence === 0 ? tag : `${tag}\u0000${occurrence}`
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ChipInputProps extends Omit<
|
|
34
|
+
HTMLAttributes<HTMLDivElement>,
|
|
35
|
+
'onChange' | 'defaultValue'
|
|
36
|
+
> {
|
|
37
|
+
/**
|
|
38
|
+
* An accessible label for the chip group, used when there's no visible `<label>`.
|
|
39
|
+
*/
|
|
40
|
+
'aria-label'?: string
|
|
41
|
+
/**
|
|
42
|
+
* Identifies a visible `<label>` element for the chip group.
|
|
43
|
+
*/
|
|
44
|
+
'aria-labelledby'?: string
|
|
45
|
+
/**
|
|
46
|
+
* Allow the same value to be added more than once. Defaults to `false`.
|
|
47
|
+
*/
|
|
48
|
+
allowDuplicates?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Space-separated chassis-css chip modifier classes (e.g. `"primary smooth"`) applied to every
|
|
51
|
+
* chip. Defaults to `"default"`.
|
|
52
|
+
*/
|
|
53
|
+
chipVariant?: string
|
|
54
|
+
/**
|
|
55
|
+
* A string of all className you want applied to the base component.
|
|
56
|
+
*/
|
|
57
|
+
className?: string
|
|
58
|
+
/**
|
|
59
|
+
* The initial set of chip values (uncontrolled).
|
|
60
|
+
*/
|
|
61
|
+
defaultValue?: string[]
|
|
62
|
+
/**
|
|
63
|
+
* Prevents new chips from being added and makes existing chips non-interactive.
|
|
64
|
+
*/
|
|
65
|
+
disabled?: boolean
|
|
66
|
+
/**
|
|
67
|
+
* A description for the field, rendered below the chips.
|
|
68
|
+
*/
|
|
69
|
+
help?: ReactNode
|
|
70
|
+
/**
|
|
71
|
+
* `id` forwarded to the text input — useful for pairing with a `<label for>`.
|
|
72
|
+
*/
|
|
73
|
+
id?: string
|
|
74
|
+
/**
|
|
75
|
+
* Set component validation state to invalid.
|
|
76
|
+
*/
|
|
77
|
+
invalid?: boolean
|
|
78
|
+
/**
|
|
79
|
+
* An error message for the field, rendered below the chips when `invalid` is set.
|
|
80
|
+
*/
|
|
81
|
+
invalidFeedback?: ReactNode
|
|
82
|
+
/**
|
|
83
|
+
* The field's caption, rendered as a `FormLabel` associated with the text input.
|
|
84
|
+
*/
|
|
85
|
+
label?: ReactNode
|
|
86
|
+
/**
|
|
87
|
+
* Maximum number of chips allowed. Omit for no limit.
|
|
88
|
+
*/
|
|
89
|
+
maxChips?: number
|
|
90
|
+
/**
|
|
91
|
+
* `name` of an auto-created hidden input per chip, kept in sync with the values, for native
|
|
92
|
+
* form submission. Omit to skip creating them.
|
|
93
|
+
*/
|
|
94
|
+
name?: string
|
|
95
|
+
/**
|
|
96
|
+
* Callback fired whenever a chip is added or removed.
|
|
97
|
+
*/
|
|
98
|
+
onChange?: (values: string[]) => void
|
|
99
|
+
/**
|
|
100
|
+
* Placeholder shown in the input when empty.
|
|
101
|
+
*/
|
|
102
|
+
placeholder?: string
|
|
103
|
+
/**
|
|
104
|
+
* Character that creates a new chip when typed, or pasted text is split on. Set to `null` to
|
|
105
|
+
* disable. Defaults to `,`.
|
|
106
|
+
*/
|
|
107
|
+
separator?: string | null
|
|
108
|
+
/**
|
|
109
|
+
* Size the component small or large.
|
|
110
|
+
*/
|
|
111
|
+
size?: 'small' | 'large'
|
|
112
|
+
/**
|
|
113
|
+
* Set component validation state to valid.
|
|
114
|
+
*/
|
|
115
|
+
valid?: boolean
|
|
116
|
+
/**
|
|
117
|
+
* A success message for the field, rendered below the chips when `valid` is set.
|
|
118
|
+
*/
|
|
119
|
+
validFeedback?: ReactNode
|
|
120
|
+
/**
|
|
121
|
+
* The set of chip values (controlled).
|
|
122
|
+
*/
|
|
123
|
+
value?: string[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const ChipInput = forwardRef<HTMLDivElement, ChipInputProps>(
|
|
127
|
+
(
|
|
128
|
+
{
|
|
129
|
+
allowDuplicates = false,
|
|
130
|
+
'aria-describedby': ariaDescribedBy,
|
|
131
|
+
'aria-label': ariaLabel,
|
|
132
|
+
'aria-labelledby': ariaLabelledBy,
|
|
133
|
+
chipVariant = 'default',
|
|
134
|
+
className,
|
|
135
|
+
defaultValue,
|
|
136
|
+
disabled,
|
|
137
|
+
help,
|
|
138
|
+
id,
|
|
139
|
+
invalid,
|
|
140
|
+
invalidFeedback,
|
|
141
|
+
label,
|
|
142
|
+
maxChips,
|
|
143
|
+
name,
|
|
144
|
+
onChange,
|
|
145
|
+
placeholder,
|
|
146
|
+
separator = ',',
|
|
147
|
+
size,
|
|
148
|
+
valid,
|
|
149
|
+
validFeedback,
|
|
150
|
+
value,
|
|
151
|
+
...rest
|
|
152
|
+
}: ChipInputProps,
|
|
153
|
+
ref
|
|
154
|
+
): ReactNode => {
|
|
155
|
+
const [tags, updateTags] = useControllableState<string[]>(value, defaultValue ?? [], onChange)
|
|
156
|
+
|
|
157
|
+
// The one memoized computation of `tags`' ids — `removeTags`, `items`, and `focusLastChip`
|
|
158
|
+
// below all used to independently recompute this same array from scratch (fresh on every
|
|
159
|
+
// render, not just when `tags` actually changed); they now all read this instead.
|
|
160
|
+
const ids = useMemo(() => buildTagIds(tags), [tags])
|
|
161
|
+
|
|
162
|
+
// Shared by `addTag` and the paste handler's loop: whether `raw` (already trimmed) is
|
|
163
|
+
// addable to `list` given `allowDuplicates`. `maxChips` is deliberately not part of this
|
|
164
|
+
// predicate — the paste loop needs to `break` (stop entirely) on hitting the limit, while an
|
|
165
|
+
// empty/duplicate value should just be skipped and the loop should keep going.
|
|
166
|
+
const isAddableValue = (list: string[], trimmed: string) =>
|
|
167
|
+
trimmed !== '' && (allowDuplicates || !list.includes(trimmed))
|
|
168
|
+
|
|
169
|
+
const addTag = (raw: string) => {
|
|
170
|
+
const trimmed = raw.trim()
|
|
171
|
+
if (!isAddableValue(tags, trimmed)) return
|
|
172
|
+
if (maxChips != null && tags.length >= maxChips) return
|
|
173
|
+
updateTags([...tags, trimmed])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const removeTags = (keys: Iterable<Key>) => {
|
|
177
|
+
const toRemove = new Set(keys)
|
|
178
|
+
updateTags(tags.filter((_tag, index) => !toRemove.has(ids[index]!)))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const items = useMemo<ChipItem[]>(
|
|
182
|
+
() => tags.map((tag, index) => ({ id: ids[index]!, value: tag })),
|
|
183
|
+
[tags, ids]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const listState = useListState<ChipItem>({
|
|
187
|
+
children: (item: ChipItem) => (
|
|
188
|
+
<Item key={item.id} textValue={item.value}>
|
|
189
|
+
{item.value}
|
|
190
|
+
</Item>
|
|
191
|
+
),
|
|
192
|
+
disabledKeys: disabled ? items.map((item) => item.id) : undefined,
|
|
193
|
+
items,
|
|
194
|
+
selectionMode: 'multiple'
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const groupRef = useRef<HTMLDivElement>(null)
|
|
198
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
199
|
+
const forkedRef = useForkedRef(ref, containerRef)
|
|
200
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
201
|
+
const [inputValue, setInputValue] = useState('')
|
|
202
|
+
|
|
203
|
+
const focusLastChip = (extend: boolean) => {
|
|
204
|
+
if (tags.length === 0) return
|
|
205
|
+
const lastKey = ids[ids.length - 1]!
|
|
206
|
+
if (extend) {
|
|
207
|
+
listState.selectionManager.extendSelection(lastKey)
|
|
208
|
+
} else {
|
|
209
|
+
listState.selectionManager.replaceSelection(lastKey)
|
|
210
|
+
}
|
|
211
|
+
const rows = groupRef.current?.querySelectorAll<HTMLElement>('[role="row"]')
|
|
212
|
+
rows?.[rows.length - 1]?.focus()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
216
|
+
if (disabled) return
|
|
217
|
+
|
|
218
|
+
if (separator && event.key === separator) {
|
|
219
|
+
event.preventDefault()
|
|
220
|
+
addTag(inputValue)
|
|
221
|
+
setInputValue('')
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
switch (event.key) {
|
|
226
|
+
case 'Enter': {
|
|
227
|
+
event.preventDefault()
|
|
228
|
+
addTag(inputValue)
|
|
229
|
+
setInputValue('')
|
|
230
|
+
break
|
|
231
|
+
}
|
|
232
|
+
case 'Backspace':
|
|
233
|
+
case 'Delete': {
|
|
234
|
+
if (inputValue === '') {
|
|
235
|
+
event.preventDefault()
|
|
236
|
+
focusLastChip(false)
|
|
237
|
+
}
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
case 'ArrowLeft': {
|
|
241
|
+
const input = inputRef.current
|
|
242
|
+
if (input && input.selectionStart === 0 && input.selectionEnd === 0) {
|
|
243
|
+
event.preventDefault()
|
|
244
|
+
focusLastChip(event.shiftKey)
|
|
245
|
+
}
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
case 'Escape': {
|
|
249
|
+
setInputValue('')
|
|
250
|
+
listState.selectionManager.clearSelection()
|
|
251
|
+
inputRef.current?.blur()
|
|
252
|
+
break
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => {
|
|
260
|
+
if (disabled || !separator) return
|
|
261
|
+
const pasted = event.clipboardData.getData('text')
|
|
262
|
+
if (!pasted.includes(separator)) return
|
|
263
|
+
|
|
264
|
+
event.preventDefault()
|
|
265
|
+
const parts = pasted.split(separator)
|
|
266
|
+
|
|
267
|
+
// Accumulate locally rather than calling `addTag` per part: each call to `addTag` reads
|
|
268
|
+
// `tags` from this render's closure, so several calls in a row within one event would all
|
|
269
|
+
// start from the same stale array instead of building on each other.
|
|
270
|
+
let next = tags
|
|
271
|
+
// `i` tracks how far the loop got, so a `maxChips` cutoff can leave everything from that
|
|
272
|
+
// point on (not just the final unsplit part) in the input instead of silently dropping it.
|
|
273
|
+
let i = 0
|
|
274
|
+
for (; i < parts.length - 1; i++) {
|
|
275
|
+
if (maxChips != null && next.length >= maxChips) break
|
|
276
|
+
const trimmed = parts[i]!.trim()
|
|
277
|
+
if (!isAddableValue(next, trimmed)) continue
|
|
278
|
+
next = [...next, trimmed]
|
|
279
|
+
}
|
|
280
|
+
if (next !== tags) updateTags(next)
|
|
281
|
+
setInputValue(parts.slice(i).join(separator))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const { describedBy, feedbackId, helpId, inputId, labelId, labelledBy } = useFormField({
|
|
285
|
+
ariaDescribedBy,
|
|
286
|
+
ariaLabelledBy,
|
|
287
|
+
help,
|
|
288
|
+
id,
|
|
289
|
+
invalid,
|
|
290
|
+
invalidFeedback,
|
|
291
|
+
label,
|
|
292
|
+
valid,
|
|
293
|
+
validFeedback
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const { inputProps } = useTextField(
|
|
297
|
+
{
|
|
298
|
+
'aria-describedby': describedBy,
|
|
299
|
+
'aria-label': ariaLabel ?? (ariaLabelledBy || label ? undefined : 'Add value'),
|
|
300
|
+
'aria-labelledby': labelledBy,
|
|
301
|
+
id: inputId,
|
|
302
|
+
isDisabled: disabled,
|
|
303
|
+
isInvalid: invalid,
|
|
304
|
+
onChange: setInputValue,
|
|
305
|
+
onFocus: () => listState.selectionManager.clearSelection(),
|
|
306
|
+
onKeyDown: handleKeyDown,
|
|
307
|
+
placeholder,
|
|
308
|
+
value: inputValue
|
|
309
|
+
},
|
|
310
|
+
inputRef
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return renderFormField({
|
|
314
|
+
children: (
|
|
315
|
+
<div
|
|
316
|
+
className={classNames(
|
|
317
|
+
'form-input',
|
|
318
|
+
'chip-input',
|
|
319
|
+
size,
|
|
320
|
+
{ disabled },
|
|
321
|
+
validationClassName(invalid, valid),
|
|
322
|
+
className
|
|
323
|
+
)}
|
|
324
|
+
{...rest}
|
|
325
|
+
ref={forkedRef}
|
|
326
|
+
>
|
|
327
|
+
<ChipList
|
|
328
|
+
chipVariant={chipVariant}
|
|
329
|
+
disabled={disabled}
|
|
330
|
+
groupRef={groupRef}
|
|
331
|
+
props={{
|
|
332
|
+
'aria-label': ariaLabel,
|
|
333
|
+
'aria-labelledby': labelledBy,
|
|
334
|
+
onRemove: disabled ? undefined : removeTags
|
|
335
|
+
}}
|
|
336
|
+
size={size}
|
|
337
|
+
state={listState}
|
|
338
|
+
/>
|
|
339
|
+
<input {...inputProps} className="ghost-input" onPaste={handlePaste} ref={inputRef} />
|
|
340
|
+
{name &&
|
|
341
|
+
items.map((item) => (
|
|
342
|
+
<input
|
|
343
|
+
disabled={disabled}
|
|
344
|
+
key={item.id}
|
|
345
|
+
name={name}
|
|
346
|
+
type="hidden"
|
|
347
|
+
value={item.value}
|
|
348
|
+
/>
|
|
349
|
+
))}
|
|
350
|
+
</div>
|
|
351
|
+
),
|
|
352
|
+
help,
|
|
353
|
+
ids: { feedback: feedbackId, help: helpId, input: inputId, label: labelId },
|
|
354
|
+
invalid,
|
|
355
|
+
invalidFeedback,
|
|
356
|
+
label,
|
|
357
|
+
valid,
|
|
358
|
+
validFeedback
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
ChipInput.displayName = 'ChipInput'
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { RefObject, useRef } from 'react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import { AriaTagGroupOptions, useButton, useTag, useTagGroup } from 'react-aria'
|
|
4
|
+
import { ListState, Node } from 'react-stately'
|
|
5
|
+
|
|
6
|
+
import { Chip } from '../chip/Chip'
|
|
7
|
+
import { CloseButton } from '../close-button/CloseButton'
|
|
8
|
+
|
|
9
|
+
// react-stately's collection builder caches nodes in a WeakMap keyed by each item's own
|
|
10
|
+
// identity, so `items` must be objects — plain tag strings can't be WeakMap keys.
|
|
11
|
+
export interface ChipItem {
|
|
12
|
+
id: string
|
|
13
|
+
value: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ChipListProps {
|
|
17
|
+
chipVariant?: string
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
groupRef: RefObject<HTMLDivElement | null>
|
|
20
|
+
props: AriaTagGroupOptions<ChipItem>
|
|
21
|
+
size?: 'small' | 'large'
|
|
22
|
+
state: ListState<ChipItem>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ChipList = ({
|
|
26
|
+
chipVariant,
|
|
27
|
+
disabled,
|
|
28
|
+
groupRef,
|
|
29
|
+
props,
|
|
30
|
+
size,
|
|
31
|
+
state
|
|
32
|
+
}: ChipListProps) => {
|
|
33
|
+
const { gridProps } = useTagGroup(props, state, groupRef)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div {...gridProps} style={{ display: 'contents' }} ref={groupRef as RefObject<HTMLDivElement>}>
|
|
37
|
+
{[...state.collection].map((item) => (
|
|
38
|
+
<ChipRow
|
|
39
|
+
chipVariant={chipVariant}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
item={item}
|
|
42
|
+
key={item.key}
|
|
43
|
+
size={size}
|
|
44
|
+
state={state}
|
|
45
|
+
/>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ChipRowProps {
|
|
52
|
+
chipVariant?: string
|
|
53
|
+
disabled?: boolean
|
|
54
|
+
item: Node<ChipItem>
|
|
55
|
+
size?: 'small' | 'large'
|
|
56
|
+
state: ListState<ChipItem>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Renders one tag as the shared `Chip` component, wrapped in the row/gridcell markup
|
|
60
|
+
// `useTag`'s grid semantics require — see FORMS.md-adjacent notes in ChipInput.tsx for why the
|
|
61
|
+
// grid role lives on `ChipList`'s own wrapper rather than `.chip-input` itself. The `active`
|
|
62
|
+
// class communicates selection directly (react-aria's `aria-selected`, from `rowProps`, is the
|
|
63
|
+
// actual ARIA signal); `Chip`'s own `pressed`/`aria-pressed` prop is deliberately left unset
|
|
64
|
+
// here since `aria-pressed` isn't a valid property on a `role="row"` element.
|
|
65
|
+
const ChipRow = ({ chipVariant, disabled, item, size, state }: ChipRowProps) => {
|
|
66
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
67
|
+
const { rowProps, gridCellProps, removeButtonProps, allowsRemoving } = useTag(
|
|
68
|
+
{ item },
|
|
69
|
+
state,
|
|
70
|
+
ref
|
|
71
|
+
)
|
|
72
|
+
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
73
|
+
const { buttonProps } = useButton(removeButtonProps, buttonRef)
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Chip
|
|
77
|
+
{...rowProps}
|
|
78
|
+
className={classNames(chipVariant, { active: state.selectionManager.isSelected(item.key) })}
|
|
79
|
+
component="div"
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
ref={ref}
|
|
82
|
+
size={size}
|
|
83
|
+
>
|
|
84
|
+
<div {...gridCellProps} style={{ display: 'contents' }}>
|
|
85
|
+
{item.rendered}
|
|
86
|
+
{allowsRemoving && (
|
|
87
|
+
// react-aria's generic ButtonHTMLAttributes typing includes a legacy `color?: string`
|
|
88
|
+
// attribute that's wider than CloseButton's `color?: ContextColor` prop; buttonProps
|
|
89
|
+
// never actually sets it, so it's safe to omit from the spread's type.
|
|
90
|
+
<CloseButton {...(buttonProps as Omit<typeof buttonProps, 'color'>)} ref={buttonRef} />
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</Chip>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ElementType,
|
|
3
|
+
ForwardRefRenderFunction,
|
|
4
|
+
MouseEventHandler,
|
|
5
|
+
ReactElement,
|
|
6
|
+
Ref
|
|
7
|
+
} from 'react'
|
|
8
|
+
import classNames from 'classnames'
|
|
9
|
+
import { mergeProps } from 'react-aria'
|
|
10
|
+
|
|
11
|
+
import { ContextColor, ContextStyle } from '../../types'
|
|
12
|
+
import { useButtonSemantics, useDisabledAnchorGuard } from '../../hooks'
|
|
13
|
+
import {
|
|
14
|
+
createPolymorphicComponent,
|
|
15
|
+
PolymorphicComponentProps,
|
|
16
|
+
PolymorphicRef
|
|
17
|
+
} from '../../utils/polymorphic'
|
|
18
|
+
|
|
19
|
+
type CloseButtonOwnProps<C extends ElementType> = {
|
|
20
|
+
/**
|
|
21
|
+
* A string of all className you want applied to the base component.
|
|
22
|
+
*/
|
|
23
|
+
className?: string
|
|
24
|
+
/**
|
|
25
|
+
* Sets the color of the component to one of Chassis context colors. Applies the
|
|
26
|
+
* `context` class alongside it, so the icon picks up that color without needing
|
|
27
|
+
* an ancestor `.context` wrapper.
|
|
28
|
+
*/
|
|
29
|
+
color?: ContextColor
|
|
30
|
+
/**
|
|
31
|
+
* Component used for the root node. Either a string to use a HTML element or a component.
|
|
32
|
+
* A custom HTML tag (e.g. `'span'`) gets button semantics — role, focus, Enter/Space
|
|
33
|
+
* activation — filled in automatically. A component is trusted to handle its own semantics,
|
|
34
|
+
* so pass one that's already interactive (e.g. `Button`).
|
|
35
|
+
*/
|
|
36
|
+
component?: C
|
|
37
|
+
/**
|
|
38
|
+
* Toggle the disabled state for the component.
|
|
39
|
+
*/
|
|
40
|
+
disabled?: boolean
|
|
41
|
+
/**
|
|
42
|
+
* The href attribute specifies the URL of the page the link goes to. Only meaningful when
|
|
43
|
+
* `component` is `'a'`.
|
|
44
|
+
*/
|
|
45
|
+
href?: string
|
|
46
|
+
/**
|
|
47
|
+
* The accessible label announced by assistive technology. Override this to
|
|
48
|
+
* localize the button for non-English contexts.
|
|
49
|
+
*/
|
|
50
|
+
label?: string
|
|
51
|
+
/**
|
|
52
|
+
* Fires on click. Typed for every element `component` can actually render (`button` or `a`
|
|
53
|
+
* natively, plus whatever a custom `component` renders), rather than narrowed to
|
|
54
|
+
* `HTMLButtonElement` alone.
|
|
55
|
+
*/
|
|
56
|
+
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>
|
|
57
|
+
/**
|
|
58
|
+
* Size the component small or large.
|
|
59
|
+
*/
|
|
60
|
+
size?: 'small' | 'large'
|
|
61
|
+
/**
|
|
62
|
+
* Specifies the type of button. Always specify the type attribute for the `<button>` element.
|
|
63
|
+
* Different browsers may use different default types for the `<button>` element.
|
|
64
|
+
*/
|
|
65
|
+
type?: 'button' | 'submit' | 'reset'
|
|
66
|
+
/**
|
|
67
|
+
* Set the close button's context style variant. Applies the `context` class
|
|
68
|
+
* alongside it, same as `color`.
|
|
69
|
+
*/
|
|
70
|
+
variant?: ContextStyle
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type CloseButtonProps<C extends ElementType = 'button'> = PolymorphicComponentProps<
|
|
74
|
+
C,
|
|
75
|
+
CloseButtonOwnProps<C>
|
|
76
|
+
>
|
|
77
|
+
|
|
78
|
+
type CloseButtonComponent = (<C extends ElementType = 'button'>(
|
|
79
|
+
props: CloseButtonProps<C> & { ref?: PolymorphicRef<C> }
|
|
80
|
+
) => ReactElement | null) & { displayName?: string }
|
|
81
|
+
|
|
82
|
+
function CloseButtonRender<C extends ElementType = 'button'>(
|
|
83
|
+
{
|
|
84
|
+
children,
|
|
85
|
+
className,
|
|
86
|
+
color,
|
|
87
|
+
component,
|
|
88
|
+
disabled,
|
|
89
|
+
href,
|
|
90
|
+
label,
|
|
91
|
+
onClick,
|
|
92
|
+
size,
|
|
93
|
+
type = 'button',
|
|
94
|
+
variant,
|
|
95
|
+
...rest
|
|
96
|
+
}: CloseButtonProps<C>,
|
|
97
|
+
ref: PolymorphicRef<C>
|
|
98
|
+
) {
|
|
99
|
+
const component_ = component ?? 'button'
|
|
100
|
+
|
|
101
|
+
// A component reference (e.g. `Button`) has its own visual identity and its own
|
|
102
|
+
// `color`/`size`/`variant` semantics — CloseButton's icon styling (the `close-button`
|
|
103
|
+
// class and its context/color/variant/size classes) would just conflict with it, and
|
|
104
|
+
// would also swallow those props before the component ever saw them. So a component
|
|
105
|
+
// reference gets none of CloseButton's own styling; `color`/`size`/`variant` pass
|
|
106
|
+
// through untouched, same as `className`, letting the component interpret them itself.
|
|
107
|
+
const isComponentReference = typeof component_ !== 'string'
|
|
108
|
+
|
|
109
|
+
// Only a bare, non-`button` HTML tag needs the `.disabled` class applied by hand — a real
|
|
110
|
+
// `<button>` gets native `:disabled` styling for free (chassis-css matches both selectors,
|
|
111
|
+
// `&:disabled, &.disabled`), and a component reference isn't styled by CloseButton at all
|
|
112
|
+
// (see `isComponentReference` above).
|
|
113
|
+
const _className = isComponentReference
|
|
114
|
+
? className
|
|
115
|
+
: classNames(
|
|
116
|
+
'close-button',
|
|
117
|
+
{ context: color || variant, disabled: component_ !== 'button' && disabled },
|
|
118
|
+
color,
|
|
119
|
+
variant,
|
|
120
|
+
size,
|
|
121
|
+
className
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
// The default icon-only close button has no visible text, so it needs the 'Close' fallback
|
|
125
|
+
// as its accessible name. Once `children` renders visible content (e.g. custom text passed
|
|
126
|
+
// through `component`), let that content be the accessible name instead — forcing the
|
|
127
|
+
// fallback label here would silently override it (a Label-in-Name accessibility failure).
|
|
128
|
+
// An explicit `label` always wins either way, same as an explicit `aria-label` always did.
|
|
129
|
+
const _label = label ?? (children == null ? 'Close' : undefined)
|
|
130
|
+
|
|
131
|
+
// `<a>` has no real `disabled` attribute, so a disabled link close-button still fires
|
|
132
|
+
// click (and still navigates) unless it's blocked here, same guard `Button` applies.
|
|
133
|
+
const handleClick = useDisabledAnchorGuard<HTMLButtonElement | HTMLAnchorElement>(
|
|
134
|
+
component_ === 'a',
|
|
135
|
+
disabled,
|
|
136
|
+
onClick
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const { buttonProps, forkedRef } = useButtonSemantics<HTMLButtonElement | HTMLAnchorElement>(
|
|
140
|
+
ref as Ref<HTMLButtonElement | HTMLAnchorElement>,
|
|
141
|
+
{ disabled, onClick }
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Rendered as explicit branches, same as `Button`, rather than one `<Component>` tag
|
|
145
|
+
// driven by a `'button' | 'a' | ElementType`-typed variable — a union-typed tag would
|
|
146
|
+
// make JSX intersect all of `button`/`a`/custom prop types at once.
|
|
147
|
+
// `aria-label={_label}` comes before `...rest` in every branch below so that an explicit
|
|
148
|
+
// `aria-label` (not destructured above, so it lands in `rest`) overrides the computed
|
|
149
|
+
// fallback via JSX's last-attribute-wins rule, rather than the other way around.
|
|
150
|
+
if (component_ === 'button') {
|
|
151
|
+
return (
|
|
152
|
+
<button
|
|
153
|
+
className={_className}
|
|
154
|
+
aria-label={_label}
|
|
155
|
+
{...(rest as Record<string, unknown>)}
|
|
156
|
+
type={type}
|
|
157
|
+
disabled={disabled}
|
|
158
|
+
onClick={handleClick}
|
|
159
|
+
ref={ref as Ref<HTMLButtonElement>}
|
|
160
|
+
>
|
|
161
|
+
{children}
|
|
162
|
+
</button>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (component_ === 'a') {
|
|
167
|
+
return (
|
|
168
|
+
<a
|
|
169
|
+
className={_className}
|
|
170
|
+
aria-label={_label}
|
|
171
|
+
{...(rest as Record<string, unknown>)}
|
|
172
|
+
href={href}
|
|
173
|
+
onClick={handleClick}
|
|
174
|
+
{...(disabled && { 'aria-disabled': true, tabIndex: -1 })}
|
|
175
|
+
ref={ref as Ref<HTMLAnchorElement>}
|
|
176
|
+
>
|
|
177
|
+
{children}
|
|
178
|
+
</a>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const Component = component_ as ElementType
|
|
183
|
+
|
|
184
|
+
// Only a bare, non-`button`/`a` HTML tag (e.g. `'span'`) reaches here needing synthesized
|
|
185
|
+
// button semantics — a component reference is trusted to already be interactive, so it
|
|
186
|
+
// skips `buttonProps` (double keyboard activation) and instead gets `color`/`size`/`variant`
|
|
187
|
+
// passed through untouched, per the comment on `isComponentReference` above.
|
|
188
|
+
return (
|
|
189
|
+
<Component
|
|
190
|
+
className={_className}
|
|
191
|
+
aria-label={_label}
|
|
192
|
+
{...(isComponentReference
|
|
193
|
+
? ({ color, size, variant, disabled, onClick, ...rest } as Record<string, unknown>)
|
|
194
|
+
: (mergeProps(rest, buttonProps) as Record<string, unknown>))}
|
|
195
|
+
ref={isComponentReference ? ref : forkedRef}
|
|
196
|
+
>
|
|
197
|
+
{children}
|
|
198
|
+
</Component>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const CloseButton = createPolymorphicComponent<CloseButtonComponent>(
|
|
203
|
+
CloseButtonRender as ForwardRefRenderFunction<Element, CloseButtonProps<ElementType>>,
|
|
204
|
+
'CloseButton'
|
|
205
|
+
)
|