@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
|
@@ -61,7 +61,7 @@ are ever rendered.
|
|
|
61
61
|
| `MillerColumns.Column` | List | A vertical list of items, projected into the strip as `role="group"` |
|
|
62
62
|
| `MillerColumns.Item` | Tree node | A `role="treeitem"`; branch when it nests a `Column`. Supports `disabled`, `asChild`, `ref` |
|
|
63
63
|
| `MillerColumns.ItemIndicator` | Icon wrapper | Decorative `aria-hidden` icon, rendered only for branch items |
|
|
64
|
-
| `MillerColumns.ResizeHandle` | Resize grip | A `role="separator"` drag
|
|
64
|
+
| `MillerColumns.ResizeHandle` | Resize grip | A focusable `role="separator"` window splitter — drag or arrow-key it to set its column's width |
|
|
65
65
|
| `MillerColumns.PreviewPanel` | Preview pane | A content-agnostic trailing panel; pair with `useMillerColumnsSelection` |
|
|
66
66
|
|
|
67
67
|
## Selection model
|
|
@@ -80,7 +80,20 @@ column deeper than _d_ closes.
|
|
|
80
80
|
The two shapes are discriminated at the type level: passing
|
|
81
81
|
`defaultValue` alongside `value` is a type error.
|
|
82
82
|
|
|
83
|
-
There is no multi-select
|
|
83
|
+
**Single path only, by decision.** There is no multi-select and there is
|
|
84
|
+
not going to be one — this is settled, not a v1 shortfall awaiting a
|
|
85
|
+
follow-up. A Miller strip's whole premise is that the selection *is* the
|
|
86
|
+
path: column N+1 exists because exactly one item in column N is chosen.
|
|
87
|
+
Two selections in a column have no defined answer for what the next
|
|
88
|
+
column should show. A consumer needing to mark many nodes should keep
|
|
89
|
+
that set themselves and render it as content inside each `Item` (a
|
|
90
|
+
checkbox, a badge), leaving the path to mean navigation.
|
|
91
|
+
|
|
92
|
+
Every item on the path renders `data-state="selected"`, including the
|
|
93
|
+
ancestors the path merely passes through. The **deepest** one also gets
|
|
94
|
+
`data-terminal`. Style the two apart — with a uniform selected state
|
|
95
|
+
there is no way to tell which of several highlighted rows was actually
|
|
96
|
+
clicked.
|
|
84
97
|
|
|
85
98
|
## Keyboard interaction
|
|
86
99
|
|
|
@@ -92,6 +105,18 @@ There is no multi-select — only one path is active at a time.
|
|
|
92
105
|
| `ArrowRight` | Branch: select it and move focus to its child column's first item; leaf: no-op |
|
|
93
106
|
| `ArrowLeft` | Move focus to the selected item of the parent column |
|
|
94
107
|
| `Tab` | Move into / out of the whole tree (single tabstop) |
|
|
108
|
+
| Printable character | Typeahead — focus the first item in the column whose label matches |
|
|
109
|
+
|
|
110
|
+
Under `dir="rtl"` the horizontal pair is mirrored: `ArrowLeft` steps
|
|
111
|
+
*into* a branch's child column and `ArrowRight` walks back out, so the
|
|
112
|
+
step-in key always points at the column it opens. `dir` is inherited
|
|
113
|
+
from a `DirectionProvider` ancestor when omitted.
|
|
114
|
+
|
|
115
|
+
Typeahead accumulates keystrokes into a prefix query that resets after
|
|
116
|
+
500 ms of inactivity; repeating one character cycles through the items
|
|
117
|
+
starting with it. The query is scoped to the **focused item's own
|
|
118
|
+
column** — each column is an independent list, so typing never jumps the
|
|
119
|
+
cursor sideways.
|
|
95
120
|
|
|
96
121
|
The strip is a single roving-tabindex widget: exactly one item is
|
|
97
122
|
tabbable at a time. The tabstop follows the last-focused item and
|
|
@@ -109,7 +134,20 @@ effect when the strip is itself an overflow container; give
|
|
|
109
134
|
|
|
110
135
|
## ARIA
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
`Root` renders **two** elements: an outer strip container you lay out and
|
|
138
|
+
scroll, and an inner `role="tree"` widget holding only the columns. A
|
|
139
|
+
tree may own nothing but `treeitem` and `group`, so the `PreviewPanel`
|
|
140
|
+
sits outside it.
|
|
141
|
+
|
|
142
|
+
Because of that split, **`aria-*` props go to the tree widget** and every
|
|
143
|
+
other prop (`className`, `id`, `style`, `data-*`) goes to the strip
|
|
144
|
+
container:
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<MillerColumns.Root aria-label="Files" className="strip">
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- The tree widget is `role="tree"`.
|
|
113
151
|
- Each `Column` is `role="group"`.
|
|
114
152
|
- Each `Item` is `role="treeitem"` with `aria-level` (1-based column
|
|
115
153
|
depth), `aria-selected`, and — on branch items — `aria-expanded`.
|
|
@@ -156,10 +194,42 @@ it to the column's trailing edge yourself:
|
|
|
156
194
|
```
|
|
157
195
|
|
|
158
196
|
The first drag measures the column's current rendered width and
|
|
159
|
-
resizes from there; later drags resume from the last resized width.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
197
|
+
resizes from there; later drags resume from the last resized width.
|
|
198
|
+
|
|
199
|
+
The handle is a full WAI-ARIA **window splitter**: it is focusable, and
|
|
200
|
+
arrow keys resize it. Pass `minWidth` / `maxWidth` / `step` to bound and
|
|
201
|
+
pace that (they clamp pointer drags too, so CSS `min-width` is no longer
|
|
202
|
+
the only floor).
|
|
203
|
+
|
|
204
|
+
| Prop | Default | Effect |
|
|
205
|
+
| ---------- | ---------- | -------------------------------------------------- |
|
|
206
|
+
| `minWidth` | `0` | Floor for drag and keys; published as `aria-valuemin` |
|
|
207
|
+
| `maxWidth` | `Infinity` | Ceiling; published as `aria-valuemax` only when finite |
|
|
208
|
+
| `step` | `10` | Pixels moved per arrow-key press |
|
|
209
|
+
|
|
210
|
+
| Key | Behaviour |
|
|
211
|
+
| ------------------------- | ---------------------------------- |
|
|
212
|
+
| Inline-end arrow | Widen the column by `step` |
|
|
213
|
+
| Inline-start arrow | Narrow the column by `step` |
|
|
214
|
+
| `Home` / `End` | Jump to `minWidth` / `maxWidth` |
|
|
215
|
+
|
|
216
|
+
The arrow pair mirrors under `dir="rtl"`. `End` does nothing while
|
|
217
|
+
`maxWidth` is unbounded — there is no largest width to jump to.
|
|
218
|
+
|
|
219
|
+
`aria-valuenow` tracks the column's real rendered width via a
|
|
220
|
+
`ResizeObserver`, so it is correct before the first resize and follows
|
|
221
|
+
CSS- or container-driven changes as well.
|
|
222
|
+
|
|
223
|
+
**Give the handle an accessible name** — without one it announces as an
|
|
224
|
+
unnamed separator:
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
<MillerColumns.ResizeHandle
|
|
228
|
+
aria-label="Resize column"
|
|
229
|
+
minWidth={120}
|
|
230
|
+
maxWidth={400}
|
|
231
|
+
/>
|
|
232
|
+
```
|
|
163
233
|
|
|
164
234
|
## Preview panel
|
|
165
235
|
|
|
@@ -199,10 +269,11 @@ function FilePreview() {
|
|
|
199
269
|
both controlled and uncontrolled roots, and throws if called outside
|
|
200
270
|
`MillerColumns.Root`.
|
|
201
271
|
|
|
202
|
-
The panel ships with no ARIA role
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
272
|
+
The panel ships with no ARIA role, and renders *outside* the inner
|
|
273
|
+
`role="tree"` widget — a tree may own only `treeitem`s and `group`s, so
|
|
274
|
+
the panel could not legally sit inside it. Give the panel content its
|
|
275
|
+
own labelled landmark (`role`, `aria-label`, ...) through props if the
|
|
276
|
+
preview warrants being announced.
|
|
206
277
|
|
|
207
278
|
## `asChild` composition
|
|
208
279
|
|
|
@@ -216,7 +287,7 @@ element:
|
|
|
216
287
|
```tsx
|
|
217
288
|
<MillerColumns.Item<HTMLAnchorElement> asChild value="docs">
|
|
218
289
|
<a href="#docs">Docs</a>
|
|
219
|
-
<MillerColumns.Column
|
|
290
|
+
<MillerColumns.Column>...</MillerColumns.Column>
|
|
220
291
|
</MillerColumns.Item>
|
|
221
292
|
```
|
|
222
293
|
|
|
@@ -229,10 +300,11 @@ Zero styles ship with the component. Style it through `data-*` hooks;
|
|
|
229
300
|
typically the strip is `display: flex` and each column scrolls
|
|
230
301
|
vertically.
|
|
231
302
|
|
|
232
|
-
Inside the
|
|
303
|
+
Inside the tree element, each column is wrapped in a transparent
|
|
233
304
|
(`display: contents`) slot element that fixes left-to-right order — so
|
|
234
305
|
target columns with `[data-miller-columns-column]`, not a direct-child
|
|
235
|
-
selector like `[data-miller-columns-
|
|
306
|
+
selector like `[data-miller-columns-tree] > *`. Note the strip's direct
|
|
307
|
+
children are the tree element and the `PreviewPanel`, not the columns.
|
|
236
308
|
|
|
237
309
|
```css
|
|
238
310
|
[data-miller-columns-strip] {
|
|
@@ -258,11 +330,12 @@ selector like `[data-miller-columns-strip] > *`.
|
|
|
258
330
|
| Element | Attributes |
|
|
259
331
|
| --------------- | --------------------------------------------------------------------------- |
|
|
260
332
|
| Strip (`Root`) | `data-miller-columns-strip`, `data-orientation="horizontal"` |
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
333
|
+
| Tree (`Root`) | `data-miller-columns-tree`, `role="tree"` — the inner widget element |
|
|
334
|
+
| `Column` | `data-miller-columns-column`, `data-depth`, `data-empty` (no items) |
|
|
335
|
+
| `Item` | `data-state="selected" \| "unselected"`, `data-terminal` (deepest selected), `data-depth`, `data-has-children`, `data-disabled` |
|
|
263
336
|
| `ItemIndicator` | `data-state`, `data-has-children` |
|
|
264
337
|
| `ResizeHandle` | `data-miller-columns-resize-handle`, `data-dragging` (present mid-drag) |
|
|
265
|
-
| `PreviewPanel` | `data-miller-columns-preview`
|
|
338
|
+
| `PreviewPanel` | `data-miller-columns-preview`, `data-empty` (nothing selected) |
|
|
266
339
|
|
|
267
340
|
## Deferred / follow-up work
|
|
268
341
|
|
|
@@ -273,6 +346,3 @@ good candidates for later, independent cycles:
|
|
|
273
346
|
exists, a leaf image `Item` could open a context menu on
|
|
274
347
|
right-click whose first entry ("Preview", with an eye icon) opens
|
|
275
348
|
the `Modal` to show the image larger.
|
|
276
|
-
2. **Keyboard-operable resize.** `MillerColumns.ResizeHandle` is
|
|
277
|
-
pointer-only; an arrow-key resize on the focused handle would round
|
|
278
|
-
out the WAI-ARIA window-splitter pattern.
|
|
@@ -67,6 +67,51 @@ describe("MillerColumns — aria", () => {
|
|
|
67
67
|
);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
it("puts the tree role on an inner element, not the strip container", () => {
|
|
71
|
+
const { container } = render(<Tree />);
|
|
72
|
+
|
|
73
|
+
const strip = container.querySelector("[data-miller-columns-strip]");
|
|
74
|
+
const tree = screen.getByRole("tree");
|
|
75
|
+
|
|
76
|
+
expect(strip).not.toBe(tree);
|
|
77
|
+
expect(strip).toContainElement(tree);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("keeps the preview panel outside the tree", () => {
|
|
81
|
+
render(
|
|
82
|
+
<MillerColumns.Root defaultValue={["a"]}>
|
|
83
|
+
<MillerColumns.Column>
|
|
84
|
+
<MillerColumns.Item value="a">A</MillerColumns.Item>
|
|
85
|
+
</MillerColumns.Column>
|
|
86
|
+
<MillerColumns.PreviewPanel data-testid="preview">
|
|
87
|
+
Preview
|
|
88
|
+
</MillerColumns.PreviewPanel>
|
|
89
|
+
</MillerColumns.Root>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// A tree may only own treeitem and group; the panel is neither.
|
|
93
|
+
expect(screen.getByRole("tree")).not.toContainElement(
|
|
94
|
+
screen.getByTestId("preview"),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("splits aria-* onto the tree widget and everything else onto the strip", () => {
|
|
99
|
+
const { container } = render(
|
|
100
|
+
<MillerColumns.Root aria-label="Files" className="strip" id="picker">
|
|
101
|
+
<MillerColumns.Column>
|
|
102
|
+
<MillerColumns.Item value="a">A</MillerColumns.Item>
|
|
103
|
+
</MillerColumns.Column>
|
|
104
|
+
</MillerColumns.Root>,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const strip = container.querySelector(".strip")!;
|
|
108
|
+
|
|
109
|
+
// The widget is what needs naming; the container is what needs styling.
|
|
110
|
+
expect(screen.getByRole("tree", { name: "Files" })).toBeInTheDocument();
|
|
111
|
+
expect(strip).not.toHaveAttribute("aria-label");
|
|
112
|
+
expect(strip).toHaveAttribute("id", "picker");
|
|
113
|
+
});
|
|
114
|
+
|
|
70
115
|
it("keeps a treeitem's accessible name to its own cell content", async () => {
|
|
71
116
|
const user = userEvent.setup();
|
|
72
117
|
|
|
@@ -32,9 +32,11 @@ describe("MillerColumns — auto-scroll", () => {
|
|
|
32
32
|
it("scrolls the strip to reveal a newly opened column", async () => {
|
|
33
33
|
const user = userEvent.setup();
|
|
34
34
|
|
|
35
|
-
render(<Tree />);
|
|
35
|
+
const { container } = render(<Tree />);
|
|
36
36
|
|
|
37
|
-
const strip =
|
|
37
|
+
const strip = container.querySelector<HTMLElement>(
|
|
38
|
+
"[data-miller-columns-strip]",
|
|
39
|
+
)!;
|
|
38
40
|
const scrollTo = vi.spyOn(strip, "scrollTo");
|
|
39
41
|
|
|
40
42
|
await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
|
|
@@ -3,8 +3,8 @@ import { render, screen } from "@testing-library/react";
|
|
|
3
3
|
import { MillerColumns } from "../MillerColumns";
|
|
4
4
|
|
|
5
5
|
describe("MillerColumns — basic rendering", () => {
|
|
6
|
-
it("renders
|
|
7
|
-
render(
|
|
6
|
+
it("renders a strip container with a horizontal orientation hook", () => {
|
|
7
|
+
const { container } = render(
|
|
8
8
|
<MillerColumns.Root>
|
|
9
9
|
<MillerColumns.Column>
|
|
10
10
|
<MillerColumns.Item value="a">A</MillerColumns.Item>
|
|
@@ -12,12 +12,27 @@ describe("MillerColumns — basic rendering", () => {
|
|
|
12
12
|
</MillerColumns.Root>,
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
const strip =
|
|
15
|
+
const strip = container.querySelector("[data-miller-columns-strip]");
|
|
16
16
|
|
|
17
17
|
expect(strip).toBeInTheDocument();
|
|
18
18
|
expect(strip).toHaveAttribute("data-orientation", "horizontal");
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
it("renders the tree widget inside the strip container", () => {
|
|
22
|
+
const { container } = render(
|
|
23
|
+
<MillerColumns.Root>
|
|
24
|
+
<MillerColumns.Column>
|
|
25
|
+
<MillerColumns.Item value="a">A</MillerColumns.Item>
|
|
26
|
+
</MillerColumns.Column>
|
|
27
|
+
</MillerColumns.Root>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const tree = screen.getByRole("tree");
|
|
31
|
+
|
|
32
|
+
expect(tree).toHaveAttribute("data-miller-columns-tree");
|
|
33
|
+
expect(container.querySelector("[data-miller-columns-strip]")).toContainElement(tree);
|
|
34
|
+
});
|
|
35
|
+
|
|
21
36
|
it("projects the top-level column into the strip as a group", () => {
|
|
22
37
|
render(
|
|
23
38
|
<MillerColumns.Root>
|
|
@@ -33,6 +33,27 @@ describe("MillerColumns — data attributes", () => {
|
|
|
33
33
|
expect(groups[1]).toHaveAttribute("data-depth", "1");
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it("flags a column with no items as empty", async () => {
|
|
37
|
+
const user = userEvent.setup();
|
|
38
|
+
|
|
39
|
+
render(
|
|
40
|
+
<MillerColumns.Root>
|
|
41
|
+
<MillerColumns.Column>
|
|
42
|
+
<MillerColumns.Item value="empty-folder">
|
|
43
|
+
Empty folder
|
|
44
|
+
<MillerColumns.Column />
|
|
45
|
+
</MillerColumns.Item>
|
|
46
|
+
</MillerColumns.Column>
|
|
47
|
+
</MillerColumns.Root>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await user.click(screen.getByRole("treeitem", { name: "Empty folder" }));
|
|
51
|
+
|
|
52
|
+
const [root, revealed] = screen.getAllByRole("group");
|
|
53
|
+
expect(root).not.toHaveAttribute("data-empty");
|
|
54
|
+
expect(revealed).toHaveAttribute("data-empty");
|
|
55
|
+
});
|
|
56
|
+
|
|
36
57
|
it("reflects selection through data-state", async () => {
|
|
37
58
|
const user = userEvent.setup();
|
|
38
59
|
|
|
@@ -46,6 +67,33 @@ describe("MillerColumns — data attributes", () => {
|
|
|
46
67
|
expect(veg).toHaveAttribute("data-state", "selected");
|
|
47
68
|
});
|
|
48
69
|
|
|
70
|
+
it("marks only the deepest selected item with data-terminal", async () => {
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
|
|
73
|
+
render(<Tree />);
|
|
74
|
+
|
|
75
|
+
const fruit = screen.getByRole("treeitem", { name: "Fruit" });
|
|
76
|
+
|
|
77
|
+
// Selected and deepest — it is the terminal selection.
|
|
78
|
+
await user.click(fruit);
|
|
79
|
+
expect(fruit).toHaveAttribute("data-terminal");
|
|
80
|
+
|
|
81
|
+
// Still selected, but now an ancestor of a deeper selection.
|
|
82
|
+
await user.click(screen.getByRole("treeitem", { name: "Apple" }));
|
|
83
|
+
|
|
84
|
+
expect(fruit).toHaveAttribute("data-state", "selected");
|
|
85
|
+
expect(fruit).not.toHaveAttribute("data-terminal");
|
|
86
|
+
expect(screen.getByRole("treeitem", { name: "Apple" })).toHaveAttribute(
|
|
87
|
+
"data-terminal",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Unselected, but sitting at the same depth the terminal item once
|
|
91
|
+
// occupied — depth alone must not be what earns the attribute.
|
|
92
|
+
expect(
|
|
93
|
+
screen.getByRole("treeitem", { name: "Veg" }),
|
|
94
|
+
).not.toHaveAttribute("data-terminal");
|
|
95
|
+
});
|
|
96
|
+
|
|
49
97
|
it("flags branch items with data-has-children and omits it on leaves", () => {
|
|
50
98
|
render(<Tree />);
|
|
51
99
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
|
|
4
|
+
import { DirectionProvider } from "../../DirectionProvider/index.ts";
|
|
4
5
|
import { MillerColumns } from "../MillerColumns";
|
|
5
6
|
import { verticalArrowCases } from "./MillerColumns.fixtures";
|
|
6
7
|
|
|
@@ -16,9 +17,9 @@ function FlatList() {
|
|
|
16
17
|
);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function Tree() {
|
|
20
|
+
function Tree({ dir }: { dir?: "ltr" | "rtl" } = {}) {
|
|
20
21
|
return (
|
|
21
|
-
<MillerColumns.Root>
|
|
22
|
+
<MillerColumns.Root dir={dir}>
|
|
22
23
|
<MillerColumns.Column>
|
|
23
24
|
<MillerColumns.Item value="fruit">
|
|
24
25
|
Fruit
|
|
@@ -178,4 +179,36 @@ describe("MillerColumns — keyboard interaction", () => {
|
|
|
178
179
|
expect(veg).toHaveFocus();
|
|
179
180
|
});
|
|
180
181
|
});
|
|
182
|
+
|
|
183
|
+
describe("reading direction", () => {
|
|
184
|
+
it("swaps the horizontal arrow pair under rtl", async () => {
|
|
185
|
+
const user = userEvent.setup();
|
|
186
|
+
|
|
187
|
+
render(<Tree dir="rtl" />);
|
|
188
|
+
screen.getByRole("treeitem", { name: "Fruit" }).focus();
|
|
189
|
+
|
|
190
|
+
// The child column sits to the LEFT in rtl, so ArrowLeft steps in.
|
|
191
|
+
await user.keyboard("{ArrowLeft}");
|
|
192
|
+
expect(screen.getByRole("treeitem", { name: "Apple" })).toHaveFocus();
|
|
193
|
+
|
|
194
|
+
// ...and ArrowRight walks back out to the parent.
|
|
195
|
+
await user.keyboard("{ArrowRight}");
|
|
196
|
+
expect(screen.getByRole("treeitem", { name: "Fruit" })).toHaveFocus();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("inherits the direction from a DirectionProvider", async () => {
|
|
200
|
+
const user = userEvent.setup();
|
|
201
|
+
|
|
202
|
+
render(
|
|
203
|
+
<DirectionProvider dir="rtl">
|
|
204
|
+
<Tree />
|
|
205
|
+
</DirectionProvider>,
|
|
206
|
+
);
|
|
207
|
+
screen.getByRole("treeitem", { name: "Fruit" }).focus();
|
|
208
|
+
|
|
209
|
+
await user.keyboard("{ArrowLeft}");
|
|
210
|
+
|
|
211
|
+
expect(screen.getByRole("treeitem", { name: "Apple" })).toHaveFocus();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
181
214
|
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { MillerColumns } from "../MillerColumns";
|
|
5
|
+
|
|
6
|
+
function Tree() {
|
|
7
|
+
return (
|
|
8
|
+
<MillerColumns.Root>
|
|
9
|
+
<MillerColumns.Column>
|
|
10
|
+
<MillerColumns.Item value="documents">
|
|
11
|
+
Documents
|
|
12
|
+
<MillerColumns.Column>
|
|
13
|
+
<MillerColumns.Item value="work">Work</MillerColumns.Item>
|
|
14
|
+
</MillerColumns.Column>
|
|
15
|
+
</MillerColumns.Item>
|
|
16
|
+
<MillerColumns.Item value="pictures">
|
|
17
|
+
Pictures
|
|
18
|
+
<MillerColumns.Column>
|
|
19
|
+
<MillerColumns.Item value="holiday">
|
|
20
|
+
Holiday
|
|
21
|
+
<MillerColumns.Column>
|
|
22
|
+
<MillerColumns.Item value="beach">beach.jpg</MillerColumns.Item>
|
|
23
|
+
<MillerColumns.Item value="sunset">sunset.jpg</MillerColumns.Item>
|
|
24
|
+
</MillerColumns.Column>
|
|
25
|
+
</MillerColumns.Item>
|
|
26
|
+
</MillerColumns.Column>
|
|
27
|
+
</MillerColumns.Item>
|
|
28
|
+
</MillerColumns.Column>
|
|
29
|
+
</MillerColumns.Root>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const state = (name: string) =>
|
|
34
|
+
screen.getByRole("treeitem", { name }).getAttribute("data-state");
|
|
35
|
+
const terminal = (name: string) =>
|
|
36
|
+
screen.getByRole("treeitem", { name }).hasAttribute("data-terminal");
|
|
37
|
+
|
|
38
|
+
describe("MillerColumns — selection state under keyboard navigation", () => {
|
|
39
|
+
it("deselects the previous branch when arrowing to a sibling and stepping in", async () => {
|
|
40
|
+
const user = userEvent.setup();
|
|
41
|
+
|
|
42
|
+
render(<Tree />);
|
|
43
|
+
|
|
44
|
+
await user.click(screen.getByRole("treeitem", { name: "Documents" }));
|
|
45
|
+
expect(state("Documents")).toBe("selected");
|
|
46
|
+
|
|
47
|
+
// Down to Pictures, then right to open it — Documents must let go.
|
|
48
|
+
await user.keyboard("{ArrowDown}{ArrowRight}");
|
|
49
|
+
|
|
50
|
+
expect(state("Pictures")).toBe("selected");
|
|
51
|
+
expect(state("Documents")).toBe("unselected");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps exactly one terminal item as focus moves deeper", async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
|
|
57
|
+
render(<Tree />);
|
|
58
|
+
|
|
59
|
+
await user.click(screen.getByRole("treeitem", { name: "Pictures" }));
|
|
60
|
+
await user.click(screen.getByRole("treeitem", { name: "Holiday" }));
|
|
61
|
+
|
|
62
|
+
expect(state("Pictures")).toBe("selected");
|
|
63
|
+
expect(terminal("Pictures")).toBe(false);
|
|
64
|
+
expect(terminal("Holiday")).toBe(true);
|
|
65
|
+
|
|
66
|
+
// Arrowing within the revealed column moves focus only — it must not
|
|
67
|
+
// change which row is terminal.
|
|
68
|
+
await user.keyboard("{ArrowRight}{ArrowDown}");
|
|
69
|
+
|
|
70
|
+
expect(screen.getByRole("treeitem", { name: "sunset.jpg" })).toHaveFocus();
|
|
71
|
+
expect(terminal("Holiday")).toBe(true);
|
|
72
|
+
expect(terminal("sunset.jpg")).toBe(false);
|
|
73
|
+
expect(state("Documents")).toBe("unselected");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("MillerColumns — a selected leaf deep in the path", () => {
|
|
78
|
+
it("marks the leaf terminal and its ancestors selected", async () => {
|
|
79
|
+
const user = userEvent.setup();
|
|
80
|
+
|
|
81
|
+
render(<Tree />);
|
|
82
|
+
|
|
83
|
+
await user.click(screen.getByRole("treeitem", { name: "Pictures" }));
|
|
84
|
+
await user.click(screen.getByRole("treeitem", { name: "Holiday" }));
|
|
85
|
+
await user.click(screen.getByRole("treeitem", { name: "beach.jpg" }));
|
|
86
|
+
|
|
87
|
+
expect(state("Pictures")).toBe("selected");
|
|
88
|
+
expect(terminal("Pictures")).toBe(false);
|
|
89
|
+
expect(state("Holiday")).toBe("selected");
|
|
90
|
+
expect(terminal("Holiday")).toBe(false);
|
|
91
|
+
|
|
92
|
+
// The row the user actually landed on.
|
|
93
|
+
expect(state("beach.jpg")).toBe("selected");
|
|
94
|
+
expect(terminal("beach.jpg")).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
2
3
|
|
|
3
4
|
import { MillerColumns } from "../MillerColumns";
|
|
4
5
|
|
|
5
6
|
describe("MillerColumns — preview panel", () => {
|
|
6
7
|
it("renders a panel at the trailing edge of the strip", () => {
|
|
7
|
-
render(
|
|
8
|
-
<MillerColumns.Root>
|
|
8
|
+
const { container } = render(
|
|
9
|
+
<MillerColumns.Root defaultValue={["a"]}>
|
|
9
10
|
<MillerColumns.Column>
|
|
10
11
|
<MillerColumns.Item value="a">A</MillerColumns.Item>
|
|
11
12
|
</MillerColumns.Column>
|
|
@@ -13,7 +14,7 @@ describe("MillerColumns — preview panel", () => {
|
|
|
13
14
|
</MillerColumns.Root>,
|
|
14
15
|
);
|
|
15
16
|
|
|
16
|
-
const strip =
|
|
17
|
+
const strip = container.querySelector("[data-miller-columns-strip]")!;
|
|
17
18
|
const panel = screen.getByText("preview");
|
|
18
19
|
|
|
19
20
|
expect(panel).toHaveAttribute("data-miller-columns-preview");
|
|
@@ -21,9 +22,33 @@ describe("MillerColumns — preview panel", () => {
|
|
|
21
22
|
expect(strip.lastElementChild).toBe(panel);
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
// Only reachable under forceMount: without it the panel does not exist while
|
|
26
|
+
// nothing is selected, so there is nothing to flag.
|
|
27
|
+
it("flags a force-mounted panel as empty until something is selected", async () => {
|
|
28
|
+
const user = userEvent.setup();
|
|
29
|
+
|
|
25
30
|
render(
|
|
26
31
|
<MillerColumns.Root>
|
|
32
|
+
<MillerColumns.Column>
|
|
33
|
+
<MillerColumns.Item value="a">A</MillerColumns.Item>
|
|
34
|
+
</MillerColumns.Column>
|
|
35
|
+
<MillerColumns.PreviewPanel forceMount>
|
|
36
|
+
preview
|
|
37
|
+
</MillerColumns.PreviewPanel>
|
|
38
|
+
</MillerColumns.Root>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const panel = screen.getByText("preview");
|
|
42
|
+
expect(panel).toHaveAttribute("data-empty");
|
|
43
|
+
|
|
44
|
+
await user.click(screen.getByRole("treeitem", { name: "A" }));
|
|
45
|
+
|
|
46
|
+
expect(panel).not.toHaveAttribute("data-empty");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("forwards props to the panel element", () => {
|
|
50
|
+
render(
|
|
51
|
+
<MillerColumns.Root defaultValue={["a"]}>
|
|
27
52
|
<MillerColumns.Column>
|
|
28
53
|
<MillerColumns.Item value="a">A</MillerColumns.Item>
|
|
29
54
|
</MillerColumns.Column>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { MillerColumns } from "../MillerColumns";
|
|
5
|
+
|
|
6
|
+
function Tree({ forceMount }: { forceMount?: boolean } = {}) {
|
|
7
|
+
return (
|
|
8
|
+
<MillerColumns.Root>
|
|
9
|
+
<MillerColumns.Column>
|
|
10
|
+
<MillerColumns.Item value="fruit">
|
|
11
|
+
Fruit
|
|
12
|
+
<MillerColumns.Column>
|
|
13
|
+
<MillerColumns.Item value="apple">Apple</MillerColumns.Item>
|
|
14
|
+
</MillerColumns.Column>
|
|
15
|
+
</MillerColumns.Item>
|
|
16
|
+
<MillerColumns.Item value="veg">Veg</MillerColumns.Item>
|
|
17
|
+
</MillerColumns.Column>
|
|
18
|
+
<MillerColumns.PreviewPanel forceMount={forceMount}>
|
|
19
|
+
preview
|
|
20
|
+
</MillerColumns.PreviewPanel>
|
|
21
|
+
</MillerColumns.Root>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Finder's rule: the preview occupies the slot *after* the selected item, so it
|
|
27
|
+
* exists only when that item has no children of its own. Select a folder and
|
|
28
|
+
* you get its contents there instead; select nothing and there is no preview
|
|
29
|
+
* at all.
|
|
30
|
+
*/
|
|
31
|
+
describe("MillerColumns — preview panel visibility", () => {
|
|
32
|
+
it("renders no preview until something is selected", () => {
|
|
33
|
+
render(<Tree />);
|
|
34
|
+
|
|
35
|
+
expect(screen.queryByText("preview")).not.toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders no preview while a branch is selected", async () => {
|
|
39
|
+
const user = userEvent.setup();
|
|
40
|
+
|
|
41
|
+
render(<Tree />);
|
|
42
|
+
await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
|
|
43
|
+
|
|
44
|
+
// The slot after Fruit belongs to its children, not to a preview.
|
|
45
|
+
expect(screen.getByRole("treeitem", { name: "Apple" })).toBeInTheDocument();
|
|
46
|
+
expect(screen.queryByText("preview")).not.toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("renders the preview once a leaf is selected", async () => {
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
|
|
52
|
+
render(<Tree />);
|
|
53
|
+
await user.click(screen.getByRole("treeitem", { name: "Veg" }));
|
|
54
|
+
|
|
55
|
+
expect(screen.getByText("preview")).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders the preview for a leaf nested inside a branch", async () => {
|
|
59
|
+
const user = userEvent.setup();
|
|
60
|
+
|
|
61
|
+
render(<Tree />);
|
|
62
|
+
await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
|
|
63
|
+
await user.click(screen.getByRole("treeitem", { name: "Apple" }));
|
|
64
|
+
|
|
65
|
+
expect(screen.getByText("preview")).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps the panel mounted throughout when forceMount is set", async () => {
|
|
69
|
+
const user = userEvent.setup();
|
|
70
|
+
|
|
71
|
+
render(<Tree forceMount />);
|
|
72
|
+
|
|
73
|
+
expect(screen.getByText("preview")).toBeInTheDocument();
|
|
74
|
+
|
|
75
|
+
await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
|
|
76
|
+
|
|
77
|
+
expect(screen.getByText("preview")).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
});
|