@primitiv-ui/react 0.1.28 → 0.1.30
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/README.md +19 -2
- package/package.json +6 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
- package/src/AccessibleIcon/types.ts +15 -2
- package/src/Accordion/Accordion.tsx +32 -11
- package/src/Accordion/README.md +2 -2
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
- package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
- package/src/Accordion/types.ts +72 -21
- package/src/Alert/Alert.tsx +36 -7
- package/src/Alert/__tests__/Alert.test.tsx +5 -0
- package/src/Alert/types.ts +18 -2
- package/src/Avatar/Avatar.tsx +99 -15
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
- package/src/Avatar/hooks/useAvatarImage.ts +16 -0
- package/src/Avatar/types.ts +15 -3
- package/src/Breadcrumb/Breadcrumb.tsx +139 -21
- package/src/Breadcrumb/README.md +47 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
- package/src/Breadcrumb/types.ts +73 -7
- package/src/Button/Button.tsx +2 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
- package/src/Button/__tests__/Button.contract.test.tsx +1 -1
- package/src/Button/types.ts +21 -0
- package/src/Carousel/Carousel.tsx +268 -20
- package/src/Carousel/README.md +764 -87
- package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
- package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
- package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
- package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
- package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
- package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
- package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
- package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
- package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
- package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
- package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
- package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
- package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
- package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
- package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
- package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
- package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
- package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
- package/src/Carousel/hooks/index.ts +1 -0
- package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
- package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
- package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
- package/src/Carousel/loopEngine.ts +54 -0
- package/src/Carousel/types.ts +458 -36
- package/src/Checkbox/Checkbox.tsx +11 -3
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
- package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
- package/src/Checkbox/types.ts +19 -0
- package/src/CheckboxCard/CheckboxCard.tsx +14 -5
- package/src/CheckboxCard/README.md +1 -1
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
- package/src/CheckboxCard/types.ts +65 -17
- package/src/Collapsible/Collapsible.tsx +49 -3
- package/src/Collapsible/README.md +41 -1
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
- package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
- package/src/Collapsible/types.ts +89 -22
- package/src/Combobox/Combobox.tsx +508 -0
- package/src/Combobox/ComboboxContext.ts +23 -0
- package/src/Combobox/README.md +170 -0
- package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
- package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
- package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
- package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
- package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
- package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
- package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
- package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
- package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
- package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
- package/src/Combobox/hooks/index.ts +1 -0
- package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
- package/src/Combobox/index.ts +3 -0
- package/src/Combobox/types.ts +133 -0
- package/src/ContextMenu/ContextMenu.tsx +270 -4
- package/src/ContextMenu/README.md +1 -1
- package/src/ContextMenu/types.ts +148 -6
- package/src/DirectionProvider/DirectionProvider.tsx +41 -9
- package/src/DirectionProvider/README.md +4 -4
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
- package/src/DirectionProvider/types.ts +28 -3
- package/src/Divider/Divider.tsx +24 -10
- package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
- package/src/Divider/__tests__/Divider.test.tsx +5 -0
- package/src/Divider/types.ts +10 -3
- package/src/Drawer/Drawer.tsx +315 -0
- package/src/Drawer/README.md +124 -0
- package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
- package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
- package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
- package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
- package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
- package/src/Drawer/index.ts +2 -0
- package/src/Drawer/types.ts +66 -0
- package/src/Dropdown/Dropdown.tsx +119 -2
- package/src/Dropdown/README.md +7 -5
- package/src/Dropdown/types.ts +241 -11
- package/src/EmptyState/EmptyState.tsx +74 -4
- package/src/EmptyState/README.md +2 -2
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
- package/src/EmptyState/types.ts +49 -6
- package/src/Field/Field.tsx +23 -0
- package/src/Field/README.md +1 -1
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
- package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
- package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
- package/src/Field/hooks/useFieldProps.ts +1 -1
- package/src/Field/types.ts +60 -12
- package/src/Fieldset/Fieldset.tsx +55 -19
- package/src/Fieldset/README.md +2 -2
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
- package/src/Fieldset/types.ts +20 -2
- package/src/Input/Input.tsx +15 -10
- package/src/Input/README.md +2 -2
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
- package/src/Input/types.ts +19 -2
- package/src/InputGroup/InputGroup.tsx +11 -3
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
- package/src/InputGroup/types.ts +26 -5
- package/src/Listbox/Listbox.tsx +475 -0
- package/src/Listbox/ListboxContext.ts +48 -0
- package/src/Listbox/README.md +248 -0
- package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
- package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
- package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
- package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
- package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
- package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
- package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
- package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
- package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
- package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
- package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
- package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
- package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
- package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
- package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
- package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
- package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
- package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
- package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
- package/src/Listbox/constants.ts +2 -0
- package/src/Listbox/hooks/index.ts +1 -0
- package/src/Listbox/hooks/useListboxRoot.ts +407 -0
- package/src/Listbox/index.ts +3 -0
- package/src/Listbox/types.ts +238 -0
- package/src/MillerColumns/MillerColumns.tsx +186 -45
- package/src/MillerColumns/README.md +90 -20
- package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
- package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
- package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
- package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
- package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
- package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
- package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
- package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
- package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
- package/src/MillerColumns/constants.ts +2 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
- package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
- package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
- package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
- package/src/MillerColumns/index.ts +4 -0
- package/src/MillerColumns/types.ts +136 -30
- package/src/MillerColumns/utils.ts +69 -3
- package/src/Modal/Modal.tsx +35 -8
- package/src/Modal/README.md +24 -15
- package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
- package/src/Modal/hooks/useModalContent.ts +32 -0
- package/src/Modal/types.ts +110 -29
- package/src/NavigationMenu/NavigationMenu.tsx +890 -0
- package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
- package/src/NavigationMenu/README.md +355 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
- package/src/NavigationMenu/constants.ts +5 -0
- package/src/NavigationMenu/hooks/index.ts +7 -0
- package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
- package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
- package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
- package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
- package/src/NavigationMenu/index.ts +3 -0
- package/src/NavigationMenu/types.ts +259 -0
- package/src/NavigationMenu/utils.ts +89 -0
- package/src/Popover/Popover.tsx +427 -0
- package/src/Popover/PopoverContext.ts +13 -0
- package/src/Popover/README.md +106 -0
- package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
- package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
- package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
- package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
- package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
- package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
- package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
- package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
- package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
- package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
- package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
- package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
- package/src/Popover/hooks/index.ts +3 -0
- package/src/Popover/hooks/usePopoverContent.ts +98 -0
- package/src/Popover/hooks/usePopoverRoot.ts +84 -0
- package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
- package/src/Popover/index.ts +2 -0
- package/src/Popover/types.ts +197 -0
- package/src/Portal/Portal.tsx +50 -10
- package/src/Portal/README.md +2 -2
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
- package/src/Portal/types.ts +5 -0
- package/src/Progress/Progress.tsx +33 -1
- package/src/Progress/README.md +1 -1
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
- package/src/Progress/types.ts +20 -4
- package/src/Radio/Radio.tsx +10 -2
- package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
- package/src/Radio/hooks/useRadioRoot.ts +3 -0
- package/src/Radio/types.ts +20 -0
- package/src/RadioCard/RadioCard.tsx +11 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
- package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
- package/src/RadioCard/types.ts +95 -22
- package/src/RadioGroup/README.md +2 -2
- package/src/RadioGroup/RadioGroup.tsx +12 -1
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
- package/src/RadioGroup/types.ts +73 -9
- package/src/SegmentedControl/README.md +154 -0
- package/src/SegmentedControl/SegmentedControl.tsx +309 -0
- package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
- package/src/SegmentedControl/hooks/index.ts +2 -0
- package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
- package/src/SegmentedControl/index.ts +2 -0
- package/src/SegmentedControl/types.ts +138 -0
- package/src/Select/README.md +122 -157
- package/src/Select/Select.tsx +596 -69
- package/src/Select/SelectContext.ts +38 -0
- package/src/Select/SelectItemIndicatorContext.ts +29 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
- package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
- package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
- package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
- package/src/Select/__tests__/Select.form.test.tsx +38 -0
- package/src/Select/__tests__/Select.group.test.tsx +6 -6
- package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
- package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
- package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
- package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
- package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
- package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
- package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
- package/src/Select/__tests__/Select.selection.test.tsx +104 -0
- package/src/Select/__tests__/Select.separator.test.tsx +71 -0
- package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
- package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
- package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
- package/src/Select/constants.ts +8 -0
- package/src/Select/hooks/index.ts +2 -0
- package/src/Select/hooks/useSelectContent.ts +149 -0
- package/src/Select/hooks/useSelectRoot.ts +149 -0
- package/src/Select/types.ts +199 -31
- package/src/SkipNav/README.md +3 -3
- package/src/SkipNav/SkipNav.tsx +72 -32
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
- package/src/SkipNav/types.ts +17 -9
- package/src/Slider/README.md +37 -7
- package/src/Slider/Slider.tsx +42 -4
- package/src/Slider/SliderContext.ts +28 -1
- package/src/Slider/types.ts +157 -9
- package/src/Slot/Slot.tsx +94 -19
- package/src/Slot/__tests__/Slot.test.tsx +112 -2
- package/src/Slot/composeEventHandlers.ts +26 -2
- package/src/Slot/types.ts +16 -0
- package/src/SplitButton/README.md +225 -0
- package/src/SplitButton/SplitButton.tsx +457 -0
- package/src/SplitButton/SplitButtonContext.ts +26 -0
- package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
- package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
- package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
- package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
- package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
- package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
- package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
- package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
- package/src/SplitButton/index.ts +2 -0
- package/src/SplitButton/types.ts +186 -0
- package/src/Status/Status.tsx +36 -15
- package/src/Status/__tests__/Status.test.tsx +5 -0
- package/src/Status/types.ts +12 -1
- package/src/Switch/README.md +1 -1
- package/src/Switch/Switch.tsx +17 -4
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
- package/src/Switch/hooks/useSwitchRoot.ts +3 -0
- package/src/Switch/types.ts +49 -22
- package/src/Table/README.md +110 -8
- package/src/Table/Table.tsx +246 -16
- package/src/Table/TableExpandableContext.ts +7 -0
- package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
- package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
- package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
- package/src/Table/types.ts +191 -17
- package/src/Tabs/README.md +4 -4
- package/src/Tabs/Tabs.tsx +13 -4
- package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
- package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
- package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
- package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
- package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
- package/src/Tabs/hooks/useTabsContent.ts +5 -0
- package/src/Tabs/hooks/useTabsRoot.ts +29 -5
- package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
- package/src/Tabs/types.ts +29 -4
- package/src/Textarea/Textarea.tsx +9 -1
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
- package/src/Textarea/types.ts +20 -3
- package/src/Toggle/Toggle.tsx +51 -13
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
- package/src/Toggle/types.ts +54 -4
- package/src/ToggleGroup/README.md +4 -4
- package/src/ToggleGroup/ToggleGroup.tsx +14 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
- package/src/ToggleGroup/types.ts +55 -2
- package/src/Tooltip/README.md +7 -7
- package/src/Tooltip/Tooltip.tsx +100 -22
- package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
- package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
- package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
- package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
- package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
- package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
- package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
- package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
- package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
- package/src/Tooltip/types.ts +89 -8
- package/src/Tree/README.md +69 -5
- package/src/Tree/Tree.tsx +188 -34
- package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
- package/src/Tree/hooks/useTreePath.ts +33 -1
- package/src/Tree/index.ts +6 -1
- package/src/Tree/types.ts +54 -12
- package/src/Tree/utils.ts +46 -4
- package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
- package/src/VisuallyHidden/types.ts +11 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTypeahead.ts +95 -0
- package/src/index.ts +10 -0
- package/src/test/intersectionObserverPolyfill.ts +7 -0
- package/src/test/resizeObserverPolyfill.ts +80 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
- package/src/useLocalStorage/index.ts +1 -0
- package/src/useLocalStorage/useLocalStorage.ts +140 -0
- package/src/useMediaQuery/README.md +66 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
- package/src/useMediaQuery/index.ts +1 -0
- package/src/useMediaQuery/useMediaQuery.ts +49 -0
- package/src/usePagination/README.md +170 -0
- package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
- package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
- package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
- package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
- package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
- package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
- package/src/usePagination/index.ts +1 -0
- package/src/usePagination/usePagination.ts +292 -0
- package/src/utils/focusable.ts +15 -0
- package/src/utils/getKeyToActionMap.ts +1 -1
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
import { Modal } from "../Modal/index.ts";
|
|
4
|
+
import type {
|
|
5
|
+
DrawerCloseProps,
|
|
6
|
+
DrawerContentProps,
|
|
7
|
+
DrawerDescriptionProps,
|
|
8
|
+
DrawerOverlayProps,
|
|
9
|
+
DrawerPortalProps,
|
|
10
|
+
DrawerRootProps,
|
|
11
|
+
DrawerTitleProps,
|
|
12
|
+
DrawerTriggerProps,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The root of a Drawer — owns open state, registers the dialog's id,
|
|
17
|
+
* title id, and description id, and exposes an imperative handle for
|
|
18
|
+
* opening / closing from outside the React subtree.
|
|
19
|
+
*
|
|
20
|
+
* A Drawer *is* a {@link Modal | Modal} that slides in from a screen edge, so
|
|
21
|
+
* `Drawer.Root` delegates its entire behaviour to `Modal.Root`: the two
|
|
22
|
+
* state modes (uncontrolled `defaultOpen` vs. controlled `open` +
|
|
23
|
+
* `onOpenChange`, statically discriminated at the type level) and the
|
|
24
|
+
* imperative open/close API are identical.
|
|
25
|
+
*
|
|
26
|
+
* @example Uncontrolled
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <Drawer.Root defaultOpen>
|
|
29
|
+
* <Drawer.Content>...</Drawer.Content>
|
|
30
|
+
* </Drawer.Root>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example Controlled
|
|
34
|
+
* ```tsx
|
|
35
|
+
* const [open, setOpen] = useState(false);
|
|
36
|
+
*
|
|
37
|
+
* <Drawer.Root open={open} onOpenChange={setOpen}>
|
|
38
|
+
* <Drawer.Trigger>Open</Drawer.Trigger>
|
|
39
|
+
* <Drawer.Portal>
|
|
40
|
+
* <Drawer.Overlay />
|
|
41
|
+
* <Drawer.Content side="right">...</Drawer.Content>
|
|
42
|
+
* </Drawer.Portal>
|
|
43
|
+
* </Drawer.Root>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function DrawerRoot(props: DrawerRootProps): ReactElement {
|
|
47
|
+
return <Modal.Root {...props} />;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
DrawerRoot.displayName = "DrawerRoot";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A button that toggles the drawer open. Renders a
|
|
54
|
+
* `<button type="button">` with full ARIA wiring inherited from
|
|
55
|
+
* `Modal.Trigger`:
|
|
56
|
+
*
|
|
57
|
+
* - `aria-haspopup="dialog"`
|
|
58
|
+
* - `aria-expanded` tracks open state
|
|
59
|
+
* - `aria-controls` points at the `Drawer.Content` dialog's id
|
|
60
|
+
*
|
|
61
|
+
* **`asChild` prop.** Pass `asChild` to render any consumer-supplied
|
|
62
|
+
* element (e.g. a router `<Link>`) with the trigger's ARIA attributes,
|
|
63
|
+
* composed event handlers, and ref merged in.
|
|
64
|
+
*
|
|
65
|
+
* @extends HTMLButtonElement
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* <Drawer.Trigger>Open menu</Drawer.Trigger>
|
|
70
|
+
*
|
|
71
|
+
* <Drawer.Trigger asChild>
|
|
72
|
+
* <Link to="/cart">Cart</Link>
|
|
73
|
+
* </Drawer.Trigger>
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
function DrawerTrigger(props: DrawerTriggerProps): ReactElement {
|
|
77
|
+
return <Modal.Trigger {...props} />;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
DrawerTrigger.displayName = "DrawerTrigger";
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Renders its children through `React.createPortal` so the drawer is
|
|
84
|
+
* detached from wherever `Drawer.Root` lives in the React tree and becomes
|
|
85
|
+
* a direct child of `container` (default `document.body`).
|
|
86
|
+
*
|
|
87
|
+
* By default the portal only renders while the drawer is open. Pass
|
|
88
|
+
* `forceMount` to keep the subtree in the DOM after `open` flips false —
|
|
89
|
+
* useful for the CSS slide-out animation driven by `data-state="closed"`.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```tsx
|
|
93
|
+
* <Drawer.Portal container={document.getElementById("drawer-root")!}>
|
|
94
|
+
* <Drawer.Overlay />
|
|
95
|
+
* <Drawer.Content>...</Drawer.Content>
|
|
96
|
+
* </Drawer.Portal>
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
function DrawerPortal(props: DrawerPortalProps): ReactElement {
|
|
100
|
+
return <Modal.Portal {...props} />;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
DrawerPortal.displayName = "DrawerPortal";
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A decorative, animation-friendly backdrop rendered as a **sibling** of
|
|
107
|
+
* `Drawer.Content`. Like `Modal.Overlay` it is **not** the click-outside
|
|
108
|
+
* event surface — opening the native `<dialog>` via `showModal()` promotes
|
|
109
|
+
* it to the top layer with a browser-painted `::backdrop` above this
|
|
110
|
+
* overlay, so click-outside-to-close is wired on the dialog itself via
|
|
111
|
+
* `onPointerDownOutside`.
|
|
112
|
+
*
|
|
113
|
+
* - `aria-hidden="true"` (the backdrop is decorative).
|
|
114
|
+
* - `data-state="open" | "closed"` follows the drawer's open state.
|
|
115
|
+
*
|
|
116
|
+
* **`asChild` prop.** Pass `asChild` to render a consumer-supplied element
|
|
117
|
+
* (e.g. a motion wrapper) with the overlay's ARIA and data-state merged in.
|
|
118
|
+
*
|
|
119
|
+
* **`forceMount` prop.** Pass `forceMount` to keep the overlay in the DOM
|
|
120
|
+
* while `open` is false so a CSS fade-out can play.
|
|
121
|
+
*
|
|
122
|
+
* @extends HTMLDivElement
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```tsx
|
|
126
|
+
* <Drawer.Overlay />
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
function DrawerOverlay(props: DrawerOverlayProps): ReactElement {
|
|
130
|
+
return <Modal.Overlay {...props} />;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
DrawerOverlay.displayName = "DrawerOverlay";
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The native `<dialog>` element — the sliding panel. Behaviour is
|
|
137
|
+
* `Modal.Content`'s: React drives `showModal()` / `close()` in response to
|
|
138
|
+
* open state, so the browser owns the inert background, the top layer, and
|
|
139
|
+
* the `Esc` key, while the same JS `Tab`-wrap focus trap (last ⇄ first)
|
|
140
|
+
* applies — inherited from `Modal.Content`. `aria-labelledby` /
|
|
141
|
+
* `aria-describedby` are wired automatically from `Drawer.Title` /
|
|
142
|
+
* `Drawer.Description`, and the `onEscapeKeyDown` / `onPointerDownOutside`
|
|
143
|
+
* escape hatches pass straight through.
|
|
144
|
+
*
|
|
145
|
+
* **The one drawer-specific addition —
|
|
146
|
+
* {@link DrawerContentProps.side | `side`}.** Which edge the panel slides in
|
|
147
|
+
* from (`"top" | "right" | "bottom" | "left"`, default `"right"`). It is
|
|
148
|
+
* emitted verbatim as `data-side` on the dialog for the styled layer to
|
|
149
|
+
* position and animate against; it changes no behaviour, focus, or ARIA.
|
|
150
|
+
*
|
|
151
|
+
* **`asChild` is intentionally not supported** (as with `Modal.Content`) —
|
|
152
|
+
* the native dialog is what provides the inert background and top layer (and
|
|
153
|
+
* hosts the inherited Tab-wrap focus trap).
|
|
154
|
+
*
|
|
155
|
+
* **Styling hooks.** `data-state="open" | "closed"` and
|
|
156
|
+
* `data-side="top|right|bottom|left"` on the dialog.
|
|
157
|
+
*
|
|
158
|
+
* @extends HTMLDialogElement
|
|
159
|
+
*
|
|
160
|
+
* @example Right-hand drawer (default)
|
|
161
|
+
* ```tsx
|
|
162
|
+
* <Drawer.Content>...</Drawer.Content>
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* @example Bottom sheet
|
|
166
|
+
* ```tsx
|
|
167
|
+
* <Drawer.Content side="bottom">...</Drawer.Content>
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
function DrawerContent({
|
|
171
|
+
side = "right",
|
|
172
|
+
...rest
|
|
173
|
+
}: DrawerContentProps): ReactElement {
|
|
174
|
+
// `side` is stripped from the props and re-emitted as `data-side` on the
|
|
175
|
+
// native <dialog> (Modal.Content forwards unrecognised props through its own
|
|
176
|
+
// `...rest`), giving the styled layer its positioning hook.
|
|
177
|
+
return <Modal.Content {...rest} data-side={side} />;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
DrawerContent.displayName = "DrawerContent";
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The drawer's accessible name. Renders an `<h2>` by default and
|
|
184
|
+
* auto-registers its generated id so `Drawer.Content` can wire it up as
|
|
185
|
+
* `aria-labelledby` (via `Modal.Title`).
|
|
186
|
+
*
|
|
187
|
+
* Pass `asChild` to render the consumer's own heading element; the id is
|
|
188
|
+
* still registered.
|
|
189
|
+
*
|
|
190
|
+
* @extends HTMLHeadingElement
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```tsx
|
|
194
|
+
* <Drawer.Title>Filters</Drawer.Title>
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
function DrawerTitle(props: DrawerTitleProps): ReactElement {
|
|
198
|
+
return <Modal.Title {...props} />;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
DrawerTitle.displayName = "DrawerTitle";
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The drawer's accessible description. Renders a `<p>` by default and
|
|
205
|
+
* auto-registers its generated id so `Drawer.Content` can wire it up as
|
|
206
|
+
* `aria-describedby` (via `Modal.Description`).
|
|
207
|
+
*
|
|
208
|
+
* Pass `asChild` to render any consumer-supplied element; the id is still
|
|
209
|
+
* registered.
|
|
210
|
+
*
|
|
211
|
+
* @extends HTMLParagraphElement
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```tsx
|
|
215
|
+
* <Drawer.Description>Narrow the results below.</Drawer.Description>
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
function DrawerDescription(props: DrawerDescriptionProps): ReactElement {
|
|
219
|
+
return <Modal.Description {...props} />;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
DrawerDescription.displayName = "DrawerDescription";
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A button that closes the drawer. Renders a `<button type="button">`
|
|
226
|
+
* whose `onClick` is composed with the close action (via `Modal.Close`) —
|
|
227
|
+
* consumer handlers run first and can `event.preventDefault()` to veto.
|
|
228
|
+
*
|
|
229
|
+
* **`asChild` prop.** Pass `asChild` to render any element (e.g. an
|
|
230
|
+
* icon-only button of your own) with the close behaviour merged in.
|
|
231
|
+
*
|
|
232
|
+
* @extends HTMLButtonElement
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```tsx
|
|
236
|
+
* <Drawer.Close>Done</Drawer.Close>
|
|
237
|
+
*
|
|
238
|
+
* <Drawer.Close asChild>
|
|
239
|
+
* <IconButton aria-label="Close" icon={<XIcon />} />
|
|
240
|
+
* </Drawer.Close>
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
function DrawerClose(props: DrawerCloseProps): ReactElement {
|
|
244
|
+
return <Modal.Close {...props} />;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
DrawerClose.displayName = "DrawerClose";
|
|
248
|
+
|
|
249
|
+
/** @internal */
|
|
250
|
+
type DrawerCompound = typeof DrawerRoot & {
|
|
251
|
+
Root: typeof DrawerRoot;
|
|
252
|
+
Trigger: typeof DrawerTrigger;
|
|
253
|
+
Portal: typeof DrawerPortal;
|
|
254
|
+
Overlay: typeof DrawerOverlay;
|
|
255
|
+
Content: typeof DrawerContent;
|
|
256
|
+
Title: typeof DrawerTitle;
|
|
257
|
+
Description: typeof DrawerDescription;
|
|
258
|
+
Close: typeof DrawerClose;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Headless, accessible **Drawer** — a Modal dialog that slides in from a
|
|
263
|
+
* screen edge. It reuses {@link Modal | Modal}'s native-`<dialog>` machinery
|
|
264
|
+
* wholesale (focus trap, inert background, top layer, `Esc`, click-outside,
|
|
265
|
+
* controlled / uncontrolled state, the imperative API), adding a single
|
|
266
|
+
* presentational axis: {@link DrawerContentProps.side | `side`} on
|
|
267
|
+
* `Drawer.Content`, emitted as `data-side` for the styled layer to slide the
|
|
268
|
+
* panel from `top` / `right` / `bottom` / `left`.
|
|
269
|
+
*
|
|
270
|
+
* `Drawer` is both callable (an alias of {@link DrawerRoot | `Drawer.Root`})
|
|
271
|
+
* and carries its sub-components as static properties.
|
|
272
|
+
*
|
|
273
|
+
* - {@link DrawerRoot | `Drawer.Root`} — state owner, context provider, imperative API holder.
|
|
274
|
+
* - {@link DrawerTrigger | `Drawer.Trigger`} — `<button>` that opens the drawer.
|
|
275
|
+
* - {@link DrawerPortal | `Drawer.Portal`} — `createPortal` wrapper with `container` + `forceMount`.
|
|
276
|
+
* - {@link DrawerOverlay | `Drawer.Overlay`} — click-outside backdrop sibling of the dialog.
|
|
277
|
+
* - {@link DrawerContent | `Drawer.Content`} — native `<dialog>` with the `side` axis and auto-ARIA.
|
|
278
|
+
* - {@link DrawerTitle | `Drawer.Title`} — accessible name; auto-wires `aria-labelledby`.
|
|
279
|
+
* - {@link DrawerDescription | `Drawer.Description`} — auto-wires `aria-describedby`.
|
|
280
|
+
* - {@link DrawerClose | `Drawer.Close`} — `<button>` that closes the drawer.
|
|
281
|
+
*
|
|
282
|
+
* @example Minimal usage
|
|
283
|
+
* ```tsx
|
|
284
|
+
* import { Drawer } from "@primitiv-ui/react";
|
|
285
|
+
*
|
|
286
|
+
* <Drawer.Root>
|
|
287
|
+
* <Drawer.Trigger>Open</Drawer.Trigger>
|
|
288
|
+
* <Drawer.Portal>
|
|
289
|
+
* <Drawer.Overlay />
|
|
290
|
+
* <Drawer.Content side="right">
|
|
291
|
+
* <Drawer.Title>Filters</Drawer.Title>
|
|
292
|
+
* <Drawer.Description>Narrow the results below.</Drawer.Description>
|
|
293
|
+
* <Drawer.Close>Done</Drawer.Close>
|
|
294
|
+
* </Drawer.Content>
|
|
295
|
+
* </Drawer.Portal>
|
|
296
|
+
* </Drawer.Root>;
|
|
297
|
+
* ```
|
|
298
|
+
*
|
|
299
|
+
* @see {@link Modal} for the shared dialog behaviour, state modes, and imperative API.
|
|
300
|
+
* @see {@link DrawerContent} for the `side` axis and the styling hooks.
|
|
301
|
+
*/
|
|
302
|
+
const DrawerCompound: DrawerCompound = Object.assign(DrawerRoot, {
|
|
303
|
+
Root: DrawerRoot,
|
|
304
|
+
Trigger: DrawerTrigger,
|
|
305
|
+
Portal: DrawerPortal,
|
|
306
|
+
Overlay: DrawerOverlay,
|
|
307
|
+
Content: DrawerContent,
|
|
308
|
+
Title: DrawerTitle,
|
|
309
|
+
Description: DrawerDescription,
|
|
310
|
+
Close: DrawerClose,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
DrawerCompound.displayName = "Drawer";
|
|
314
|
+
|
|
315
|
+
export { DrawerCompound as Drawer };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Drawer
|
|
2
|
+
|
|
3
|
+
A headless, accessible **dialog that slides in from a screen edge** — a
|
|
4
|
+
settings panel, a filter sheet, a navigation drawer, a cart. Behaviourally it
|
|
5
|
+
_is_ [Modal](../Modal/README.md): it reuses the same native
|
|
6
|
+
[`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) +
|
|
7
|
+
`showModal()` machinery, so the inert background, top-layer stacking, and
|
|
8
|
+
`Esc`-to-close are handled by the browser, and the same JS `Tab`-wrap focus
|
|
9
|
+
trap (last ⇄ first) applies. The single addition is a **`side`** axis on
|
|
10
|
+
`Drawer.Content` that says which edge the panel enters from.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Drawer } from "@primitiv-ui/react";
|
|
14
|
+
|
|
15
|
+
<Drawer.Root>
|
|
16
|
+
<Drawer.Trigger>Filters</Drawer.Trigger>
|
|
17
|
+
<Drawer.Portal>
|
|
18
|
+
<Drawer.Overlay />
|
|
19
|
+
<Drawer.Content side="right">
|
|
20
|
+
<Drawer.Title>Filters</Drawer.Title>
|
|
21
|
+
<Drawer.Description>Narrow the results below.</Drawer.Description>
|
|
22
|
+
{/* body */}
|
|
23
|
+
<Drawer.Close>Done</Drawer.Close>
|
|
24
|
+
</Drawer.Content>
|
|
25
|
+
</Drawer.Portal>
|
|
26
|
+
</Drawer.Root>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Relationship to Modal
|
|
30
|
+
|
|
31
|
+
Drawer is a **thin composition over Modal** — every sub-component renders its
|
|
32
|
+
`Modal.*` counterpart and inherits its behaviour and props verbatim. There is no
|
|
33
|
+
separate state machine, context, or focus logic to learn or maintain: a fix to
|
|
34
|
+
Modal is a fix to Drawer. The _only_ behavioural surface that differs is
|
|
35
|
+
`Drawer.Content`'s `side` prop. If you need a centred dialog rather than an
|
|
36
|
+
edge-anchored one, use [Modal](../Modal/README.md) directly.
|
|
37
|
+
|
|
38
|
+
The visual difference — the panel spanning an edge and sliding in — is a
|
|
39
|
+
**styling concern**, driven by the `data-side` hook below, not by this headless
|
|
40
|
+
layer. Panel size is likewise a styling concern (a `size` variant on the styled
|
|
41
|
+
distribution), not a headless prop.
|
|
42
|
+
|
|
43
|
+
## Sub-components
|
|
44
|
+
|
|
45
|
+
| Export | Element / role | Notes |
|
|
46
|
+
| --------------------- | -------------------- | ------------------------------------------------------------------------------ |
|
|
47
|
+
| `Drawer.Root` | State owner | Uncontrolled (`defaultOpen`) or controlled (`open` + `onOpenChange`); exposes the imperative API via `ref` |
|
|
48
|
+
| `Drawer.Trigger` | `<button>` | `aria-haspopup="dialog"` / `aria-expanded` / `aria-controls`; `asChild` |
|
|
49
|
+
| `Drawer.Portal` | `createPortal` | `container?` (default `document.body`); `forceMount` |
|
|
50
|
+
| `Drawer.Overlay` | `<div>` (sibling) | Decorative backdrop — **not** the click-outside surface (see Modal); `asChild`, `forceMount` |
|
|
51
|
+
| `Drawer.Content` | `<dialog>` | The sliding panel; adds the `side` axis; escape hatches + auto-ARIA. `asChild` **not** supported |
|
|
52
|
+
| `Drawer.Title` | `<h2>` | Accessible name; auto-wires `aria-labelledby`; `asChild` |
|
|
53
|
+
| `Drawer.Description` | `<p>` | Accessible description; auto-wires `aria-describedby`; `asChild` |
|
|
54
|
+
| `Drawer.Close` | `<button>` | Composes `onClick` with close; `asChild` |
|
|
55
|
+
|
|
56
|
+
## The `side` axis
|
|
57
|
+
|
|
58
|
+
`Drawer.Content` accepts **`side`** — `"top" | "right" | "bottom" | "left"`,
|
|
59
|
+
default `"right"`. It is emitted verbatim as **`data-side`** on the `<dialog>`
|
|
60
|
+
and changes no behaviour, focus, or ARIA — it exists purely so the styling layer
|
|
61
|
+
can position and animate the panel against the right edge:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<Drawer.Content side="bottom">...</Drawer.Content>
|
|
65
|
+
// → <dialog data-side="bottom" data-state="open" ...>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```css
|
|
69
|
+
dialog[data-side="right"] {
|
|
70
|
+
inset: 0 0 0 auto;
|
|
71
|
+
block-size: 100dvh;
|
|
72
|
+
}
|
|
73
|
+
dialog[data-side="bottom"] {
|
|
74
|
+
inset: auto 0 0 0;
|
|
75
|
+
inline-size: 100vw;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## State, focus, keyboard, escape hatches
|
|
80
|
+
|
|
81
|
+
All identical to [Modal](../Modal/README.md) — see its README for the full
|
|
82
|
+
detail:
|
|
83
|
+
|
|
84
|
+
- **State modes** — uncontrolled `defaultOpen`, or controlled `open` +
|
|
85
|
+
`onOpenChange` (statically discriminated; mixing them is a type error).
|
|
86
|
+
- **Imperative API** — `ref.current?.open()` / `.close()` on `Drawer.Root`
|
|
87
|
+
(`DrawerImperativeApi`). In controlled mode these delegate to `onOpenChange`.
|
|
88
|
+
- **Keyboard** — `Esc` closes (native `cancel`); `Tab` / `Shift+Tab` wrap
|
|
89
|
+
focus at the dialog's boundaries (the JS trap inherited from `Modal.Content`,
|
|
90
|
+
since native `showModal()` doesn't cycle focus).
|
|
91
|
+
- **Escape hatches** — `onEscapeKeyDown` and `onPointerDownOutside` on
|
|
92
|
+
`Drawer.Content`; call `event.preventDefault()` to keep the drawer open.
|
|
93
|
+
- **Click-outside** — detected on the dialog itself (the `::backdrop` sits above
|
|
94
|
+
a sibling `Drawer.Overlay`), exactly as in Modal.
|
|
95
|
+
|
|
96
|
+
## Animation hooks
|
|
97
|
+
|
|
98
|
+
`Drawer.Portal` and `Drawer.Overlay` accept `forceMount` to keep the subtree in
|
|
99
|
+
the DOM while `open` is false, so a CSS slide-out can play against
|
|
100
|
+
`data-state="closed"` before unmount. Combine `data-state` with `data-side` to
|
|
101
|
+
drive direction-aware transforms:
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
dialog[data-side="right"][data-state="closed"] {
|
|
105
|
+
translate: 100% 0;
|
|
106
|
+
}
|
|
107
|
+
dialog[data-side="right"][data-state="open"] {
|
|
108
|
+
translate: 0 0;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Styling hooks
|
|
113
|
+
|
|
114
|
+
- `data-state="open" | "closed"` on `Drawer.Content` (and `Drawer.Overlay`).
|
|
115
|
+
- `data-side="top" | "right" | "bottom" | "left"` on `Drawer.Content`.
|
|
116
|
+
- `aria-expanded` on the trigger tracks the same open state.
|
|
117
|
+
|
|
118
|
+
## `asChild` composition
|
|
119
|
+
|
|
120
|
+
Every rendered sub-component except `Drawer.Content` accepts `asChild` to project
|
|
121
|
+
its behaviour onto a consumer element via [Slot](../Slot.tsx), exactly as in
|
|
122
|
+
Modal. `Drawer.Content` is intentionally not slot-able — its native `<dialog>` is
|
|
123
|
+
what provides the inert background and top layer (and hosts the inherited
|
|
124
|
+
Tab-wrap focus trap).
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import "./dialog-polyfill";
|
|
2
|
+
|
|
3
|
+
import { act, render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { createRef } from "react";
|
|
6
|
+
|
|
7
|
+
import { Drawer } from "../Drawer";
|
|
8
|
+
import type { DrawerImperativeApi } from "../types";
|
|
9
|
+
|
|
10
|
+
describe("Drawer — behaviour (delegated to Modal)", () => {
|
|
11
|
+
it("opens on trigger click and closes on Close click", async () => {
|
|
12
|
+
// Arrange
|
|
13
|
+
const user = userEvent.setup();
|
|
14
|
+
render(
|
|
15
|
+
<Drawer.Root>
|
|
16
|
+
<Drawer.Trigger>Open</Drawer.Trigger>
|
|
17
|
+
<Drawer.Portal>
|
|
18
|
+
<Drawer.Content data-testid="drawer">
|
|
19
|
+
<Drawer.Close>Done</Drawer.Close>
|
|
20
|
+
</Drawer.Content>
|
|
21
|
+
</Drawer.Portal>
|
|
22
|
+
</Drawer.Root>,
|
|
23
|
+
);
|
|
24
|
+
// closed → the portal renders nothing
|
|
25
|
+
expect(screen.queryByTestId("drawer")).toBeNull();
|
|
26
|
+
|
|
27
|
+
// Act — open
|
|
28
|
+
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
29
|
+
// Assert
|
|
30
|
+
expect(screen.getByTestId("drawer")).toHaveAttribute("open");
|
|
31
|
+
|
|
32
|
+
// Act — close
|
|
33
|
+
await user.click(screen.getByRole("button", { name: "Done" }));
|
|
34
|
+
// Assert
|
|
35
|
+
expect(screen.queryByTestId("drawer")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("defers to onOpenChange in controlled mode", async () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
const user = userEvent.setup();
|
|
41
|
+
const onOpenChange = vi.fn();
|
|
42
|
+
render(
|
|
43
|
+
<Drawer.Root open={false} onOpenChange={onOpenChange}>
|
|
44
|
+
<Drawer.Trigger>Open</Drawer.Trigger>
|
|
45
|
+
</Drawer.Root>,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
50
|
+
|
|
51
|
+
// Assert
|
|
52
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("exposes an imperative open/close handle on Root", () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const ref = createRef<DrawerImperativeApi>();
|
|
58
|
+
render(
|
|
59
|
+
<Drawer.Root ref={ref}>
|
|
60
|
+
<Drawer.Content data-testid="drawer">body</Drawer.Content>
|
|
61
|
+
</Drawer.Root>,
|
|
62
|
+
);
|
|
63
|
+
expect(screen.getByTestId("drawer")).not.toHaveAttribute("open");
|
|
64
|
+
|
|
65
|
+
// Act — open
|
|
66
|
+
act(() => ref.current?.open());
|
|
67
|
+
// Assert
|
|
68
|
+
expect(screen.getByTestId("drawer")).toHaveAttribute("open");
|
|
69
|
+
|
|
70
|
+
// Act — close
|
|
71
|
+
act(() => ref.current?.close());
|
|
72
|
+
// Assert
|
|
73
|
+
expect(screen.getByTestId("drawer")).not.toHaveAttribute("open");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("forwards a ref to the <dialog> and keeps forceMount mounted while closed", () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const ref = createRef<HTMLDialogElement>();
|
|
79
|
+
|
|
80
|
+
// Act
|
|
81
|
+
render(
|
|
82
|
+
<Drawer.Root>
|
|
83
|
+
<Drawer.Portal forceMount>
|
|
84
|
+
<Drawer.Overlay forceMount data-testid="overlay" />
|
|
85
|
+
<Drawer.Content ref={ref} data-testid="drawer">
|
|
86
|
+
body
|
|
87
|
+
</Drawer.Content>
|
|
88
|
+
</Drawer.Portal>
|
|
89
|
+
</Drawer.Root>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Assert — forceMount keeps the subtree in the DOM even though closed
|
|
93
|
+
expect(ref.current).toBe(screen.getByTestId("drawer"));
|
|
94
|
+
expect(ref.current?.tagName).toBe("DIALOG");
|
|
95
|
+
expect(screen.getByTestId("overlay")).toHaveAttribute(
|
|
96
|
+
"data-state",
|
|
97
|
+
"closed",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import "./dialog-polyfill";
|
|
2
|
+
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import { Drawer } from "../Drawer";
|
|
7
|
+
|
|
8
|
+
// Drawer.Content delegates wholesale to Modal.Content, so it inherits the
|
|
9
|
+
// boundary Tab-wrap focus trap. These two tests are the conformance guard that
|
|
10
|
+
// the inheritance holds — the exhaustive branch coverage lives in
|
|
11
|
+
// Modal.focus-trap.test.tsx. The Trigger sits outside the dialog so a native
|
|
12
|
+
// wrap (without the trap) would land there, not on the asserted element.
|
|
13
|
+
describe("Drawer focus trap — inherited from Modal.Content", () => {
|
|
14
|
+
it("wraps focus from the last element back to the first on Tab", async () => {
|
|
15
|
+
// Arrange
|
|
16
|
+
const user = userEvent.setup();
|
|
17
|
+
render(
|
|
18
|
+
<Drawer.Root defaultOpen>
|
|
19
|
+
<Drawer.Trigger>Open</Drawer.Trigger>
|
|
20
|
+
<Drawer.Content>
|
|
21
|
+
<button type="button">First</button>
|
|
22
|
+
<button type="button">Second</button>
|
|
23
|
+
</Drawer.Content>
|
|
24
|
+
</Drawer.Root>,
|
|
25
|
+
);
|
|
26
|
+
screen.getByRole("button", { name: "Second" }).focus();
|
|
27
|
+
|
|
28
|
+
// Act
|
|
29
|
+
await user.tab();
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
32
|
+
expect(screen.getByRole("button", { name: "First" })).toHaveFocus();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("wraps focus from the first element to the last on Shift+Tab", async () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const user = userEvent.setup();
|
|
38
|
+
render(
|
|
39
|
+
<Drawer.Root defaultOpen>
|
|
40
|
+
<Drawer.Trigger>Open</Drawer.Trigger>
|
|
41
|
+
<Drawer.Content>
|
|
42
|
+
<button type="button">First</button>
|
|
43
|
+
<button type="button">Second</button>
|
|
44
|
+
</Drawer.Content>
|
|
45
|
+
</Drawer.Root>,
|
|
46
|
+
);
|
|
47
|
+
screen.getByRole("button", { name: "First" }).focus();
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
await user.tab({ shift: true });
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
expect(screen.getByRole("button", { name: "Second" })).toHaveFocus();
|
|
54
|
+
});
|
|
55
|
+
});
|