@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,14 @@
|
|
|
1
|
+
import { createContext } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface ToastContextProps {
|
|
4
|
+
/**
|
|
5
|
+
* Whether the toast is currently visible.
|
|
6
|
+
*/
|
|
7
|
+
visible?: boolean
|
|
8
|
+
/**
|
|
9
|
+
* Dismisses the toast. Wire this to any element's `onClick` — see `useToast`.
|
|
10
|
+
*/
|
|
11
|
+
close: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ToastContext = createContext<ToastContextProps>({ close: () => {} })
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { Toast } from './Toast'
|
|
2
|
+
export type { ToastProps } from './Toast'
|
|
3
|
+
export { ToastBody } from './ToastBody'
|
|
4
|
+
export type { ToastBodyProps } from './ToastBody'
|
|
5
|
+
export { ToastFooter } from './ToastFooter'
|
|
6
|
+
export type { ToastFooterProps } from './ToastFooter'
|
|
7
|
+
export { ToastHeader } from './ToastHeader'
|
|
8
|
+
export type { ToastHeaderProps } from './ToastHeader'
|
|
9
|
+
export { ToastIcon } from './ToastIcon'
|
|
10
|
+
export type { ToastIconProps } from './ToastIcon'
|
|
11
|
+
|
|
12
|
+
// Toaster is an independent manager/container component (react-hot-toast-style), not a Toast
|
|
13
|
+
// sub-part — it subscribes to the shared toastQueue and renders Toast instances from it, rather
|
|
14
|
+
// than being composed as a child of Toast. Stays a separate top-level export.
|
|
15
|
+
export { Toaster } from './Toaster'
|
|
16
|
+
export type { ToasterProps } from './Toaster'
|
|
17
|
+
export { addToast, closeToast, toastQueue } from './toastQueue'
|
|
18
|
+
export type { ToastContent } from './toastQueue'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
import { ToastQueue } from 'react-stately'
|
|
3
|
+
|
|
4
|
+
import { ContextColor } from '../../types'
|
|
5
|
+
|
|
6
|
+
export interface ToastContent {
|
|
7
|
+
/**
|
|
8
|
+
* Apply a CSS fade transition to the toast.
|
|
9
|
+
*/
|
|
10
|
+
animation?: boolean
|
|
11
|
+
/**
|
|
12
|
+
* Auto hide the toast. The timer starts once the show transition completes and pauses
|
|
13
|
+
* while the pointer is over the toast or focus is within it.
|
|
14
|
+
*/
|
|
15
|
+
autohide?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* Content of the toast — typically a `Toast.Header`/`Toast.Body`/`Toast.Footer`.
|
|
18
|
+
*/
|
|
19
|
+
children: ReactNode
|
|
20
|
+
/**
|
|
21
|
+
* Sets the color of the component to one of Chassis context colors.
|
|
22
|
+
*/
|
|
23
|
+
color?: ContextColor
|
|
24
|
+
/**
|
|
25
|
+
* Delay hiding the toast (ms).
|
|
26
|
+
*/
|
|
27
|
+
delay?: number
|
|
28
|
+
/**
|
|
29
|
+
* Apply a full-color background with inverted text. Only meaningful alongside `color`.
|
|
30
|
+
*/
|
|
31
|
+
solid?: boolean
|
|
32
|
+
/**
|
|
33
|
+
* Apply a semi-transparent background.
|
|
34
|
+
*/
|
|
35
|
+
translucent?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The queue backing `Toaster` — a module-level singleton so `addToast()` is callable from
|
|
39
|
+
// anywhere (an event handler, an async callback) without threading a `push` prop through
|
|
40
|
+
// render. `Toaster` subscribes to it via `useToastQueue`; nothing renders until a
|
|
41
|
+
// `<Toaster />` is actually mounted somewhere to display the queue's contents.
|
|
42
|
+
export const toastQueue = new ToastQueue<ToastContent>()
|
|
43
|
+
|
|
44
|
+
// Adds a toast to the queue. Returns the toast's key, which can be passed to `closeToast` to
|
|
45
|
+
// dismiss it programmatically.
|
|
46
|
+
export function addToast(
|
|
47
|
+
children: ReactNode,
|
|
48
|
+
options: Omit<ToastContent, 'children'> = {}
|
|
49
|
+
): string {
|
|
50
|
+
return toastQueue.add({ children, ...options })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Removes a queued toast immediately, bypassing its own exit animation.
|
|
54
|
+
export function closeToast(key: string): void {
|
|
55
|
+
toastQueue.close(key)
|
|
56
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React, { FC, ReactElement, ReactNode, useRef } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import classNames from 'classnames'
|
|
4
|
+
import { mergeProps, useOverlayPosition, useTooltip, useTooltipTrigger } from 'react-aria'
|
|
5
|
+
import { useTooltipTriggerState } from 'react-stately'
|
|
6
|
+
import { Transition } from 'react-transition-group'
|
|
7
|
+
|
|
8
|
+
import { getOverlayArrowStyle, getOverlayTransitionClass, useFloatingOverlay } from '../../hooks'
|
|
9
|
+
import { Placement, resolveDataPlacement, toAriaPlacement } from '../../utils/overlayPlacement'
|
|
10
|
+
|
|
11
|
+
export type { Placement }
|
|
12
|
+
|
|
13
|
+
export interface TooltipProps {
|
|
14
|
+
children: ReactElement
|
|
15
|
+
/**
|
|
16
|
+
* Content node for your component.
|
|
17
|
+
*/
|
|
18
|
+
content: ReactNode | string
|
|
19
|
+
/**
|
|
20
|
+
* Offset of the tooltip relative to its target, as `[crossAxis, mainAxis]`.
|
|
21
|
+
*/
|
|
22
|
+
offset?: [number, number]
|
|
23
|
+
/**
|
|
24
|
+
* Callback fired when the component requests to be hidden.
|
|
25
|
+
*/
|
|
26
|
+
onHide?: () => void
|
|
27
|
+
/**
|
|
28
|
+
* Callback fired when the component requests to be shown.
|
|
29
|
+
*/
|
|
30
|
+
onShow?: () => void
|
|
31
|
+
/**
|
|
32
|
+
* Describes the preferred placement of your component. Chassis will flip it to keep it in
|
|
33
|
+
* view.
|
|
34
|
+
*/
|
|
35
|
+
placement?: Placement
|
|
36
|
+
/**
|
|
37
|
+
* Tooltips always show on focus, since keyboard/screen-reader users need them too. Set to
|
|
38
|
+
* `'focus'` to disable the hover trigger and show on focus only.
|
|
39
|
+
*/
|
|
40
|
+
trigger?: 'hover' | 'focus'
|
|
41
|
+
/**
|
|
42
|
+
* Toggle the visibility of the tooltip component.
|
|
43
|
+
*/
|
|
44
|
+
visible?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Tooltip: FC<TooltipProps> = ({
|
|
48
|
+
children,
|
|
49
|
+
content,
|
|
50
|
+
placement = 'top',
|
|
51
|
+
offset: offsetProp = [0, 6],
|
|
52
|
+
onHide,
|
|
53
|
+
onShow,
|
|
54
|
+
trigger,
|
|
55
|
+
visible,
|
|
56
|
+
...rest
|
|
57
|
+
}) => {
|
|
58
|
+
const arrowRef = useRef<HTMLDivElement>(null)
|
|
59
|
+
const triggerRef = useRef<HTMLElement | null>(null)
|
|
60
|
+
const floatingRef = useRef<HTMLDivElement>(null)
|
|
61
|
+
|
|
62
|
+
// react-stately defaults to a 1500ms warmup delay (and 500ms cooldown) before a first tooltip
|
|
63
|
+
// shows, spectrum-style — chassis-css's own JS plugin defaults to instant (`delay: 0`), so
|
|
64
|
+
// match that here rather than leaving new adopters to wonder why the first hover lags.
|
|
65
|
+
const state = useTooltipTriggerState({ trigger, delay: 0, closeDelay: 0 })
|
|
66
|
+
const { triggerProps, tooltipProps: tooltipTriggerProps } = useTooltipTrigger(
|
|
67
|
+
{ trigger },
|
|
68
|
+
state,
|
|
69
|
+
triggerRef
|
|
70
|
+
)
|
|
71
|
+
const { tooltipProps } = useTooltip({}, state)
|
|
72
|
+
|
|
73
|
+
// Bypasses the hook's hover-warmup delay (`open`/`close`'s `immediate` argument) since a
|
|
74
|
+
// programmatic `visible` change, an `onShow`/`onHide` sync, or a containing dialog closing
|
|
75
|
+
// should all apply right away, same as before.
|
|
76
|
+
const portalContainer = useFloatingOverlay({
|
|
77
|
+
close: () => state.close(true),
|
|
78
|
+
isOpen: state.isOpen,
|
|
79
|
+
open: () => state.open(true),
|
|
80
|
+
onHide,
|
|
81
|
+
onShow,
|
|
82
|
+
triggerRef,
|
|
83
|
+
visible
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const {
|
|
87
|
+
overlayProps,
|
|
88
|
+
arrowProps,
|
|
89
|
+
placement: resolvedPlacement
|
|
90
|
+
} = useOverlayPosition({
|
|
91
|
+
targetRef: triggerRef,
|
|
92
|
+
overlayRef: floatingRef,
|
|
93
|
+
placement: toAriaPlacement(placement),
|
|
94
|
+
offset: offsetProp[1],
|
|
95
|
+
crossOffset: offsetProp[0],
|
|
96
|
+
arrowSize: 8,
|
|
97
|
+
arrowRef,
|
|
98
|
+
isOpen: state.isOpen
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const floatingStyle: React.CSSProperties = {
|
|
102
|
+
position: overlayProps.style?.position as React.CSSProperties['position'],
|
|
103
|
+
top: overlayProps.style?.top,
|
|
104
|
+
left: overlayProps.style?.left
|
|
105
|
+
}
|
|
106
|
+
const placementAttr = resolveDataPlacement(placement, resolvedPlacement)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
{React.cloneElement(children, {
|
|
111
|
+
ref: (node: HTMLElement | null) => {
|
|
112
|
+
triggerRef.current = node
|
|
113
|
+
},
|
|
114
|
+
...triggerProps
|
|
115
|
+
})}
|
|
116
|
+
{typeof window !== 'undefined' &&
|
|
117
|
+
createPortal(
|
|
118
|
+
<Transition
|
|
119
|
+
in={state.isOpen}
|
|
120
|
+
mountOnEnter
|
|
121
|
+
nodeRef={floatingRef}
|
|
122
|
+
timeout={{
|
|
123
|
+
enter: 0,
|
|
124
|
+
exit: 200
|
|
125
|
+
}}
|
|
126
|
+
unmountOnExit
|
|
127
|
+
>
|
|
128
|
+
{(transitionState) => {
|
|
129
|
+
const transitionClass = getOverlayTransitionClass(transitionState)
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
className={classNames('tooltip cx-tooltip-auto', transitionClass)}
|
|
133
|
+
data-cx-placement={placementAttr}
|
|
134
|
+
ref={floatingRef}
|
|
135
|
+
style={floatingStyle}
|
|
136
|
+
{...mergeProps(tooltipTriggerProps, tooltipProps)}
|
|
137
|
+
{...rest}
|
|
138
|
+
>
|
|
139
|
+
<div
|
|
140
|
+
className="tooltip-arrow"
|
|
141
|
+
{...arrowProps}
|
|
142
|
+
style={getOverlayArrowStyle(arrowProps)}
|
|
143
|
+
></div>
|
|
144
|
+
<div className="tooltip-inner">{content}</div>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}}
|
|
148
|
+
</Transition>,
|
|
149
|
+
portalContainer ?? document.body
|
|
150
|
+
)}
|
|
151
|
+
</>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Tooltip.displayName = 'Tooltip'
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useAutoDismiss } from './useAutoDismiss'
|
|
2
|
+
import { useButtonSemantics } from './useButtonSemantics'
|
|
3
|
+
import { useControllableState } from './useControllableState'
|
|
4
|
+
import { useDialogElement } from './useDialogElement'
|
|
5
|
+
import { useDisabledAnchorGuard } from './useDisabledAnchorGuard'
|
|
6
|
+
import { useDismissibleTransition } from './useDismissibleTransition'
|
|
7
|
+
import { useDrawer } from './useDrawer'
|
|
8
|
+
import {
|
|
9
|
+
getOverlayArrowStyle,
|
|
10
|
+
getOverlayTransitionClass,
|
|
11
|
+
useFloatingOverlay
|
|
12
|
+
} from './useFloatingOverlay'
|
|
13
|
+
import { useForkedRef } from './useForkedRef'
|
|
14
|
+
import { useFormField } from './useFormField'
|
|
15
|
+
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
|
|
16
|
+
import { useModal } from './useModal'
|
|
17
|
+
import { useNotification } from './useNotification'
|
|
18
|
+
import { useOverlayPlacement } from './useOverlayPlacement'
|
|
19
|
+
import { usePagination } from './usePagination'
|
|
20
|
+
import { useToast } from './useToast'
|
|
21
|
+
import { useToastRegionQueue } from './useToastRegionQueue'
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
getOverlayArrowStyle,
|
|
25
|
+
getOverlayTransitionClass,
|
|
26
|
+
useAutoDismiss,
|
|
27
|
+
useButtonSemantics,
|
|
28
|
+
useControllableState,
|
|
29
|
+
useDialogElement,
|
|
30
|
+
useDisabledAnchorGuard,
|
|
31
|
+
useDismissibleTransition,
|
|
32
|
+
useDrawer,
|
|
33
|
+
useFloatingOverlay,
|
|
34
|
+
useForkedRef,
|
|
35
|
+
useFormField,
|
|
36
|
+
useIsomorphicLayoutEffect,
|
|
37
|
+
useModal,
|
|
38
|
+
useNotification,
|
|
39
|
+
useOverlayPlacement,
|
|
40
|
+
usePagination,
|
|
41
|
+
useToast,
|
|
42
|
+
useToastRegionQueue
|
|
43
|
+
}
|
|
44
|
+
export type { UseAutoDismissOptions, UseAutoDismissResult } from './useAutoDismiss'
|
|
45
|
+
export type { UseDialogElementOptions } from './useDialogElement'
|
|
46
|
+
export type {
|
|
47
|
+
UseDismissibleTransitionOptions,
|
|
48
|
+
UseDismissibleTransitionResult
|
|
49
|
+
} from './useDismissibleTransition'
|
|
50
|
+
export type { UseDrawerResult } from './useDrawer'
|
|
51
|
+
export type { UseFloatingOverlayOptions } from './useFloatingOverlay'
|
|
52
|
+
export type { UseFormFieldOptions, UseFormFieldResult } from './useFormField'
|
|
53
|
+
export type { UseModalResult } from './useModal'
|
|
54
|
+
export type { UseNotificationResult } from './useNotification'
|
|
55
|
+
export type { UsePaginationResult } from './usePagination'
|
|
56
|
+
export type { UseToastResult } from './useToast'
|
|
57
|
+
export type { UseToastRegionQueueResult } from './useToastRegionQueue'
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface UseAutoDismissOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Enables the auto-dismiss timer. When `false`, `onHide` is never called and the returned
|
|
6
|
+
* handlers are no-ops.
|
|
7
|
+
*/
|
|
8
|
+
enabled?: boolean
|
|
9
|
+
/**
|
|
10
|
+
* Delay in ms before `onHide` fires once the timer is (re)started.
|
|
11
|
+
*/
|
|
12
|
+
delay?: number
|
|
13
|
+
/**
|
|
14
|
+
* Whether the dismissible element is currently visible. The timer only runs while `true`.
|
|
15
|
+
*/
|
|
16
|
+
visible: boolean
|
|
17
|
+
/**
|
|
18
|
+
* Called once the delay elapses without an intervening pointer/focus interaction.
|
|
19
|
+
*/
|
|
20
|
+
onHide: () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseAutoDismissResult {
|
|
24
|
+
onMouseEnter: () => void
|
|
25
|
+
onMouseLeave: () => void
|
|
26
|
+
onFocus: () => void
|
|
27
|
+
onBlur: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Shared timer logic for components that auto-dismiss after a delay, pausing while the pointer
|
|
31
|
+
// or focus is on the element (mirrors the behavior `Toast` already implements). Extracted so new
|
|
32
|
+
// auto-dismissing components don't reimplement the pause/resume bookkeeping.
|
|
33
|
+
export function useAutoDismiss({
|
|
34
|
+
enabled = false,
|
|
35
|
+
delay = 5000,
|
|
36
|
+
visible,
|
|
37
|
+
onHide
|
|
38
|
+
}: UseAutoDismissOptions): UseAutoDismissResult {
|
|
39
|
+
const timeout = useRef<number>()
|
|
40
|
+
const hasMouseInteraction = useRef(false)
|
|
41
|
+
const hasKeyboardInteraction = useRef(false)
|
|
42
|
+
|
|
43
|
+
const clearAutoDismissTimeout = useCallback(() => {
|
|
44
|
+
clearTimeout(timeout.current)
|
|
45
|
+
timeout.current = undefined
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const maybeScheduleHide = useCallback(() => {
|
|
49
|
+
clearAutoDismissTimeout()
|
|
50
|
+
if (!enabled || hasMouseInteraction.current || hasKeyboardInteraction.current) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
timeout.current = window.setTimeout(onHide, delay)
|
|
54
|
+
}, [enabled, delay, onHide, clearAutoDismissTimeout])
|
|
55
|
+
|
|
56
|
+
// Clears on unmount.
|
|
57
|
+
useEffect(() => clearAutoDismissTimeout, [clearAutoDismissTimeout])
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (visible) {
|
|
61
|
+
maybeScheduleHide()
|
|
62
|
+
} else {
|
|
63
|
+
clearAutoDismissTimeout()
|
|
64
|
+
}
|
|
65
|
+
}, [visible, enabled, delay, maybeScheduleHide, clearAutoDismissTimeout])
|
|
66
|
+
|
|
67
|
+
const onMouseEnter = useCallback(() => {
|
|
68
|
+
hasMouseInteraction.current = true
|
|
69
|
+
clearAutoDismissTimeout()
|
|
70
|
+
}, [clearAutoDismissTimeout])
|
|
71
|
+
|
|
72
|
+
const onMouseLeave = useCallback(() => {
|
|
73
|
+
hasMouseInteraction.current = false
|
|
74
|
+
maybeScheduleHide()
|
|
75
|
+
}, [maybeScheduleHide])
|
|
76
|
+
|
|
77
|
+
const onFocus = useCallback(() => {
|
|
78
|
+
hasKeyboardInteraction.current = true
|
|
79
|
+
clearAutoDismissTimeout()
|
|
80
|
+
}, [clearAutoDismissTimeout])
|
|
81
|
+
|
|
82
|
+
const onBlur = useCallback(() => {
|
|
83
|
+
hasKeyboardInteraction.current = false
|
|
84
|
+
maybeScheduleHide()
|
|
85
|
+
}, [maybeScheduleHide])
|
|
86
|
+
|
|
87
|
+
return { onMouseEnter, onMouseLeave, onFocus, onBlur }
|
|
88
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MouseEventHandler, Ref, RefObject, useRef } from 'react'
|
|
2
|
+
import { AriaButtonProps, useButton } from 'react-aria'
|
|
3
|
+
|
|
4
|
+
import { useForkedRef } from './useForkedRef'
|
|
5
|
+
|
|
6
|
+
interface UseButtonSemanticsOptions<T extends HTMLElement> {
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
onClick?: MouseEventHandler<T>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Synthesizes native button semantics (role, tabIndex, Enter/Space activation) for a
|
|
12
|
+
// non-native `component` — a `<button>`/`<a>`/`<input>` gets all of this for free from the
|
|
13
|
+
// browser, but an arbitrary `component` (e.g. `'div'`, `'span'`) doesn't. Shared by
|
|
14
|
+
// `Link`/`Button`/`CloseButton`, which otherwise duplicated this ref/hook wiring near-verbatim.
|
|
15
|
+
// `elementType: 'div'` and the `HTMLDivElement` cast below are fixed regardless of the actual
|
|
16
|
+
// rendered tag — `useButton` only uses `elementType` to pick which ARIA attributes apply
|
|
17
|
+
// (a `div`-shaped set is the right one for an arbitrary non-native element either way), not to
|
|
18
|
+
// constrain the ref's real type.
|
|
19
|
+
export const useButtonSemantics = <T extends HTMLElement>(
|
|
20
|
+
ref: Ref<T>,
|
|
21
|
+
{ disabled, onClick }: UseButtonSemanticsOptions<T>
|
|
22
|
+
) => {
|
|
23
|
+
const buttonRef = useRef<T | null>(null)
|
|
24
|
+
const forkedRef = useForkedRef(ref, buttonRef)
|
|
25
|
+
const buttonAriaProps: AriaButtonProps<'div'> = {
|
|
26
|
+
elementType: 'div',
|
|
27
|
+
isDisabled: disabled,
|
|
28
|
+
onClick: onClick as unknown as AriaButtonProps<'div'>['onClick']
|
|
29
|
+
}
|
|
30
|
+
const { buttonProps } = useButton(buttonAriaProps, buttonRef as RefObject<HTMLDivElement | null>)
|
|
31
|
+
|
|
32
|
+
return { buttonProps, forkedRef }
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared controlled/uncontrolled state pattern: `value` present means controlled (the returned
|
|
5
|
+
* state always mirrors it), otherwise state is tracked internally starting from `defaultValue`.
|
|
6
|
+
* The returned setter updates internal state (when uncontrolled) and always calls `onChange`,
|
|
7
|
+
* leaving the caller free to layer additional logic (e.g. an `onComplete` callback) around it.
|
|
8
|
+
*/
|
|
9
|
+
export function useControllableState<T>(
|
|
10
|
+
value: T | undefined,
|
|
11
|
+
defaultValue: T,
|
|
12
|
+
onChange?: (value: T) => void
|
|
13
|
+
): [T, (next: T) => void] {
|
|
14
|
+
const isControlled = value !== undefined
|
|
15
|
+
const [uncontrolledValue, setUncontrolledValue] = useState<T>(defaultValue)
|
|
16
|
+
const state = isControlled ? (value as T) : uncontrolledValue
|
|
17
|
+
|
|
18
|
+
const setState = (next: T) => {
|
|
19
|
+
if (!isControlled) setUncontrolledValue(next)
|
|
20
|
+
onChange?.(next)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return [state, setState]
|
|
24
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ForwardedRef,
|
|
3
|
+
MouseEvent as ReactMouseEvent,
|
|
4
|
+
SyntheticEvent,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { usePreventScroll } from 'react-aria'
|
|
10
|
+
|
|
11
|
+
import { useForkedRef } from './useForkedRef'
|
|
12
|
+
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
|
|
13
|
+
import { executeAfterTransition } from '../utils/dialogTransition'
|
|
14
|
+
|
|
15
|
+
export interface UseDialogElementOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Same-named prop from the caller (`Modal`/`Drawer`) — only ever consulted here to
|
|
18
|
+
* distinguish `'static'` from everything else for the backdrop-click/Escape bounce.
|
|
19
|
+
*/
|
|
20
|
+
backdrop?: boolean | 'static'
|
|
21
|
+
/**
|
|
22
|
+
* Disables the open/close transition entirely.
|
|
23
|
+
*/
|
|
24
|
+
instant?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* Whether this open should use `showModal()` (top-layer, native backdrop, scroll-locked) or
|
|
27
|
+
* `show()` — `Modal` passes its own `modal` prop straight through; `Drawer` derives it from
|
|
28
|
+
* `backdrop`/`scroll` instead, since its vanilla chassis-css counterpart does the same.
|
|
29
|
+
*/
|
|
30
|
+
isModal: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Closes on the escape key / native `cancel` event.
|
|
33
|
+
*/
|
|
34
|
+
keyboard?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Fires synchronously right before a closed dialog actually opens (after the "already open"
|
|
37
|
+
* bail-out, before capturing the pre-open trigger element) — `Drawer`'s only hook into the
|
|
38
|
+
* show effect, used to auto-close every other open drawer first.
|
|
39
|
+
*/
|
|
40
|
+
onBeforeShow?: (dialog: HTMLDialogElement) => void
|
|
41
|
+
onClose?: () => void
|
|
42
|
+
onClosePrevented?: () => void
|
|
43
|
+
onHidden?: () => void
|
|
44
|
+
onShow?: () => void
|
|
45
|
+
onShown?: () => void
|
|
46
|
+
ref: ForwardedRef<HTMLDialogElement>
|
|
47
|
+
visible?: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Shared open/close machinery for a native `<dialog>`-based component — `Modal` and `Drawer`
|
|
51
|
+
// otherwise duplicated this near-verbatim: forked ref, the show/begin-hide effect (showModal()
|
|
52
|
+
// vs. show(), initial focus, scroll lock), the finish-hide effect (waits out the exit
|
|
53
|
+
// transition), the non-modal Escape-key listener (the native `cancel` event only fires for
|
|
54
|
+
// dialogs opened with `showModal()`), and the static-backdrop "bounce" used by both the
|
|
55
|
+
// backdrop-click handler and the Escape/`cancel` handler. What's left in each caller is only
|
|
56
|
+
// what's genuinely component-specific: `Modal`/`Drawer`'s own `_className` building, and
|
|
57
|
+
// `Drawer`'s cross-instance "auto-close every other open drawer" registry (wired in via
|
|
58
|
+
// `onBeforeShow`, since it needs to run at one precise point inside the show effect).
|
|
59
|
+
export const useDialogElement = ({
|
|
60
|
+
backdrop,
|
|
61
|
+
instant,
|
|
62
|
+
isModal,
|
|
63
|
+
keyboard = true,
|
|
64
|
+
onBeforeShow,
|
|
65
|
+
onClose,
|
|
66
|
+
onClosePrevented,
|
|
67
|
+
onHidden,
|
|
68
|
+
onShow,
|
|
69
|
+
onShown,
|
|
70
|
+
ref,
|
|
71
|
+
visible
|
|
72
|
+
}: UseDialogElementOptions) => {
|
|
73
|
+
const dialogRef = useRef<HTMLDialogElement>(null)
|
|
74
|
+
const forkedRef = useForkedRef(ref, dialogRef)
|
|
75
|
+
|
|
76
|
+
const [_visible, setVisible] = useState(visible)
|
|
77
|
+
const [hiding, setHiding] = useState(false)
|
|
78
|
+
const [staticBounce, setStaticBounce] = useState(false)
|
|
79
|
+
const [scrollLocked, setScrollLocked] = useState(false)
|
|
80
|
+
const openedAsModalRef = useRef(false)
|
|
81
|
+
const triggerRef = useRef<HTMLElement | null>(null)
|
|
82
|
+
|
|
83
|
+
// Locked from showModal() until the exit transition finishes (see the show/hide effect
|
|
84
|
+
// below) — usePreventScroll releases it automatically on unmount too, even mid-transition.
|
|
85
|
+
usePreventScroll({ isDisabled: !scrollLocked })
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
setVisible(visible)
|
|
89
|
+
}, [visible])
|
|
90
|
+
|
|
91
|
+
const close = () => {
|
|
92
|
+
onClose?.()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const triggerStaticBounce = () => {
|
|
96
|
+
onClosePrevented?.()
|
|
97
|
+
const dialog = dialogRef.current
|
|
98
|
+
if (!dialog) return
|
|
99
|
+
setStaticBounce(true)
|
|
100
|
+
executeAfterTransition(dialog, () => setStaticBounce(false), !instant)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Read through refs, kept current every render, so the keydown listener below — which only
|
|
104
|
+
// resubscribes when `keyboard` changes — never invokes a stale `close`/`triggerStaticBounce`
|
|
105
|
+
// closure if `onClose`/`onClosePrevented`/`instant` change identity without `keyboard` also
|
|
106
|
+
// changing. Same pattern as `Drawer`'s own `closeRef` and `useFloatingOverlay`'s `closeRef`.
|
|
107
|
+
const closeRef = useRef(close)
|
|
108
|
+
closeRef.current = close
|
|
109
|
+
const triggerStaticBounceRef = useRef(triggerStaticBounce)
|
|
110
|
+
triggerStaticBounceRef.current = triggerStaticBounce
|
|
111
|
+
|
|
112
|
+
// Show / begin-hide
|
|
113
|
+
useIsomorphicLayoutEffect(() => {
|
|
114
|
+
const dialog = dialogRef.current
|
|
115
|
+
if (!dialog) return undefined
|
|
116
|
+
|
|
117
|
+
if (_visible) {
|
|
118
|
+
if (dialog.open) return undefined
|
|
119
|
+
|
|
120
|
+
onBeforeShow?.(dialog)
|
|
121
|
+
|
|
122
|
+
triggerRef.current =
|
|
123
|
+
document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
124
|
+
|
|
125
|
+
openedAsModalRef.current = isModal
|
|
126
|
+
if (isModal) {
|
|
127
|
+
dialog.showModal()
|
|
128
|
+
setScrollLocked(true)
|
|
129
|
+
} else {
|
|
130
|
+
dialog.show()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const autofocusEl = dialog.querySelector<HTMLElement>('[autofocus]')
|
|
134
|
+
if (autofocusEl) {
|
|
135
|
+
autofocusEl.focus()
|
|
136
|
+
} else {
|
|
137
|
+
dialog.setAttribute('tabindex', '-1')
|
|
138
|
+
dialog.focus()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
onShow?.()
|
|
142
|
+
setHiding(false)
|
|
143
|
+
|
|
144
|
+
return executeAfterTransition(dialog, () => onShown?.(), !instant)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!dialog.open) return undefined
|
|
148
|
+
setHiding(true)
|
|
149
|
+
return undefined
|
|
150
|
+
}, [_visible])
|
|
151
|
+
|
|
152
|
+
// Finish hide once the exit transition (or lack thereof) completes
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const dialog = dialogRef.current
|
|
155
|
+
if (!hiding || !dialog) return undefined
|
|
156
|
+
|
|
157
|
+
return executeAfterTransition(
|
|
158
|
+
dialog,
|
|
159
|
+
() => {
|
|
160
|
+
if (dialog.open) {
|
|
161
|
+
dialog.close()
|
|
162
|
+
}
|
|
163
|
+
if (openedAsModalRef.current) {
|
|
164
|
+
setScrollLocked(false)
|
|
165
|
+
}
|
|
166
|
+
setHiding(false)
|
|
167
|
+
onHidden?.()
|
|
168
|
+
const trigger = triggerRef.current
|
|
169
|
+
if (trigger && document.contains(trigger)) {
|
|
170
|
+
trigger.focus()
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
!instant
|
|
174
|
+
)
|
|
175
|
+
}, [hiding])
|
|
176
|
+
|
|
177
|
+
// Escape key for non-modal (show()) dialogs — the native `cancel` event below only
|
|
178
|
+
// fires for dialogs opened with showModal().
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
const dialog = dialogRef.current
|
|
181
|
+
if (!dialog) return undefined
|
|
182
|
+
|
|
183
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
184
|
+
if (event.key !== 'Escape' || openedAsModalRef.current) return
|
|
185
|
+
event.preventDefault()
|
|
186
|
+
if (!keyboard) {
|
|
187
|
+
triggerStaticBounceRef.current()
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
closeRef.current()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
dialog.addEventListener('keydown', handleKeyDown)
|
|
194
|
+
return () => dialog.removeEventListener('keydown', handleKeyDown)
|
|
195
|
+
}, [keyboard])
|
|
196
|
+
|
|
197
|
+
const handleCancel = (event: SyntheticEvent<HTMLDialogElement>) => {
|
|
198
|
+
event.preventDefault()
|
|
199
|
+
if (!keyboard) {
|
|
200
|
+
triggerStaticBounce()
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
close()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const handleBackdropClick = (event: ReactMouseEvent<HTMLDialogElement>) => {
|
|
207
|
+
if (event.target !== dialogRef.current || !openedAsModalRef.current) return
|
|
208
|
+
if (backdrop === 'static') {
|
|
209
|
+
triggerStaticBounce()
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
close()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { close, dialogRef, forkedRef, handleBackdropClick, handleCancel, hiding, staticBounce }
|
|
216
|
+
}
|