@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
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
KeyboardEvent,
|
|
3
|
+
PointerEvent,
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
2
9
|
|
|
3
10
|
import type { MillerColumnsResizeHandleProps } from "../types";
|
|
4
11
|
|
|
@@ -9,12 +16,43 @@ type DragState = { startX: number; startWidth: number };
|
|
|
9
16
|
|
|
10
17
|
export function useMillerColumnsResizeHandle({
|
|
11
18
|
onPointerDown,
|
|
19
|
+
onKeyDown,
|
|
20
|
+
minWidth = 0,
|
|
21
|
+
maxWidth = Number.POSITIVE_INFINITY,
|
|
22
|
+
step = 10,
|
|
12
23
|
...rest
|
|
13
24
|
}: MillerColumnsResizeHandleProps) {
|
|
14
|
-
const { columnWidths, setColumnWidth } = useMillerColumnsContext();
|
|
25
|
+
const { columnWidths, setColumnWidth, dir } = useMillerColumnsContext();
|
|
15
26
|
const { depth } = useMillerColumnsColumnContext();
|
|
16
27
|
|
|
28
|
+
const handleRef = useRef<HTMLDivElement>(null);
|
|
17
29
|
const [drag, setDrag] = useState<DragState | null>(null);
|
|
30
|
+
// The column's rendered width before anything resizes it. Tracked so
|
|
31
|
+
// `aria-valuenow` is truthful from the first render — a splitter whose
|
|
32
|
+
// value only appears after the first drag announces nothing until then.
|
|
33
|
+
const [measured, setMeasured] = useState(0);
|
|
34
|
+
|
|
35
|
+
const clamp = (width: number) =>
|
|
36
|
+
Math.min(maxWidth, Math.max(minWidth, width));
|
|
37
|
+
|
|
38
|
+
// The resized width wins; otherwise the column is still sized by CSS.
|
|
39
|
+
const width = columnWidths.get(depth) ?? measured;
|
|
40
|
+
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
const column = handleRef.current?.closest<HTMLElement>(
|
|
43
|
+
"[data-miller-columns-column]",
|
|
44
|
+
);
|
|
45
|
+
/* c8 ignore next 3 -- the handle only renders inside a Column, which
|
|
46
|
+
context already enforces, so the ancestor is always present. */
|
|
47
|
+
if (!column) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const measure = () => setMeasured(column.getBoundingClientRect().width);
|
|
51
|
+
measure();
|
|
52
|
+
const observer = new ResizeObserver(measure);
|
|
53
|
+
observer.observe(column);
|
|
54
|
+
return () => observer.disconnect();
|
|
55
|
+
}, []);
|
|
18
56
|
|
|
19
57
|
// A drag is tracked on `window`, not the handle, so the pointer can
|
|
20
58
|
// travel anywhere once it is down; the listeners live only for the
|
|
@@ -27,13 +65,8 @@ export function useMillerColumnsResizeHandle({
|
|
|
27
65
|
const activeDrag = drag;
|
|
28
66
|
|
|
29
67
|
function handleMove(event: globalThis.PointerEvent) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Math.max(
|
|
33
|
-
0,
|
|
34
|
-
activeDrag.startWidth + event.clientX - activeDrag.startX,
|
|
35
|
-
),
|
|
36
|
-
);
|
|
68
|
+
const next = activeDrag.startWidth + event.clientX - activeDrag.startX;
|
|
69
|
+
setColumnWidth(depth, Math.min(maxWidth, Math.max(minWidth, next)));
|
|
37
70
|
}
|
|
38
71
|
|
|
39
72
|
function endDrag() {
|
|
@@ -48,7 +81,7 @@ export function useMillerColumnsResizeHandle({
|
|
|
48
81
|
window.removeEventListener("pointerup", endDrag);
|
|
49
82
|
window.removeEventListener("pointercancel", endDrag);
|
|
50
83
|
};
|
|
51
|
-
}, [drag, depth, setColumnWidth]);
|
|
84
|
+
}, [drag, depth, setColumnWidth, minWidth, maxWidth]);
|
|
52
85
|
|
|
53
86
|
function handlePointerDown(event: PointerEvent<HTMLDivElement>) {
|
|
54
87
|
onPointerDown?.(event);
|
|
@@ -63,13 +96,50 @@ export function useMillerColumnsResizeHandle({
|
|
|
63
96
|
setDrag({ startX: event.clientX, startWidth });
|
|
64
97
|
}
|
|
65
98
|
|
|
99
|
+
// The column extends towards the inline-end edge, which is leftwards
|
|
100
|
+
// under rtl — so the key that widens it mirrors with the direction.
|
|
101
|
+
const growKey = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
|
|
102
|
+
const shrinkKey = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
|
|
103
|
+
|
|
104
|
+
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
|
105
|
+
onKeyDown?.(event);
|
|
106
|
+
|
|
107
|
+
const next = (() => {
|
|
108
|
+
switch (event.key) {
|
|
109
|
+
case growKey:
|
|
110
|
+
return width + step;
|
|
111
|
+
case shrinkKey:
|
|
112
|
+
return width - step;
|
|
113
|
+
case "Home":
|
|
114
|
+
return minWidth;
|
|
115
|
+
case "End":
|
|
116
|
+
return maxWidth;
|
|
117
|
+
default:
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
|
|
122
|
+
if (next === null || !Number.isFinite(next)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
setColumnWidth(depth, clamp(next));
|
|
128
|
+
}
|
|
129
|
+
|
|
66
130
|
return {
|
|
67
131
|
handleProps: {
|
|
132
|
+
ref: handleRef,
|
|
68
133
|
role: "separator" as const,
|
|
69
134
|
"aria-orientation": "vertical" as const,
|
|
135
|
+
tabIndex: 0,
|
|
136
|
+
"aria-valuenow": Math.round(width),
|
|
137
|
+
"aria-valuemin": minWidth,
|
|
138
|
+
...(Number.isFinite(maxWidth) ? { "aria-valuemax": maxWidth } : {}),
|
|
70
139
|
"data-miller-columns-resize-handle": "",
|
|
71
140
|
...(drag ? { "data-dragging": "" } : {}),
|
|
72
141
|
onPointerDown: handlePointerDown,
|
|
142
|
+
onKeyDown: handleKeyDown,
|
|
73
143
|
...rest,
|
|
74
144
|
},
|
|
75
145
|
};
|
|
Binary file
|
|
@@ -1,77 +1,173 @@
|
|
|
1
|
-
import { ComponentProps, ReactNode, Ref } from "react";
|
|
1
|
+
import { ComponentProps, KeyboardEvent, ReactNode, Ref } from "react";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import type { Direction } from "../DirectionProvider/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared base for both {@link MillerColumnsRootProps} variants — the native
|
|
7
|
+
* `<div>` attributes (minus `ref`, which the strip owns internally, and
|
|
8
|
+
* `dir`, narrowed below to the {@link Direction} union).
|
|
9
|
+
*/
|
|
10
|
+
export type MillerColumnsRootBaseProps = Omit<
|
|
11
|
+
ComponentProps<"div">,
|
|
12
|
+
"ref" | "defaultValue" | "dir"
|
|
13
|
+
> & {
|
|
14
|
+
/**
|
|
15
|
+
* Reading direction. Under `"rtl"` the horizontal arrow pair is
|
|
16
|
+
* mirrored — ArrowLeft steps *into* a branch's child column and
|
|
17
|
+
* ArrowRight walks back out — so navigation follows the visual order.
|
|
18
|
+
* Inherited from a `DirectionProvider` ancestor when omitted.
|
|
19
|
+
* @default "ltr"
|
|
20
|
+
*/
|
|
21
|
+
dir?: Direction;
|
|
22
|
+
/**
|
|
23
|
+
* The strip's tree, authored recursively: root
|
|
24
|
+
* {@link MillerColumnsColumnProps | Column(s)} whose branch
|
|
25
|
+
* {@link MillerColumnsItemProps | Items} nest further Columns, plus an
|
|
26
|
+
* optional trailing {@link MillerColumnsPreviewPanelProps | PreviewPanel}.
|
|
27
|
+
* There is no `items` data prop — the JSX *is* the data.
|
|
28
|
+
*/
|
|
6
29
|
children: ReactNode;
|
|
7
30
|
};
|
|
8
31
|
|
|
9
32
|
/**
|
|
10
|
-
*
|
|
11
|
-
* selection path. Pass `defaultValue`
|
|
33
|
+
* Uncontrolled variant of {@link MillerColumnsRootProps}: the component owns
|
|
34
|
+
* the selection path. Pass `defaultValue` (or omit it to start empty).
|
|
12
35
|
*/
|
|
13
36
|
export type MillerColumnsRootUncontrolledProps = MillerColumnsRootBaseProps & {
|
|
14
|
-
/** Initial selection path
|
|
37
|
+
/** Initial selection path on first render, root column first (e.g.
|
|
38
|
+
* `["docs", "guides"]`). The component owns the path thereafter. */
|
|
15
39
|
defaultValue?: string[];
|
|
16
|
-
/** Forbidden in uncontrolled mode. */
|
|
40
|
+
/** Forbidden in uncontrolled mode — use `defaultValue` instead. */
|
|
17
41
|
value?: never;
|
|
18
|
-
/** Forbidden in uncontrolled mode
|
|
42
|
+
/** Forbidden in uncontrolled mode — pass `value` + `onValueChange` to
|
|
43
|
+
* control the component. */
|
|
19
44
|
onValueChange?: never;
|
|
20
45
|
};
|
|
21
46
|
|
|
22
47
|
/**
|
|
23
|
-
*
|
|
48
|
+
* Controlled variant of {@link MillerColumnsRootProps}: the parent owns the
|
|
24
49
|
* selection path. Pass `value` and `onValueChange` together.
|
|
25
50
|
*/
|
|
26
51
|
export type MillerColumnsRootControlledProps = MillerColumnsRootBaseProps & {
|
|
27
|
-
/** Forbidden in controlled mode. */
|
|
52
|
+
/** Forbidden in controlled mode — use `value` instead. */
|
|
28
53
|
defaultValue?: never;
|
|
29
|
-
/** The controlled selection path, root column first.
|
|
54
|
+
/** The controlled selection path, root column first. Must be kept in sync
|
|
55
|
+
* by the parent via `onValueChange`. */
|
|
30
56
|
value: string[];
|
|
31
|
-
/** Called with the new path whenever the selection changes.
|
|
57
|
+
/** Called with the new selection path whenever the selection changes.
|
|
58
|
+
* Required in controlled mode. */
|
|
32
59
|
onValueChange: (path: string[]) => void;
|
|
33
60
|
};
|
|
34
61
|
|
|
35
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Props for {@link MillerColumnsRoot | `MillerColumns.Root`}.
|
|
64
|
+
*
|
|
65
|
+
* Resolves to either {@link MillerColumnsRootUncontrolledProps} or
|
|
66
|
+
* {@link MillerColumnsRootControlledProps} — only one shape is accepted by
|
|
67
|
+
* TypeScript at a time.
|
|
68
|
+
*/
|
|
36
69
|
export type MillerColumnsRootProps =
|
|
37
70
|
| MillerColumnsRootUncontrolledProps
|
|
38
71
|
| MillerColumnsRootControlledProps;
|
|
39
72
|
|
|
40
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Props for {@link MillerColumnsColumn | `MillerColumns.Column`}, a single
|
|
75
|
+
* vertical list. Rendered element is portal-projected into the Root strip.
|
|
76
|
+
*/
|
|
41
77
|
export type MillerColumnsColumnProps = ComponentProps<"div"> & {
|
|
42
|
-
/** The
|
|
43
|
-
|
|
78
|
+
/** The column's {@link MillerColumnsItemProps | Items} and, optionally, a
|
|
79
|
+
* {@link MillerColumnsResizeHandleProps | ResizeHandle}. Optional: a
|
|
80
|
+
* childless branch legitimately opens an empty column, which carries
|
|
81
|
+
* `data-empty` for the styling layer. */
|
|
82
|
+
children?: ReactNode;
|
|
44
83
|
};
|
|
45
84
|
|
|
46
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Props for {@link MillerColumnsItem | `MillerColumns.Item`}, a single
|
|
87
|
+
* selectable node within a column.
|
|
88
|
+
*
|
|
89
|
+
* Generic over the rendered element type so `asChild` consumers can type
|
|
90
|
+
* the forwarded `ref` (e.g. `MillerColumns.Item<HTMLAnchorElement>`).
|
|
91
|
+
*/
|
|
47
92
|
export type MillerColumnsItemProps<T extends HTMLElement = HTMLDivElement> =
|
|
48
93
|
Omit<ComponentProps<"div">, "ref"> & {
|
|
49
|
-
/** Stable identifier used to match this item against the selection path.
|
|
94
|
+
/** Stable identifier used to match this item against the selection path.
|
|
95
|
+
* The item is selected when the path entry at its depth equals `value`. */
|
|
50
96
|
value: string;
|
|
51
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* When `true`, the item renders `aria-disabled` / `data-disabled` and is
|
|
99
|
+
* skipped by roving navigation — it cannot be selected, focused, or
|
|
100
|
+
* activated.
|
|
101
|
+
* @default false
|
|
102
|
+
*/
|
|
52
103
|
disabled?: boolean;
|
|
53
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Render the cell as a single consumer-supplied child element (e.g. an
|
|
106
|
+
* `<a>`) instead of the default `<div>`, merging the treeitem ARIA,
|
|
107
|
+
* handlers, and ref onto it via the {@link Slot} pattern. Any nested
|
|
108
|
+
* `<MillerColumns.Column>` remains a sibling of the cell element.
|
|
109
|
+
* @default false
|
|
110
|
+
*/
|
|
54
111
|
asChild?: boolean;
|
|
55
|
-
/**
|
|
112
|
+
/** The cell content and, for a branch item, a single nested
|
|
113
|
+
* `<MillerColumns.Column>` (split out by `partitionItemChildren`). */
|
|
56
114
|
children: ReactNode;
|
|
57
|
-
/**
|
|
58
|
-
* `asChild`, specify the child's element type.
|
|
115
|
+
/** Forwarded to the rendered element. Defaults to `HTMLDivElement`; when
|
|
116
|
+
* using `asChild`, specify the child's element type. Composed with the
|
|
117
|
+
* library's internal ref. */
|
|
59
118
|
ref?: Ref<T>;
|
|
60
119
|
};
|
|
61
120
|
|
|
62
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Props for {@link MillerColumnsItemIndicator | `MillerColumns.ItemIndicator`},
|
|
123
|
+
* the branch chevron affordance (renders nothing for leaf items).
|
|
124
|
+
*/
|
|
63
125
|
export type MillerColumnsItemIndicatorProps = ComponentProps<"span"> & {
|
|
64
|
-
/** The glyph to render (
|
|
126
|
+
/** The glyph to render (e.g. `▸`). Optional. */
|
|
65
127
|
children?: ReactNode;
|
|
66
128
|
};
|
|
67
129
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Props for {@link MillerColumnsResizeHandle | `MillerColumns.ResizeHandle`},
|
|
132
|
+
* the drag-to-resize separator for its containing column.
|
|
133
|
+
*/
|
|
134
|
+
export type MillerColumnsResizeHandleProps = ComponentProps<"div"> & {
|
|
135
|
+
/**
|
|
136
|
+
* Smallest width the column can be dragged or stepped to, in pixels.
|
|
137
|
+
* Also published as `aria-valuemin`.
|
|
138
|
+
* @default 0
|
|
139
|
+
*/
|
|
140
|
+
minWidth?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Largest width the column can be dragged or stepped to, in pixels.
|
|
143
|
+
* Published as `aria-valuemax` when finite; left unbounded by default,
|
|
144
|
+
* in which case the attribute is omitted.
|
|
145
|
+
* @default Infinity
|
|
146
|
+
*/
|
|
147
|
+
maxWidth?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Pixels moved per arrow-key press.
|
|
150
|
+
* @default 10
|
|
151
|
+
*/
|
|
152
|
+
step?: number;
|
|
153
|
+
};
|
|
70
154
|
|
|
71
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Props for {@link MillerColumnsPreviewPanel | `MillerColumns.PreviewPanel`},
|
|
157
|
+
* the trailing preview pane.
|
|
158
|
+
*/
|
|
72
159
|
export type MillerColumnsPreviewPanelProps = ComponentProps<"div"> & {
|
|
73
|
-
/** The preview content for the current selection
|
|
160
|
+
/** The preview content for the current selection (typically driven by
|
|
161
|
+
* {@link useMillerColumnsSelection}). */
|
|
74
162
|
children?: ReactNode;
|
|
163
|
+
/**
|
|
164
|
+
* Keep the panel mounted regardless of the selection, instead of following
|
|
165
|
+
* Finder's rule (a preview only where a selected *leaf* would open a child
|
|
166
|
+
* column). Use it for a persistent inspector, or to animate the panel in
|
|
167
|
+
* and out.
|
|
168
|
+
* @default false
|
|
169
|
+
*/
|
|
170
|
+
forceMount?: boolean;
|
|
75
171
|
};
|
|
76
172
|
|
|
77
173
|
/** The current selection, as returned by `useMillerColumnsSelection`. */
|
|
@@ -100,6 +196,8 @@ export type MillerColumnsItemAddress = {
|
|
|
100
196
|
|
|
101
197
|
/** Value shared through the root MillerColumns context. */
|
|
102
198
|
export type MillerColumnsContextValue = {
|
|
199
|
+
/** The resolved reading direction — the `dir` prop, else the inherited one. */
|
|
200
|
+
dir: Direction;
|
|
103
201
|
/** Portal slots keyed by column depth. */
|
|
104
202
|
slots: Map<number, HTMLElement>;
|
|
105
203
|
/** Per-depth column widths set via the resize handle. */
|
|
@@ -116,9 +214,15 @@ export type MillerColumnsContextValue = {
|
|
|
116
214
|
value: string,
|
|
117
215
|
element: HTMLElement | null,
|
|
118
216
|
disabled: boolean,
|
|
217
|
+
hasChildren: boolean,
|
|
119
218
|
) => void;
|
|
219
|
+
/** Whether the deepest selected item is a leaf — Finder's rule for whether a
|
|
220
|
+
* preview occupies the slot after it. `false` when nothing is selected. */
|
|
221
|
+
terminalIsLeaf: boolean;
|
|
120
222
|
/** Get the registered items for a column. */
|
|
121
223
|
getColumnItems: (depth: number) => MillerColumnsItemMeta[];
|
|
224
|
+
/** Read a registered item's rendered cell text, for typeahead matching. */
|
|
225
|
+
getItemLabel: (depth: number, value: string) => string;
|
|
122
226
|
/** Move focus to the given item. */
|
|
123
227
|
focusItem: (depth: number, value: string) => void;
|
|
124
228
|
/** Focus the first item in a column; returns whether one was focused. */
|
|
@@ -129,6 +233,8 @@ export type MillerColumnsContextValue = {
|
|
|
129
233
|
activeItem: MillerColumnsItemAddress | null;
|
|
130
234
|
/** Set the active (focused) item. */
|
|
131
235
|
setActiveItem: (item: MillerColumnsItemAddress) => void;
|
|
236
|
+
/** Printable-character typeahead, scoped to the focused item's column. */
|
|
237
|
+
handleTypeahead: (event: KeyboardEvent<HTMLElement>) => void;
|
|
132
238
|
};
|
|
133
239
|
|
|
134
240
|
/** Value shared through a column's context. */
|
|
@@ -23,11 +23,77 @@ export function selectAtDepth(
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Splits a props bag into its `aria-*` entries and everything else.
|
|
27
|
+
*
|
|
28
|
+
* `Root` renders two elements — a plain strip container and the inner
|
|
29
|
+
* `role="tree"` widget — so one props bag has two possible homes. ARIA
|
|
30
|
+
* belongs on the widget (an `aria-label` must name the tree, not its
|
|
31
|
+
* scroll container); `className`, `id`, `style` and the rest belong on
|
|
32
|
+
* the container the consumer actually lays out.
|
|
33
|
+
*/
|
|
34
|
+
export function partitionAriaProps(props: Record<string, unknown>): {
|
|
35
|
+
aria: Record<string, unknown>;
|
|
36
|
+
rest: Record<string, unknown>;
|
|
37
|
+
} {
|
|
38
|
+
const aria: Record<string, unknown> = {};
|
|
39
|
+
const rest: Record<string, unknown> = {};
|
|
40
|
+
|
|
41
|
+
for (const [key, value] of Object.entries(props)) {
|
|
42
|
+
if (key.startsWith("aria-")) {
|
|
43
|
+
aria[key] = value;
|
|
44
|
+
} else {
|
|
45
|
+
rest[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { aria, rest };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Marker a styling layer sets on a component that renders a `MillerColumns`
|
|
54
|
+
* part, so this module recognises the wrapper as that part.
|
|
55
|
+
*
|
|
56
|
+
* `Item` inspects its children **before** rendering them, so it sees the
|
|
57
|
+
* wrapper component rather than the `MillerColumns.Column` the wrapper
|
|
58
|
+
* returns. Without a marker a wrapped child column is mis-read as cell
|
|
59
|
+
* content: the item stops being a branch (no `aria-expanded`, no chevron, no
|
|
60
|
+
* ArrowRight) and — worse — the column projects into whatever depth slot
|
|
61
|
+
* happens to be open, so selecting *one* branch reveals *every* sibling's
|
|
62
|
+
* children at once.
|
|
63
|
+
*
|
|
64
|
+
* ```tsx
|
|
65
|
+
* function StyledColumn(props: MillerColumnsColumnProps) {
|
|
66
|
+
* return <MillerColumns.Column {...props} className="column" />;
|
|
67
|
+
* }
|
|
68
|
+
* StyledColumn[MILLER_COLUMNS_PART] = "column";
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export const MILLER_COLUMNS_PART: "__primitivMillerColumnsPart" =
|
|
72
|
+
"__primitivMillerColumnsPart";
|
|
73
|
+
|
|
74
|
+
/** The part names {@link MILLER_COLUMNS_PART} accepts. */
|
|
75
|
+
export type MillerColumnsPartName = "column";
|
|
76
|
+
|
|
77
|
+
/** Read a component's {@link MILLER_COLUMNS_PART} marker, if it declares one. */
|
|
78
|
+
function millerColumnsPartOf(
|
|
79
|
+
type: unknown,
|
|
80
|
+
): MillerColumnsPartName | undefined {
|
|
81
|
+
return (type as Record<string, MillerColumnsPartName | undefined> | null)?.[
|
|
82
|
+
MILLER_COLUMNS_PART
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether `node` is a `MillerColumns.Column` element — either the part itself,
|
|
88
|
+
* or a component that marks itself as standing in for it (see
|
|
89
|
+
* {@link MILLER_COLUMNS_PART}).
|
|
28
90
|
*/
|
|
29
91
|
export function isColumnElement(node: ReactNode): node is ReactElement {
|
|
30
|
-
return
|
|
92
|
+
return (
|
|
93
|
+
isValidElement(node) &&
|
|
94
|
+
(node.type === MillerColumnsColumn ||
|
|
95
|
+
millerColumnsPartOf(node.type) === "column")
|
|
96
|
+
);
|
|
31
97
|
}
|
|
32
98
|
|
|
33
99
|
/**
|
package/src/Modal/Modal.tsx
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
* @example Uncontrolled
|
|
52
52
|
* ```tsx
|
|
53
53
|
* <Modal.Root defaultOpen>
|
|
54
|
-
* <Modal.Content
|
|
54
|
+
* <Modal.Content>...</Modal.Content>
|
|
55
55
|
* </Modal.Root>
|
|
56
56
|
* ```
|
|
57
57
|
*
|
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
* <Modal.Trigger>Open</Modal.Trigger>
|
|
64
64
|
* <Modal.Portal>
|
|
65
65
|
* <Modal.Overlay />
|
|
66
|
-
* <Modal.Content
|
|
66
|
+
* <Modal.Content>...</Modal.Content>
|
|
67
67
|
* </Modal.Portal>
|
|
68
68
|
* </Modal.Root>
|
|
69
69
|
* ```
|
|
@@ -97,6 +97,8 @@ ModalRoot.displayName = "ModalRoot";
|
|
|
97
97
|
* element (e.g. a router `<Link>`) with the trigger's ARIA attributes,
|
|
98
98
|
* composed event handlers, and ref merged in.
|
|
99
99
|
*
|
|
100
|
+
* @extends HTMLButtonElement
|
|
101
|
+
*
|
|
100
102
|
* @example
|
|
101
103
|
* ```tsx
|
|
102
104
|
* <Modal.Trigger>Open</Modal.Trigger>
|
|
@@ -138,7 +140,7 @@ ModalTrigger.displayName = "ModalTrigger";
|
|
|
138
140
|
* ```tsx
|
|
139
141
|
* <Modal.Portal container={document.getElementById("modal-root")!}>
|
|
140
142
|
* <Modal.Overlay />
|
|
141
|
-
* <Modal.Content
|
|
143
|
+
* <Modal.Content>...</Modal.Content>
|
|
142
144
|
* </Modal.Portal>
|
|
143
145
|
* ```
|
|
144
146
|
*/
|
|
@@ -177,6 +179,8 @@ ModalPortal.displayName = "ModalPortal";
|
|
|
177
179
|
* **`forceMount` prop.** Pass `forceMount` to keep the overlay in the
|
|
178
180
|
* DOM while `open` is false so a CSS exit animation can play.
|
|
179
181
|
*
|
|
182
|
+
* @extends HTMLDivElement
|
|
183
|
+
*
|
|
180
184
|
* @example
|
|
181
185
|
* ```tsx
|
|
182
186
|
* <Modal.Overlay />
|
|
@@ -215,8 +219,15 @@ ModalOverlay.displayName = "ModalOverlay";
|
|
|
215
219
|
/**
|
|
216
220
|
* The native `<dialog>` element. React drives its `showModal()` and
|
|
217
221
|
* `close()` calls in response to `open` state changes, letting the
|
|
218
|
-
* browser own
|
|
219
|
-
*
|
|
222
|
+
* browser own the inert background, top-layer rendering, and the `Esc`
|
|
223
|
+
* key.
|
|
224
|
+
*
|
|
225
|
+
* **Focus trapping.** `showModal()` inerts the background but does
|
|
226
|
+
* *not* cycle focus — `Tab` past the last element escapes to the
|
|
227
|
+
* browser chrome, then wraps back in. To conform to the WAI-ARIA APG
|
|
228
|
+
* Modal Dialog pattern, a `keydown` handler wraps `Tab` (last → first)
|
|
229
|
+
* and `Shift+Tab` (first → last); interior Tabs are left to the
|
|
230
|
+
* browser's native, inert-aware order.
|
|
220
231
|
*
|
|
221
232
|
* **ARIA wiring.** `aria-labelledby` and `aria-describedby` are set
|
|
222
233
|
* automatically when `Modal.Title` and `Modal.Description` are rendered
|
|
@@ -234,11 +245,21 @@ ModalOverlay.displayName = "ModalOverlay";
|
|
|
234
245
|
* `PointerEvent` is passed through, not a synthetic React event.
|
|
235
246
|
*
|
|
236
247
|
* **`asChild` is intentionally not supported.** The native dialog
|
|
237
|
-
* primitive is what provides the
|
|
238
|
-
* swapping it for a
|
|
248
|
+
* primitive is what provides the inert background and top-layer
|
|
249
|
+
* stacking (and hosts the Tab-wrap handler above); swapping it for a
|
|
250
|
+
* `<div>` would break all of it.
|
|
239
251
|
*
|
|
240
252
|
* **Styling hook.** `data-state="open" | "closed"` on the dialog.
|
|
241
253
|
*
|
|
254
|
+
* **Nested modals.** A `Modal.Root` may be rendered inside another modal's
|
|
255
|
+
* `Modal.Content`. Each root owns its own {@link ModalContextValue | context}
|
|
256
|
+
* and dialog, so every dialog stacks independently in the browser's top
|
|
257
|
+
* layer and each dismissal gesture (Close, Esc, click-outside) resolves
|
|
258
|
+
* against the nearest dialog only — closing the inner modal leaves the
|
|
259
|
+
* outer one open.
|
|
260
|
+
*
|
|
261
|
+
* @extends HTMLDialogElement
|
|
262
|
+
*
|
|
242
263
|
* @example
|
|
243
264
|
* ```tsx
|
|
244
265
|
* <Modal.Content
|
|
@@ -246,7 +267,7 @@ ModalOverlay.displayName = "ModalOverlay";
|
|
|
246
267
|
* if (hasUnsavedChanges) event.preventDefault();
|
|
247
268
|
* }}
|
|
248
269
|
* >
|
|
249
|
-
*
|
|
270
|
+
* ...
|
|
250
271
|
* </Modal.Content>
|
|
251
272
|
* ```
|
|
252
273
|
*/
|
|
@@ -293,6 +314,8 @@ ModalContent.displayName = "ModalContent";
|
|
|
293
314
|
* an `<h3>` for a nested dialog or a styled heading component); the
|
|
294
315
|
* id is still registered.
|
|
295
316
|
*
|
|
317
|
+
* @extends HTMLHeadingElement
|
|
318
|
+
*
|
|
296
319
|
* @example
|
|
297
320
|
* ```tsx
|
|
298
321
|
* <Modal.Title>Payment</Modal.Title>
|
|
@@ -340,6 +363,8 @@ ModalTitle.displayName = "ModalTitle";
|
|
|
340
363
|
* Pass `asChild` to render any consumer-supplied element; the id is
|
|
341
364
|
* still registered.
|
|
342
365
|
*
|
|
366
|
+
* @extends HTMLParagraphElement
|
|
367
|
+
*
|
|
343
368
|
* @example
|
|
344
369
|
* ```tsx
|
|
345
370
|
* <Modal.Description>
|
|
@@ -386,6 +411,8 @@ ModalDescription.displayName = "ModalDescription";
|
|
|
386
411
|
* text link or an icon-only `<button>` of your own) with the close
|
|
387
412
|
* behaviour merged in.
|
|
388
413
|
*
|
|
414
|
+
* @extends HTMLButtonElement
|
|
415
|
+
*
|
|
389
416
|
* @example
|
|
390
417
|
* ```tsx
|
|
391
418
|
* <Modal.Close>Cancel</Modal.Close>
|
package/src/Modal/README.md
CHANGED
|
@@ -5,10 +5,16 @@ A headless, accessible compound component implementing the
|
|
|
5
5
|
|
|
6
6
|
Modal is built on the native
|
|
7
7
|
[`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)
|
|
8
|
-
element and its `showModal()` API, so
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
element and its `showModal()` API, so the inert background, top-layer
|
|
9
|
+
stacking, and Esc-to-close are handled by the browser. The React layer
|
|
10
|
+
adds what native `<dialog>` doesn't give you:
|
|
11
|
+
|
|
12
|
+
- **Focus trapping.** `showModal()` inerts the background but does
|
|
13
|
+
_not_ cycle focus — Tab past the last element escapes to the browser
|
|
14
|
+
chrome and back. A `keydown` handler on the dialog wraps `Tab`
|
|
15
|
+
(last → first) and `Shift+Tab` (first → last) so focus stays inside,
|
|
16
|
+
as the WAI-ARIA APG requires. Interior Tabs stay native, so the
|
|
17
|
+
browser's inert-aware order (radio groups, etc.) is untouched.
|
|
12
18
|
- Click-outside-to-close via a `pointerdown` listener on the dialog
|
|
13
19
|
that checks the pointer against `getBoundingClientRect()` — coords
|
|
14
20
|
outside the rect mean the pointer landed on the native `::backdrop`.
|
|
@@ -68,10 +74,11 @@ README for the canonical composed example; nothing below changes.
|
|
|
68
74
|
|
|
69
75
|
## Keyboard interaction
|
|
70
76
|
|
|
71
|
-
| Key
|
|
72
|
-
|
|
|
73
|
-
| `Esc`
|
|
74
|
-
| `Tab`
|
|
77
|
+
| Key | Behaviour |
|
|
78
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `Esc` | Closes the modal (native `cancel` event). Preventable via `onEscapeKeyDown` |
|
|
80
|
+
| `Tab` | Wraps from the last focusable back to the first — focus stays inside the dialog. `showModal()` inerts the background but doesn't cycle focus, so this wrap is added by a `keydown` handler |
|
|
81
|
+
| `Shift+Tab` | Wraps from the first focusable to the last |
|
|
75
82
|
|
|
76
83
|
## State modes
|
|
77
84
|
|
|
@@ -83,12 +90,12 @@ rejects mixing them.
|
|
|
83
90
|
|
|
84
91
|
```tsx
|
|
85
92
|
// Uncontrolled
|
|
86
|
-
<Modal.Root defaultOpen
|
|
93
|
+
<Modal.Root defaultOpen>...</Modal.Root>;
|
|
87
94
|
|
|
88
95
|
// Controlled
|
|
89
96
|
const [open, setOpen] = useState(false);
|
|
90
97
|
<Modal.Root open={open} onOpenChange={setOpen}>
|
|
91
|
-
|
|
98
|
+
...
|
|
92
99
|
</Modal.Root>;
|
|
93
100
|
```
|
|
94
101
|
|
|
@@ -108,7 +115,7 @@ open. `onPointerDownOutside` receives the native `PointerEvent`
|
|
|
108
115
|
if (isRequiredFlow) event.preventDefault();
|
|
109
116
|
}}
|
|
110
117
|
>
|
|
111
|
-
|
|
118
|
+
...
|
|
112
119
|
</Modal.Content>
|
|
113
120
|
```
|
|
114
121
|
|
|
@@ -119,7 +126,7 @@ import { Modal, type ModalImperativeApi } from "@primitiv-ui/react";
|
|
|
119
126
|
|
|
120
127
|
const ref = useRef<ModalImperativeApi>(null);
|
|
121
128
|
|
|
122
|
-
<Modal.Root ref={ref}
|
|
129
|
+
<Modal.Root ref={ref}>...</Modal.Root>;
|
|
123
130
|
|
|
124
131
|
ref.current?.open();
|
|
125
132
|
ref.current?.close();
|
|
@@ -156,8 +163,10 @@ handlers, so only its ARIA and `data-state` are forwarded.
|
|
|
156
163
|
```
|
|
157
164
|
|
|
158
165
|
`Modal.Content` is intentionally **not** slot-able. Its native
|
|
159
|
-
`<dialog>` element
|
|
160
|
-
|
|
166
|
+
`<dialog>` element — the inert background and top-layer stacking — is
|
|
167
|
+
the whole reason Modal needs no scroll-lock library and only a minimal
|
|
168
|
+
Tab-wrap (rather than a full focus-trap library), so we don't expose a
|
|
169
|
+
way to swap it.
|
|
161
170
|
|
|
162
171
|
## Animation hooks
|
|
163
172
|
|
|
@@ -168,7 +177,7 @@ animation can play against the `data-state="closed"` attribute:
|
|
|
168
177
|
```tsx
|
|
169
178
|
<Modal.Portal forceMount>
|
|
170
179
|
<Modal.Overlay forceMount />
|
|
171
|
-
<Modal.Content
|
|
180
|
+
<Modal.Content>...</Modal.Content>
|
|
172
181
|
</Modal.Portal>
|
|
173
182
|
```
|
|
174
183
|
|