@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
package/src/Switch/types.ts
CHANGED
|
@@ -1,50 +1,67 @@
|
|
|
1
1
|
import { ChangeEventHandler, ComponentProps, ReactNode, Ref } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Shared base for both {@link SwitchRootProps} variants — the native
|
|
5
|
+
* `<input type="checkbox">` attributes (minus the ones the component owns:
|
|
6
|
+
* `type`, `role`, `checked`, and `defaultChecked`), plus `children` and a
|
|
7
|
+
* typed `ref`.
|
|
8
|
+
*
|
|
9
|
+
* Prop routing: `className` / `style` style the **track** (the visible
|
|
10
|
+
* `<label>`, since the input is visually hidden); every other prop — `name`,
|
|
11
|
+
* `value`, `id`, `aria-*`, `required`, `disabled`, `ref`, ... — spreads onto the
|
|
12
|
+
* hidden `<input>`, because semantically the Root *is* the switch.
|
|
9
13
|
*/
|
|
10
14
|
export type SwitchRootBaseProps = Omit<
|
|
11
15
|
ComponentProps<"input">,
|
|
12
16
|
"type" | "role" | "checked" | "defaultChecked"
|
|
13
17
|
> & {
|
|
14
|
-
/**
|
|
18
|
+
/** Content of the switch — typically a single
|
|
19
|
+
* {@link SwitchThumbProps | `Switch.Thumb`}. */
|
|
15
20
|
children?: ReactNode;
|
|
16
|
-
/**
|
|
21
|
+
/** Forwarded to the underlying native `HTMLInputElement` (the real,
|
|
22
|
+
* visually-hidden checkbox), not the visible track. */
|
|
17
23
|
ref?: Ref<HTMLInputElement>;
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
|
-
*
|
|
22
|
-
* checked state
|
|
23
|
-
* forbidden.
|
|
27
|
+
* Uncontrolled variant of {@link SwitchRootProps}: the **browser** owns the
|
|
28
|
+
* checked state, so the switch participates in native form submission and
|
|
29
|
+
* reset. Pass `defaultChecked` (or omit it); `checked` is forbidden.
|
|
24
30
|
*/
|
|
25
31
|
export type SwitchRootUncontrolledProps = SwitchRootBaseProps & {
|
|
26
|
-
/**
|
|
32
|
+
/** Checked state on first render. Defaults to `false` when omitted.
|
|
33
|
+
* @default false */
|
|
27
34
|
defaultChecked?: boolean;
|
|
28
|
-
/** Forbidden in uncontrolled mode. */
|
|
35
|
+
/** Forbidden in uncontrolled mode — use `defaultChecked` instead. */
|
|
29
36
|
checked?: never;
|
|
30
|
-
/** Called
|
|
37
|
+
/** Called with the new boolean state whenever the switch toggles. Optional
|
|
38
|
+
* in uncontrolled mode. */
|
|
31
39
|
onCheckedChange?: (checked: boolean) => void;
|
|
32
40
|
};
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
*
|
|
36
|
-
* value. Pass `checked` and `onCheckedChange` together
|
|
43
|
+
* Controlled variant of {@link SwitchRootProps}: the parent owns the checked
|
|
44
|
+
* value. Pass `checked` and `onCheckedChange` together; `defaultChecked` is
|
|
45
|
+
* forbidden.
|
|
37
46
|
*/
|
|
38
47
|
export type SwitchRootControlledProps = SwitchRootBaseProps & {
|
|
39
|
-
/** Forbidden in controlled mode. */
|
|
48
|
+
/** Forbidden in controlled mode — use `checked` instead. */
|
|
40
49
|
defaultChecked?: never;
|
|
41
|
-
/** The controlled checked state.
|
|
50
|
+
/** The controlled checked state. Must be kept in sync by the parent via
|
|
51
|
+
* `onCheckedChange`. */
|
|
42
52
|
checked: boolean;
|
|
43
|
-
/** Called whenever the
|
|
53
|
+
/** Called with the new boolean state whenever the user requests a toggle.
|
|
54
|
+
* Required in controlled mode. */
|
|
44
55
|
onCheckedChange: (checked: boolean) => void;
|
|
45
56
|
};
|
|
46
57
|
|
|
47
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Props for {@link SwitchRoot | `Switch.Root`}.
|
|
60
|
+
*
|
|
61
|
+
* Resolves to either {@link SwitchRootUncontrolledProps} or
|
|
62
|
+
* {@link SwitchRootControlledProps} — only one shape is accepted by TypeScript
|
|
63
|
+
* at a time.
|
|
64
|
+
*/
|
|
48
65
|
export type SwitchRootProps =
|
|
49
66
|
| SwitchRootUncontrolledProps
|
|
50
67
|
| SwitchRootControlledProps;
|
|
@@ -56,11 +73,21 @@ export type SwitchRootProps =
|
|
|
56
73
|
*/
|
|
57
74
|
export type SwitchProps = SwitchRootProps;
|
|
58
75
|
|
|
59
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Props for {@link SwitchThumb | `Switch.Thumb`} — the decorative sliding
|
|
78
|
+
* indicator inside the track. All `HTMLSpanElement` attributes, plus the
|
|
79
|
+
* `asChild` escape hatch.
|
|
80
|
+
*/
|
|
60
81
|
export type SwitchThumbProps = ComponentProps<"span"> & {
|
|
61
|
-
/** Optional thumb content
|
|
82
|
+
/** Optional thumb content (rarely needed — the thumb is usually styled
|
|
83
|
+
* purely with CSS). */
|
|
62
84
|
children?: ReactNode;
|
|
63
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* When `true`, render the consumer's own element as the thumb instead of the
|
|
87
|
+
* default `<span>`, merging `aria-hidden` and `data-state` onto it via the
|
|
88
|
+
* {@link Slot} pattern.
|
|
89
|
+
* @default false
|
|
90
|
+
*/
|
|
64
91
|
asChild?: boolean;
|
|
65
92
|
};
|
|
66
93
|
|
package/src/Table/README.md
CHANGED
|
@@ -51,6 +51,9 @@ import { Table } from "@primitiv-ui/react";
|
|
|
51
51
|
| `Table.Cell` | `<td>` | Data cell. Accepts `colSpan` and `rowSpan`. |
|
|
52
52
|
| `Table.ScrollArea` | `<div>` | Horizontal-scroll wrapper — see [Responsive scrolling](#responsive-scrolling). |
|
|
53
53
|
| `Table.Caption` | `<caption>` | Visible table label — see [Caption](#caption). |
|
|
54
|
+
| `Table.Expandable` | *nothing* | Pairs a row with its detail row — see [Expandable rows](#expandable-rows). |
|
|
55
|
+
| `Table.ExpandTrigger` | `<button>` | The row's disclosure control; owns `aria-expanded` / `aria-controls`. |
|
|
56
|
+
| `Table.DetailRow` | `<tr>` | The revealed panel, as a `<td colSpan>`. `gutter` offsets it to the first data column. |
|
|
54
57
|
|
|
55
58
|
## Accessible headers
|
|
56
59
|
|
|
@@ -96,14 +99,14 @@ the table via the CSS `caption-side` property. Defaults to `"bottom"`.
|
|
|
96
99
|
/* Caption below table (default) */
|
|
97
100
|
}
|
|
98
101
|
<Table.Root>
|
|
99
|
-
<Table.Caption>Q1 sales by region</Table.Caption
|
|
102
|
+
<Table.Caption>Q1 sales by region</Table.Caption>...
|
|
100
103
|
</Table.Root>;
|
|
101
104
|
|
|
102
105
|
{
|
|
103
106
|
/* Caption above table */
|
|
104
107
|
}
|
|
105
108
|
<Table.Root>
|
|
106
|
-
<Table.Caption captionSide="top">Q1 sales by region</Table.Caption
|
|
109
|
+
<Table.Caption captionSide="top">Q1 sales by region</Table.Caption>...
|
|
107
110
|
</Table.Root>;
|
|
108
111
|
```
|
|
109
112
|
|
|
@@ -111,7 +114,7 @@ When a visible caption is not desirable, use `aria-label` directly on
|
|
|
111
114
|
`Table.Root` instead:
|
|
112
115
|
|
|
113
116
|
```tsx
|
|
114
|
-
<Table.Root aria-label="Q1 sales by region"
|
|
117
|
+
<Table.Root aria-label="Q1 sales by region">...</Table.Root>
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
## Responsive scrolling
|
|
@@ -124,7 +127,7 @@ narrow viewports instead of overflowing or compressing columns.
|
|
|
124
127
|
|
|
125
128
|
```tsx
|
|
126
129
|
<Table.ScrollArea>
|
|
127
|
-
<Table.Root
|
|
130
|
+
<Table.Root>...</Table.Root>
|
|
128
131
|
</Table.ScrollArea>
|
|
129
132
|
```
|
|
130
133
|
|
|
@@ -133,7 +136,7 @@ base scroll styles, so you can layer additional styles freely:
|
|
|
133
136
|
|
|
134
137
|
```tsx
|
|
135
138
|
<Table.ScrollArea style={{ borderRadius: "8px" }}>
|
|
136
|
-
<Table.Root
|
|
139
|
+
<Table.Root>...</Table.Root>
|
|
137
140
|
</Table.ScrollArea>
|
|
138
141
|
```
|
|
139
142
|
|
|
@@ -144,14 +147,14 @@ separating logical row groups within a single table:
|
|
|
144
147
|
|
|
145
148
|
```tsx
|
|
146
149
|
<Table.Root>
|
|
147
|
-
<Table.Head
|
|
150
|
+
<Table.Head>...</Table.Head>
|
|
148
151
|
<Table.Body>
|
|
149
152
|
{/* Group 1 */}
|
|
150
|
-
<Table.Row
|
|
153
|
+
<Table.Row>...</Table.Row>
|
|
151
154
|
</Table.Body>
|
|
152
155
|
<Table.Body>
|
|
153
156
|
{/* Group 2 */}
|
|
154
|
-
<Table.Row
|
|
157
|
+
<Table.Row>...</Table.Row>
|
|
155
158
|
</Table.Body>
|
|
156
159
|
</Table.Root>
|
|
157
160
|
```
|
|
@@ -203,3 +206,102 @@ caption {
|
|
|
203
206
|
---
|
|
204
207
|
|
|
205
208
|
[Back to @primitiv-ui/react](../../README.md)
|
|
209
|
+
|
|
210
|
+
## Expandable rows
|
|
211
|
+
|
|
212
|
+
A row can open onto a detail panel. This is a **disclosure**, not a
|
|
213
|
+
[treegrid](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/): expanding into
|
|
214
|
+
hierarchical child *rows* is a different pattern with a full cell-navigation
|
|
215
|
+
keyboard model, and is deliberately out of scope here.
|
|
216
|
+
|
|
217
|
+
`Table.Expandable` renders no DOM of its own — it is a context provider, so both
|
|
218
|
+
`<tr>`s stay direct children of the `<tbody>` and the table's structure is
|
|
219
|
+
untouched.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
<Table.Body>
|
|
223
|
+
<Table.Expandable expanded={open} onExpandedChange={setOpen}>
|
|
224
|
+
<Table.Row>
|
|
225
|
+
<Table.Cell>
|
|
226
|
+
<Table.ExpandTrigger>
|
|
227
|
+
<ChevronIcon aria-hidden="true" />
|
|
228
|
+
<span className="sr-only">Show details for Ada Lovelace</span>
|
|
229
|
+
</Table.ExpandTrigger>
|
|
230
|
+
</Table.Cell>
|
|
231
|
+
<Table.Cell>Ada Lovelace</Table.Cell>
|
|
232
|
+
</Table.Row>
|
|
233
|
+
|
|
234
|
+
<Table.DetailRow colSpan={2}>
|
|
235
|
+
<ContributorDetail />
|
|
236
|
+
</Table.DetailRow>
|
|
237
|
+
</Table.Expandable>
|
|
238
|
+
</Table.Body>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Controlled via `expanded` + `onExpandedChange` (what an external table engine
|
|
242
|
+
drives), or uncontrolled with `defaultExpanded`.
|
|
243
|
+
|
|
244
|
+
### Why the state is on the button
|
|
245
|
+
|
|
246
|
+
`role="row"` does list `aria-expanded` among its supported properties, but a
|
|
247
|
+
**button's** expanded state is announced universally while a row's is only
|
|
248
|
+
reliably announced inside a `role="treegrid"`. So the button owns
|
|
249
|
+
`aria-expanded` and `aria-controls`, and the detail row publishes the matching
|
|
250
|
+
`id`.
|
|
251
|
+
|
|
252
|
+
Give the trigger a name that identifies **its row**. A column of buttons all
|
|
253
|
+
called "Expand" is the same defect as a column of checkboxes all called
|
|
254
|
+
"Select": obvious on screen, useless in a screen reader's element list.
|
|
255
|
+
|
|
256
|
+
### `colSpan` is required
|
|
257
|
+
|
|
258
|
+
It must cover every column the table renders, control columns included. It is a
|
|
259
|
+
prop rather than a count taken from context on purpose: counting would mean the
|
|
260
|
+
table registering its own columns, and an external engine already knows the
|
|
261
|
+
number (TanStack Table: `table.getVisibleLeafColumns().length`).
|
|
262
|
+
|
|
263
|
+
### `gutter` aligns the panel with the first data column
|
|
264
|
+
|
|
265
|
+
A panel that starts at the table's edge, under the checkbox, reads as a new
|
|
266
|
+
section of the table rather than as an opened row. `gutter` is how many leading
|
|
267
|
+
columns to leave empty — the control columns in front of the data:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
{/* select + expand columns, then eight data columns */}
|
|
271
|
+
<Table.DetailRow colSpan={10} gutter={2}>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The panel spans what remains, so `colSpan` keeps its meaning: the table's full
|
|
275
|
+
column count, gutter included.
|
|
276
|
+
|
|
277
|
+
It renders **one** empty `<td colSpan={gutter}>`, not `gutter` of them — a
|
|
278
|
+
spanning cell takes the full width of the columns it covers, so the alignment is
|
|
279
|
+
identical and the row gains a single blank cell instead of several.
|
|
280
|
+
|
|
281
|
+
Aligning by empty cell rather than by a CSS indent is the point: the browser's
|
|
282
|
+
own table layout resolves the width, so it stays exact across every size,
|
|
283
|
+
density and control-column set. Indenting instead means summing control widths
|
|
284
|
+
and paddings by hand, which is right at one size and wrong at the other four —
|
|
285
|
+
measured drift of 7-21px across xs...xl before this existed.
|
|
286
|
+
|
|
287
|
+
### `forceMount` and the row count
|
|
288
|
+
|
|
289
|
+
By default a collapsed detail row is `hidden`, so it is absent from the
|
|
290
|
+
accessibility tree — which matters more here than on a `<div>`-based disclosure,
|
|
291
|
+
because a mounted `<tr>` is still a **row**: 24 data rows each with a
|
|
292
|
+
force-mounted detail row would announce 48.
|
|
293
|
+
|
|
294
|
+
Pass `forceMount` to keep it mounted so CSS can transition it. While collapsed it
|
|
295
|
+
then carries `aria-hidden="true"`, so the announced row count stays truthful
|
|
296
|
+
either way.
|
|
297
|
+
|
|
298
|
+
### Styling hooks
|
|
299
|
+
|
|
300
|
+
`data-state="open" | "closed"` on both the trigger and the detail row.
|
|
301
|
+
|
|
302
|
+
Note that the `display: grid` 0fr↔1fr open/close technique used by Accordion and
|
|
303
|
+
Collapsible **cannot** go on the `<tr>` or the `<td>` — `display: grid` on either
|
|
304
|
+
takes it out of the table layout algorithm and column alignment collapses with
|
|
305
|
+
it. Put the animated wrapper inside the cell, and zero the cell's block padding
|
|
306
|
+
so a closed row leaves no padded band. The registry `data-table` component does
|
|
307
|
+
this for you.
|
package/src/Table/Table.tsx
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useId, useMemo, type MouseEvent, type ReactElement } from "react";
|
|
2
2
|
|
|
3
|
+
import { useControllableState } from "../hooks/index.ts";
|
|
4
|
+
import { Slot } from "../Slot/index.ts";
|
|
5
|
+
import { deriveId } from "../utils/index.ts";
|
|
6
|
+
import {
|
|
7
|
+
TableExpandableContext,
|
|
8
|
+
useTableExpandableContext,
|
|
9
|
+
} from "./TableExpandableContext";
|
|
3
10
|
import {
|
|
4
11
|
TableBodyProps,
|
|
5
12
|
TableCaptionProps,
|
|
6
13
|
TableCellProps,
|
|
14
|
+
TableDetailRowProps,
|
|
15
|
+
TableExpandableProps,
|
|
16
|
+
TableExpandTriggerProps,
|
|
7
17
|
TableFooterProps,
|
|
8
18
|
TableHeaderProps,
|
|
9
19
|
TableHeadProps,
|
|
@@ -16,13 +26,28 @@ import {
|
|
|
16
26
|
* The root of a Table widget — renders a plain `<table>` element and passes
|
|
17
27
|
* all `TableHTMLAttributes` through to the DOM.
|
|
18
28
|
*
|
|
29
|
+
* Structural: every part below renders exactly what its native HTML element
|
|
30
|
+
* provides, with no context and no `data-*` attributes of its own (verified by
|
|
31
|
+
* `Table.contract.test.tsx` against the registry contract).
|
|
32
|
+
*
|
|
33
|
+
* The one exception is the expandable-row trio —
|
|
34
|
+
* {@link TableExpandable | `Table.Expandable`},
|
|
35
|
+
* {@link TableExpandTrigger | `Table.ExpandTrigger`} and
|
|
36
|
+
* {@link TableDetailRow | `Table.DetailRow`} — which share a context so the
|
|
37
|
+
* trigger's `aria-controls` can reach the detail row. They are opt-in: a table
|
|
38
|
+
* that does not use them is still entirely static.
|
|
39
|
+
*
|
|
19
40
|
* The `<table>` element carries an implicit `role="table"` in the accessibility
|
|
20
41
|
* tree. Assistive technology will announce it as a table and report the number
|
|
21
42
|
* of rows and columns to the user.
|
|
22
43
|
*
|
|
23
|
-
* Always pair column headers (`Table.Header`) with the
|
|
24
|
-
* {@link TableHeaderProps.scope | `scope`} attribute so screen
|
|
25
|
-
* associate data cells with their headers.
|
|
44
|
+
* Always pair column headers ({@link TableHeader | `Table.Header`}) with the
|
|
45
|
+
* correct {@link TableHeaderProps.scope | `scope`} attribute so screen
|
|
46
|
+
* readers can associate data cells with their headers. Wrap the whole table
|
|
47
|
+
* in {@link TableScrollArea | `Table.ScrollArea`} when it has many columns
|
|
48
|
+
* and needs to scroll horizontally on narrow viewports.
|
|
49
|
+
*
|
|
50
|
+
* @extends HTMLTableElement
|
|
26
51
|
*
|
|
27
52
|
* @example
|
|
28
53
|
* ```tsx
|
|
@@ -47,6 +72,10 @@ export function TableRoot({ children, ...rest }: TableRootProps): ReactElement {
|
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
/** @internal */
|
|
75
|
+
// `TableRoot` and the exported `Table` compound are the same object
|
|
76
|
+
// (`Object.assign`), and the compound re-assigns this displayName at the bottom
|
|
77
|
+
// of the file — so this assignment is unobservable and any value survives.
|
|
78
|
+
// Stryker disable next-line StringLiteral: equivalent — overwritten by the compound.
|
|
50
79
|
TableRoot.displayName = "Table";
|
|
51
80
|
|
|
52
81
|
/**
|
|
@@ -54,7 +83,12 @@ TableRoot.displayName = "Table";
|
|
|
54
83
|
*
|
|
55
84
|
* Browsers and assistive technology treat rows inside `<thead>` as column
|
|
56
85
|
* headers. When a table is printed across multiple pages, the browser may
|
|
57
|
-
* repeat the `<thead>` content at the top of each page.
|
|
86
|
+
* repeat the `<thead>` content at the top of each page. Renders as a direct
|
|
87
|
+
* child of {@link TableRoot | `Table.Root`}'s `<table>`, typically
|
|
88
|
+
* containing a single {@link TableRow | `Table.Row`} of
|
|
89
|
+
* {@link TableHeader | `Table.Header`}s.
|
|
90
|
+
*
|
|
91
|
+
* @extends HTMLTableSectionElement
|
|
58
92
|
*
|
|
59
93
|
* @example
|
|
60
94
|
* ```tsx
|
|
@@ -79,7 +113,9 @@ TableHead.displayName = "TableHead";
|
|
|
79
113
|
* Using `<tbody>` explicitly improves accessibility and allows browsers to
|
|
80
114
|
* scroll the body independently of a fixed header. Multiple `<tbody>` elements
|
|
81
115
|
* are valid and useful for visually separating logical row groups within a
|
|
82
|
-
* single table.
|
|
116
|
+
* single table — render more than one `Table.Body` as siblings for that case.
|
|
117
|
+
*
|
|
118
|
+
* @extends HTMLTableSectionElement
|
|
83
119
|
*
|
|
84
120
|
* @example
|
|
85
121
|
* ```tsx
|
|
@@ -103,7 +139,11 @@ TableBody.displayName = "TableBody";
|
|
|
103
139
|
*
|
|
104
140
|
* Typically used for summary rows — totals, averages, counts. When a table is
|
|
105
141
|
* printed across multiple pages some browsers repeat the `<tfoot>` content at
|
|
106
|
-
* the bottom of each page.
|
|
142
|
+
* the bottom of each page. Renders as a sibling of
|
|
143
|
+
* {@link TableHead | `Table.Head`} and {@link TableBody | `Table.Body`}
|
|
144
|
+
* inside {@link TableRoot | `Table.Root`}.
|
|
145
|
+
*
|
|
146
|
+
* @extends HTMLTableSectionElement
|
|
107
147
|
*
|
|
108
148
|
* @example
|
|
109
149
|
* ```tsx
|
|
@@ -128,9 +168,13 @@ TableFooter.displayName = "TableFooter";
|
|
|
128
168
|
/**
|
|
129
169
|
* An individual table row — renders a `<tr>` element.
|
|
130
170
|
*
|
|
131
|
-
* May contain
|
|
132
|
-
*
|
|
133
|
-
*
|
|
171
|
+
* May contain {@link TableHeader | `Table.Header`} (`<th>`) or
|
|
172
|
+
* {@link TableCell | `Table.Cell`} (`<td>`) children, or a mix of both when
|
|
173
|
+
* the row contains both header and data cells (e.g. the first column of each
|
|
174
|
+
* row is a row header). Lives inside {@link TableHead | `Table.Head`},
|
|
175
|
+
* {@link TableBody | `Table.Body`}, or {@link TableFooter | `Table.Footer`}.
|
|
176
|
+
*
|
|
177
|
+
* @extends HTMLTableRowElement
|
|
134
178
|
*
|
|
135
179
|
* @example
|
|
136
180
|
* ```tsx
|
|
@@ -166,6 +210,8 @@ TableRow.displayName = "TableRow";
|
|
|
166
210
|
* All `ThHTMLAttributes` (including `colSpan`, `rowSpan`, `abbr`) pass
|
|
167
211
|
* through to the DOM.
|
|
168
212
|
*
|
|
213
|
+
* @extends HTMLTableCellElement
|
|
214
|
+
*
|
|
169
215
|
* @example Column header
|
|
170
216
|
* ```tsx
|
|
171
217
|
* <Table.Header scope="col">Name</Table.Header>
|
|
@@ -190,7 +236,11 @@ TableHeader.displayName = "TableHeader";
|
|
|
190
236
|
* A data cell — renders a `<td>` element.
|
|
191
237
|
*
|
|
192
238
|
* All `TdHTMLAttributes` pass through to the DOM, including `colSpan` and
|
|
193
|
-
* `rowSpan` for spanning multiple columns or rows.
|
|
239
|
+
* `rowSpan` for spanning multiple columns or rows. Use
|
|
240
|
+
* {@link TableHeader | `Table.Header`} instead when a cell's content is a
|
|
241
|
+
* label for a row or column rather than data.
|
|
242
|
+
*
|
|
243
|
+
* @extends HTMLTableCellElement
|
|
194
244
|
*
|
|
195
245
|
* @example Basic cell
|
|
196
246
|
* ```tsx
|
|
@@ -224,15 +274,17 @@ TableCell.displayName = "TableCell";
|
|
|
224
274
|
*
|
|
225
275
|
* ```tsx
|
|
226
276
|
* <Table.ScrollArea style={{ borderRadius: "8px" }}>
|
|
227
|
-
*
|
|
277
|
+
* ...
|
|
228
278
|
* </Table.ScrollArea>
|
|
229
279
|
* ```
|
|
230
280
|
*
|
|
281
|
+
* @extends HTMLDivElement
|
|
282
|
+
*
|
|
231
283
|
* @example
|
|
232
284
|
* ```tsx
|
|
233
285
|
* <Table.ScrollArea>
|
|
234
286
|
* <Table.Root>
|
|
235
|
-
*
|
|
287
|
+
* ...
|
|
236
288
|
* </Table.Root>
|
|
237
289
|
* </Table.ScrollArea>
|
|
238
290
|
* ```
|
|
@@ -277,11 +329,13 @@ TableScrollArea.displayName = "TableScrollArea";
|
|
|
277
329
|
* | `"bottom"` (default) | Below the table |
|
|
278
330
|
* | `"top"` | Above the table |
|
|
279
331
|
*
|
|
332
|
+
* @extends HTMLTableCaptionElement
|
|
333
|
+
*
|
|
280
334
|
* @example Default (caption below table)
|
|
281
335
|
* ```tsx
|
|
282
336
|
* <Table.Root>
|
|
283
337
|
* <Table.Caption>Q1 sales figures by region</Table.Caption>
|
|
284
|
-
*
|
|
338
|
+
* ...
|
|
285
339
|
* </Table.Root>
|
|
286
340
|
* ```
|
|
287
341
|
*
|
|
@@ -289,7 +343,7 @@ TableScrollArea.displayName = "TableScrollArea";
|
|
|
289
343
|
* ```tsx
|
|
290
344
|
* <Table.Root>
|
|
291
345
|
* <Table.Caption captionSide="top">Q1 sales figures by region</Table.Caption>
|
|
292
|
-
*
|
|
346
|
+
* ...
|
|
293
347
|
* </Table.Root>
|
|
294
348
|
* ```
|
|
295
349
|
*/
|
|
@@ -308,11 +362,184 @@ export function TableCaption({
|
|
|
308
362
|
/** @internal */
|
|
309
363
|
TableCaption.displayName = "TableCaption";
|
|
310
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Pairs one expandable {@link TableRow | `Table.Row`} with its
|
|
367
|
+
* {@link TableDetailRow | `Table.DetailRow`}, sharing the id that wires the
|
|
368
|
+
* trigger's `aria-controls` to the detail row.
|
|
369
|
+
*
|
|
370
|
+
* Renders **no DOM of its own** — it is a context provider, so the two `<tr>`
|
|
371
|
+
* elements stay direct children of the `<tbody>` and the table's structure is
|
|
372
|
+
* untouched.
|
|
373
|
+
*/
|
|
374
|
+
export function TableExpandable({
|
|
375
|
+
children,
|
|
376
|
+
expanded: expandedProp,
|
|
377
|
+
defaultExpanded = false,
|
|
378
|
+
onExpandedChange,
|
|
379
|
+
}: TableExpandableProps): ReactElement {
|
|
380
|
+
const reactId = useId();
|
|
381
|
+
const [expanded, setExpanded] = useControllableState(
|
|
382
|
+
expandedProp,
|
|
383
|
+
defaultExpanded,
|
|
384
|
+
onExpandedChange,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const value = useMemo(
|
|
388
|
+
() => ({
|
|
389
|
+
expanded,
|
|
390
|
+
// The id is opaque: the only contracts are that the trigger's
|
|
391
|
+
// aria-controls matches the detail row's id, and that two Expandables on
|
|
392
|
+
// one page differ — both of which `useId` alone guarantees. The suffixes
|
|
393
|
+
// are for legibility when inspecting the DOM, so emptying them yields an
|
|
394
|
+
// equally valid id.
|
|
395
|
+
// Stryker disable next-line StringLiteral: equivalent — see above.
|
|
396
|
+
detailId: deriveId(reactId, "detail", "row"),
|
|
397
|
+
toggle: () => setExpanded(!expanded),
|
|
398
|
+
}),
|
|
399
|
+
[expanded, reactId, setExpanded],
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<TableExpandableContext.Provider value={value}>
|
|
404
|
+
{children}
|
|
405
|
+
</TableExpandableContext.Provider>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** @internal */
|
|
410
|
+
TableExpandable.displayName = "TableExpandable";
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The disclosure control for an expandable row — renders a `<button>` inside
|
|
414
|
+
* one of the row's cells.
|
|
415
|
+
*
|
|
416
|
+
* The **button** carries the state, not the row. `role="row"` does list
|
|
417
|
+
* `aria-expanded` among its supported properties, but a button's expanded state
|
|
418
|
+
* is announced universally while a row's is only reliably announced inside a
|
|
419
|
+
* `role="treegrid"` — so the button owns `aria-expanded` and `aria-controls`.
|
|
420
|
+
*
|
|
421
|
+
* Give it a name that identifies its row ("Show details for Ada Lovelace", or
|
|
422
|
+
* visually-hidden text beside the glyph). A column of buttons all called
|
|
423
|
+
* "Expand" is the same defect as a column of checkboxes all called "Select":
|
|
424
|
+
* obvious on screen, useless in a screen reader's element list.
|
|
425
|
+
*
|
|
426
|
+
* @extends HTMLButtonElement
|
|
427
|
+
*/
|
|
428
|
+
export function TableExpandTrigger({
|
|
429
|
+
children,
|
|
430
|
+
asChild = false,
|
|
431
|
+
onClick,
|
|
432
|
+
...rest
|
|
433
|
+
}: TableExpandTriggerProps): ReactElement {
|
|
434
|
+
const { expanded, detailId, toggle } = useTableExpandableContext();
|
|
435
|
+
|
|
436
|
+
const wiring = {
|
|
437
|
+
"aria-expanded": expanded,
|
|
438
|
+
"aria-controls": detailId,
|
|
439
|
+
"data-state": expanded ? "open" : "closed",
|
|
440
|
+
onClick: (event: MouseEvent<HTMLButtonElement>) => {
|
|
441
|
+
onClick?.(event);
|
|
442
|
+
toggle();
|
|
443
|
+
},
|
|
444
|
+
...rest,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// `type` is set only on the native button: a Slot child may not be a
|
|
448
|
+
// <button> at all, and `type` on a non-button element is invalid HTML.
|
|
449
|
+
return asChild ? (
|
|
450
|
+
<Slot {...wiring}>{children}</Slot>
|
|
451
|
+
) : (
|
|
452
|
+
<button type="button" {...wiring}>
|
|
453
|
+
{children}
|
|
454
|
+
</button>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** @internal */
|
|
459
|
+
TableExpandTrigger.displayName = "TableExpandTrigger";
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The revealed panel for an expandable row — a `<tr>` holding a `<td colSpan>`,
|
|
463
|
+
* and the target of its {@link TableExpandTrigger | `Table.ExpandTrigger`}'s
|
|
464
|
+
* `aria-controls`. Must be rendered inside a
|
|
465
|
+
* {@link TableExpandable | `Table.Expandable`}, as the sibling of the data row.
|
|
466
|
+
*
|
|
467
|
+
* This is a **disclosure**, not a treegrid: the panel holds detail *about* the
|
|
468
|
+
* row (a summary, a form, a chart), not child rows. Expanding into hierarchical
|
|
469
|
+
* child rows is a different pattern with a different role and keyboard model.
|
|
470
|
+
*
|
|
471
|
+
* Two things it leaves to you, both because only the caller can know them:
|
|
472
|
+
* {@link TableDetailRowProps.colSpan | `colSpan`}, since counting columns would
|
|
473
|
+
* mean the table registering its own, and
|
|
474
|
+
* {@link TableDetailRowProps.gutter | `gutter`}, which aligns the panel with the
|
|
475
|
+
* first data column by spanning the control columns in front of it.
|
|
476
|
+
*
|
|
477
|
+
* Collapsed, the row is `hidden` — so it cannot be reached, and cannot inflate
|
|
478
|
+
* the table's announced row count. Pass
|
|
479
|
+
* {@link TableDetailRowProps.forceMount | `forceMount`} when you are animating
|
|
480
|
+
* it: `display: none` cannot transition, so the row stays in the layout and
|
|
481
|
+
* takes `aria-hidden` instead, which keeps the row count honest either way.
|
|
482
|
+
*
|
|
483
|
+
* @extends HTMLTableRowElement
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```tsx
|
|
487
|
+
* <Table.Expandable>
|
|
488
|
+
* <Table.Row>
|
|
489
|
+
* <Table.Cell>
|
|
490
|
+
* <Table.ExpandTrigger aria-label="Show details for harmoni-engine">
|
|
491
|
+
* ▸
|
|
492
|
+
* </Table.ExpandTrigger>
|
|
493
|
+
* </Table.Cell>
|
|
494
|
+
* <Table.Cell>harmoni-engine</Table.Cell>
|
|
495
|
+
* </Table.Row>
|
|
496
|
+
* <Table.DetailRow colSpan={2} gutter={1}>
|
|
497
|
+
* <DeploymentSummary />
|
|
498
|
+
* </Table.DetailRow>
|
|
499
|
+
* </Table.Expandable>
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
export function TableDetailRow({
|
|
503
|
+
children,
|
|
504
|
+
colSpan,
|
|
505
|
+
gutter = 0,
|
|
506
|
+
forceMount = false,
|
|
507
|
+
cellProps,
|
|
508
|
+
...rest
|
|
509
|
+
}: TableDetailRowProps): ReactElement | null {
|
|
510
|
+
const { expanded, detailId } = useTableExpandableContext();
|
|
511
|
+
const collapsed = !expanded;
|
|
512
|
+
|
|
513
|
+
return (
|
|
514
|
+
<tr
|
|
515
|
+
id={detailId}
|
|
516
|
+
data-state={collapsed ? "closed" : "open"}
|
|
517
|
+
hidden={collapsed && !forceMount}
|
|
518
|
+
aria-hidden={collapsed && forceMount ? true : undefined}
|
|
519
|
+
{...rest}
|
|
520
|
+
>
|
|
521
|
+
{/* ONE spanning cell rather than `gutter` empty ones: a spanning cell
|
|
522
|
+
takes the full width of the columns it covers, so the alignment is
|
|
523
|
+
identical and the row gains a single blank cell instead of several. */}
|
|
524
|
+
{gutter > 0 ? <td colSpan={gutter} /> : null}
|
|
525
|
+
<td colSpan={colSpan - gutter} {...cellProps}>
|
|
526
|
+
{children}
|
|
527
|
+
</td>
|
|
528
|
+
</tr>
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** @internal */
|
|
533
|
+
TableDetailRow.displayName = "TableDetailRow";
|
|
534
|
+
|
|
311
535
|
/**
|
|
312
536
|
* The shape of the exported `Table` value — callable as `Table.Root` and
|
|
313
537
|
* carrying every sub-component as a static property.
|
|
314
538
|
*/
|
|
315
539
|
export type TableCompound = typeof TableRoot & {
|
|
540
|
+
Expandable: typeof TableExpandable;
|
|
541
|
+
ExpandTrigger: typeof TableExpandTrigger;
|
|
542
|
+
DetailRow: typeof TableDetailRow;
|
|
316
543
|
Root: typeof TableRoot;
|
|
317
544
|
Head: typeof TableHead;
|
|
318
545
|
Body: typeof TableBody;
|
|
@@ -370,7 +597,7 @@ export type TableCompound = typeof TableRoot & {
|
|
|
370
597
|
* @example Responsive — horizontal scroll on narrow viewports
|
|
371
598
|
* ```tsx
|
|
372
599
|
* <Table.ScrollArea>
|
|
373
|
-
* <Table.Root
|
|
600
|
+
* <Table.Root>...</Table.Root>
|
|
374
601
|
* </Table.ScrollArea>
|
|
375
602
|
* ```
|
|
376
603
|
*
|
|
@@ -389,6 +616,9 @@ export type TableCompound = typeof TableRoot & {
|
|
|
389
616
|
* @see {@link TableScrollArea} for the horizontal-scroll style caveat.
|
|
390
617
|
*/
|
|
391
618
|
const TableCompound: TableCompound = Object.assign(TableRoot, {
|
|
619
|
+
Expandable: TableExpandable,
|
|
620
|
+
ExpandTrigger: TableExpandTrigger,
|
|
621
|
+
DetailRow: TableDetailRow,
|
|
392
622
|
Root: TableRoot,
|
|
393
623
|
Head: TableHead,
|
|
394
624
|
Body: TableBody,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createStrictContext } from "../utils/index.ts";
|
|
2
|
+
import { TableExpandableContextValue } from "./types";
|
|
3
|
+
|
|
4
|
+
export const [TableExpandableContext, useTableExpandableContext] =
|
|
5
|
+
createStrictContext<TableExpandableContextValue>(
|
|
6
|
+
"Table.ExpandTrigger and Table.DetailRow must be rendered inside a <Table.Expandable>.",
|
|
7
|
+
);
|