@primitiv-ui/react 0.1.0
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 +79 -0
- package/package.json +59 -0
- package/src/AccessibleIcon/AccessibleIcon.tsx +40 -0
- package/src/AccessibleIcon/README.md +42 -0
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +47 -0
- package/src/AccessibleIcon/index.ts +2 -0
- package/src/AccessibleIcon/types.ts +8 -0
- package/src/Accordion/Accordion.tsx +412 -0
- package/src/Accordion/AccordionContext.ts +12 -0
- package/src/Accordion/README.md +202 -0
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +237 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +333 -0
- package/src/Accordion/__tests__/Accordion.controlled-state.test.tsx +175 -0
- package/src/Accordion/__tests__/Accordion.data-attributes.test.tsx +272 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +311 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +119 -0
- package/src/Accordion/__tests__/Accordion.forceMount.test.tsx +119 -0
- package/src/Accordion/__tests__/Accordion.keyboard-interaction.test.tsx +736 -0
- package/src/Accordion/__tests__/Accordion.mouse-interaction.test.tsx +212 -0
- package/src/Accordion/__tests__/Accordion.multiple-mode.test.tsx +90 -0
- package/src/Accordion/__tests__/Accordion.reading-direction.test.tsx +139 -0
- package/src/Accordion/__tests__/Accordion.uncontrolled-state.test.tsx +154 -0
- package/src/Accordion/hooks/index.ts +6 -0
- package/src/Accordion/hooks/useAccordionContext.ts +1 -0
- package/src/Accordion/hooks/useAccordionHeaderContext.ts +10 -0
- package/src/Accordion/hooks/useAccordionItem.ts +22 -0
- package/src/Accordion/hooks/useAccordionItemContext.ts +1 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +151 -0
- package/src/Accordion/hooks/useAccordionTrigger.ts +90 -0
- package/src/Accordion/index.ts +1 -0
- package/src/Accordion/types.ts +81 -0
- package/src/Alert/Alert.tsx +43 -0
- package/src/Alert/README.md +54 -0
- package/src/Alert/__tests__/Alert.test.tsx +28 -0
- package/src/Alert/index.ts +2 -0
- package/src/Alert/types.ts +5 -0
- package/src/Avatar/Avatar.tsx +149 -0
- package/src/Avatar/AvatarContext.ts +20 -0
- package/src/Avatar/README.md +116 -0
- package/src/Avatar/__tests__/Avatar.asChild.test.tsx +53 -0
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +14 -0
- package/src/Avatar/__tests__/Avatar.error-handling.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +75 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +81 -0
- package/src/Avatar/hooks/index.ts +2 -0
- package/src/Avatar/hooks/useAvatarContext.ts +1 -0
- package/src/Avatar/hooks/useAvatarImage.ts +40 -0
- package/src/Avatar/index.ts +3 -0
- package/src/Avatar/types.ts +44 -0
- package/src/Breadcrumb/Breadcrumb.tsx +234 -0
- package/src/Breadcrumb/README.md +111 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.asChild.test.tsx +33 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +132 -0
- package/src/Breadcrumb/index.ts +2 -0
- package/src/Breadcrumb/types.ts +22 -0
- package/src/Button/Button.tsx +95 -0
- package/src/Button/README.md +112 -0
- package/src/Button/__tests__/Button.asChild.test.tsx +91 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +126 -0
- package/src/Button/__tests__/Button.contract.test.tsx +72 -0
- package/src/Button/__tests__/Button.disabled.test.tsx +52 -0
- package/src/Button/__tests__/Button.icon-usage.test.tsx +57 -0
- package/src/Button/__tests__/Button.keyboard-interaction.test.tsx +70 -0
- package/src/Button/index.ts +2 -0
- package/src/Button/types.ts +8 -0
- package/src/Carousel/Carousel.tsx +708 -0
- package/src/Carousel/CarouselContext.ts +11 -0
- package/src/Carousel/README.md +848 -0
- package/src/Carousel/__tests__/Carousel.asChild.test.tsx +178 -0
- package/src/Carousel/__tests__/Carousel.auto-play.test.tsx +617 -0
- package/src/Carousel/__tests__/Carousel.basic-rendering.test.tsx +569 -0
- package/src/Carousel/__tests__/Carousel.controlled-state.test.tsx +137 -0
- package/src/Carousel/__tests__/Carousel.error-handling.test.tsx +81 -0
- package/src/Carousel/__tests__/Carousel.ids.test.tsx +111 -0
- package/src/Carousel/__tests__/Carousel.imperative-api.test.tsx +213 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +560 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +276 -0
- package/src/Carousel/__tests__/Carousel.keyboard-navigation.test.tsx +158 -0
- package/src/Carousel/__tests__/Carousel.play-pause.test.tsx +232 -0
- package/src/Carousel/__tests__/Carousel.prev-next.test.tsx +68 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +49 -0
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +87 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +179 -0
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +109 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +151 -0
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +183 -0
- package/src/Carousel/__tests__/Carousel.touch-interaction.test.tsx +96 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +70 -0
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +157 -0
- package/src/Carousel/__tests__/Carousel.uncontrolled-state.test.tsx +146 -0
- package/src/Carousel/hooks/index.ts +4 -0
- package/src/Carousel/hooks/useCarouselContext.ts +13 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +450 -0
- package/src/Carousel/hooks/useCarouselSlide.ts +45 -0
- package/src/Carousel/hooks/useCarouselViewport.ts +290 -0
- package/src/Carousel/index.ts +3 -0
- package/src/Carousel/types.ts +400 -0
- package/src/Checkbox/Checkbox.tsx +228 -0
- package/src/Checkbox/CheckboxContext.ts +12 -0
- package/src/Checkbox/README.md +156 -0
- package/src/Checkbox/__tests__/Checkbox.asChild.test.tsx +69 -0
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +41 -0
- package/src/Checkbox/__tests__/Checkbox.controlled-state.test.tsx +82 -0
- package/src/Checkbox/__tests__/Checkbox.disabled.test.tsx +15 -0
- package/src/Checkbox/__tests__/Checkbox.indeterminate.test.tsx +82 -0
- package/src/Checkbox/__tests__/Checkbox.indicator.test.tsx +117 -0
- package/src/Checkbox/__tests__/Checkbox.uncontrolled-state.test.tsx +89 -0
- package/src/Checkbox/hooks/index.ts +2 -0
- package/src/Checkbox/hooks/useCheckboxContext.ts +1 -0
- package/src/Checkbox/hooks/useCheckboxRoot.ts +32 -0
- package/src/Checkbox/index.ts +1 -0
- package/src/Checkbox/types.ts +33 -0
- package/src/CheckboxCard/CheckboxCard.tsx +208 -0
- package/src/CheckboxCard/CheckboxCardContext.ts +12 -0
- package/src/CheckboxCard/README.md +114 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.asChild.test.tsx +54 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +58 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.controlled-state.test.tsx +77 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.disabled.test.tsx +55 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.error-handling.test.tsx +20 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.indeterminate.test.tsx +60 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.indicator.test.tsx +136 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.uncontrolled-state.test.tsx +73 -0
- package/src/CheckboxCard/hooks/index.ts +2 -0
- package/src/CheckboxCard/hooks/useCheckboxCardContext.ts +1 -0
- package/src/CheckboxCard/hooks/useCheckboxCardRoot.ts +30 -0
- package/src/CheckboxCard/index.ts +3 -0
- package/src/CheckboxCard/types.ts +33 -0
- package/src/Collapsible/Collapsible.tsx +316 -0
- package/src/Collapsible/CollapsibleContext.ts +7 -0
- package/src/Collapsible/README.md +174 -0
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +240 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +118 -0
- package/src/Collapsible/__tests__/Collapsible.controlled-state.test.tsx +134 -0
- package/src/Collapsible/__tests__/Collapsible.disabled.test.tsx +132 -0
- package/src/Collapsible/__tests__/Collapsible.error-handling.test.tsx +40 -0
- package/src/Collapsible/__tests__/Collapsible.forceMount.test.tsx +111 -0
- package/src/Collapsible/__tests__/Collapsible.triggerIcon.test.tsx +93 -0
- package/src/Collapsible/__tests__/Collapsible.uncontrolled-state.test.tsx +125 -0
- package/src/Collapsible/hooks/index.ts +2 -0
- package/src/Collapsible/hooks/useCollapsibleRoot.ts +34 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +49 -0
- package/src/Collapsible/index.ts +1 -0
- package/src/Collapsible/types.ts +48 -0
- package/src/ContextMenu/ContextMenu.tsx +1004 -0
- package/src/ContextMenu/ContextMenuContentContext.ts +15 -0
- package/src/ContextMenu/ContextMenuContext.ts +21 -0
- package/src/ContextMenu/ContextMenuGroupContext.ts +8 -0
- package/src/ContextMenu/ContextMenuItemIndicatorContext.ts +8 -0
- package/src/ContextMenu/ContextMenuRadioGroupContext.ts +9 -0
- package/src/ContextMenu/ContextMenuSubContext.ts +15 -0
- package/src/ContextMenu/README.md +275 -0
- package/src/ContextMenu/__tests__/ContextMenu.asChild.test.tsx +186 -0
- package/src/ContextMenu/__tests__/ContextMenu.basic-rendering.test.tsx +39 -0
- package/src/ContextMenu/__tests__/ContextMenu.checkbox-item.test.tsx +145 -0
- package/src/ContextMenu/__tests__/ContextMenu.error-handling.test.tsx +113 -0
- package/src/ContextMenu/__tests__/ContextMenu.group-label.test.tsx +48 -0
- package/src/ContextMenu/__tests__/ContextMenu.item-indicator.test.tsx +88 -0
- package/src/ContextMenu/__tests__/ContextMenu.item.test.tsx +106 -0
- package/src/ContextMenu/__tests__/ContextMenu.keyboard-interaction.test.tsx +172 -0
- package/src/ContextMenu/__tests__/ContextMenu.mouse-interaction.test.tsx +227 -0
- package/src/ContextMenu/__tests__/ContextMenu.radio-item.test.tsx +127 -0
- package/src/ContextMenu/__tests__/ContextMenu.reading-direction.test.tsx +152 -0
- package/src/ContextMenu/__tests__/ContextMenu.separator.test.tsx +47 -0
- package/src/ContextMenu/__tests__/ContextMenu.state-modes.test.tsx +119 -0
- package/src/ContextMenu/__tests__/ContextMenu.sub.test.tsx +262 -0
- package/src/ContextMenu/__tests__/ContextMenu.typeahead.test.tsx +89 -0
- package/src/ContextMenu/constants.ts +4 -0
- package/src/ContextMenu/index.ts +1 -0
- package/src/ContextMenu/types.ts +199 -0
- package/src/DirectionProvider/DirectionContext.ts +21 -0
- package/src/DirectionProvider/DirectionProvider.tsx +31 -0
- package/src/DirectionProvider/README.md +62 -0
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +29 -0
- package/src/DirectionProvider/index.ts +3 -0
- package/src/DirectionProvider/types.ts +10 -0
- package/src/Divider/Divider.tsx +57 -0
- package/src/Divider/README.md +57 -0
- package/src/Divider/__tests__/Divider.test.tsx +41 -0
- package/src/Divider/index.ts +1 -0
- package/src/Divider/types.ts +5 -0
- package/src/Dropdown/Dropdown.tsx +842 -0
- package/src/Dropdown/DropdownContentContext.ts +15 -0
- package/src/Dropdown/DropdownContext.ts +17 -0
- package/src/Dropdown/DropdownGroupContext.ts +8 -0
- package/src/Dropdown/DropdownItemIndicatorContext.ts +13 -0
- package/src/Dropdown/DropdownRadioGroupContext.ts +9 -0
- package/src/Dropdown/DropdownSubContext.ts +15 -0
- package/src/Dropdown/README.md +284 -0
- package/src/Dropdown/__tests__/Dropdown.asChild.test.tsx +286 -0
- package/src/Dropdown/__tests__/Dropdown.basic-rendering.test.tsx +43 -0
- package/src/Dropdown/__tests__/Dropdown.checkbox-item.test.tsx +121 -0
- package/src/Dropdown/__tests__/Dropdown.disabled.test.tsx +143 -0
- package/src/Dropdown/__tests__/Dropdown.error-handling.test.tsx +85 -0
- package/src/Dropdown/__tests__/Dropdown.group-label.test.tsx +68 -0
- package/src/Dropdown/__tests__/Dropdown.item-indicator.test.tsx +260 -0
- package/src/Dropdown/__tests__/Dropdown.item.test.tsx +72 -0
- package/src/Dropdown/__tests__/Dropdown.keyboard-edge-cases.test.tsx +77 -0
- package/src/Dropdown/__tests__/Dropdown.keyboard-interaction.test.tsx +310 -0
- package/src/Dropdown/__tests__/Dropdown.mouse-interaction.test.tsx +347 -0
- package/src/Dropdown/__tests__/Dropdown.radio-item.test.tsx +134 -0
- package/src/Dropdown/__tests__/Dropdown.reading-direction.test.tsx +153 -0
- package/src/Dropdown/__tests__/Dropdown.separator.test.tsx +46 -0
- package/src/Dropdown/__tests__/Dropdown.state-modes.test.tsx +100 -0
- package/src/Dropdown/__tests__/Dropdown.sub.test.tsx +185 -0
- package/src/Dropdown/__tests__/Dropdown.trigger.test.tsx +110 -0
- package/src/Dropdown/__tests__/Dropdown.typeahead.test.tsx +133 -0
- package/src/Dropdown/constants.ts +4 -0
- package/src/Dropdown/hooks/index.ts +9 -0
- package/src/Dropdown/hooks/useCloseSiblingSub.ts +13 -0
- package/src/Dropdown/hooks/useDropdownContent.ts +162 -0
- package/src/Dropdown/hooks/useDropdownContext.ts +1 -0
- package/src/Dropdown/hooks/useDropdownGroup.ts +18 -0
- package/src/Dropdown/hooks/useDropdownItem.ts +49 -0
- package/src/Dropdown/hooks/useDropdownLabel.ts +15 -0
- package/src/Dropdown/hooks/useDropdownRoot.ts +57 -0
- package/src/Dropdown/hooks/useDropdownSubContext.ts +1 -0
- package/src/Dropdown/hooks/useDropdownTrigger.ts +31 -0
- package/src/Dropdown/index.ts +1 -0
- package/src/Dropdown/types.ts +200 -0
- package/src/EmptyState/EmptyState.tsx +245 -0
- package/src/EmptyState/README.md +129 -0
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +32 -0
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +30 -0
- package/src/EmptyState/__tests__/EmptyState.Media.test.tsx +34 -0
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +28 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +26 -0
- package/src/EmptyState/index.ts +2 -0
- package/src/EmptyState/types.ts +21 -0
- package/src/Field/Field.tsx +239 -0
- package/src/Field/FieldContext.ts +22 -0
- package/src/Field/README.md +167 -0
- package/src/Field/__tests__/Field.asChild.test.tsx +83 -0
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +104 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +75 -0
- package/src/Field/hooks/index.ts +2 -0
- package/src/Field/hooks/useFieldContext.ts +1 -0
- package/src/Field/hooks/useFieldProps.ts +57 -0
- package/src/Field/index.ts +2 -0
- package/src/Field/types.ts +33 -0
- package/src/Fieldset/Fieldset.tsx +104 -0
- package/src/Fieldset/README.md +74 -0
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +81 -0
- package/src/Fieldset/__tests__/Fieldset.disabled.test.tsx +41 -0
- package/src/Fieldset/index.ts +2 -0
- package/src/Fieldset/types.ts +5 -0
- package/src/Input/Input.tsx +120 -0
- package/src/Input/README.md +180 -0
- package/src/Input/__tests__/Input.asChild.test.tsx +85 -0
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +118 -0
- package/src/Input/__tests__/Input.disabled.test.tsx +49 -0
- package/src/Input/__tests__/Input.field-integration.test.tsx +148 -0
- package/src/Input/index.ts +2 -0
- package/src/Input/types.ts +7 -0
- package/src/InputGroup/InputGroup.tsx +228 -0
- package/src/InputGroup/README.md +178 -0
- package/src/InputGroup/__tests__/InputGroup.asChild.test.tsx +109 -0
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +106 -0
- package/src/InputGroup/index.ts +2 -0
- package/src/InputGroup/types.ts +13 -0
- package/src/MillerColumns/MillerColumns.tsx +329 -0
- package/src/MillerColumns/MillerColumnsContext.ts +25 -0
- package/src/MillerColumns/README.md +278 -0
- package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +82 -0
- package/src/MillerColumns/__tests__/MillerColumns.asChild.test.tsx +106 -0
- package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +68 -0
- package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +52 -0
- package/src/MillerColumns/__tests__/MillerColumns.column-projection.test.tsx +161 -0
- package/src/MillerColumns/__tests__/MillerColumns.controlled-state.test.tsx +90 -0
- package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +77 -0
- package/src/MillerColumns/__tests__/MillerColumns.disabled-items.test.tsx +65 -0
- package/src/MillerColumns/__tests__/MillerColumns.error-handling.test.tsx +57 -0
- package/src/MillerColumns/__tests__/MillerColumns.fixtures.ts +15 -0
- package/src/MillerColumns/__tests__/MillerColumns.item-indicator.test.tsx +57 -0
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +181 -0
- package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +47 -0
- package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +137 -0
- package/src/MillerColumns/__tests__/MillerColumns.roving-tabindex.test.tsx +91 -0
- package/src/MillerColumns/__tests__/MillerColumns.selection.test.tsx +54 -0
- package/src/MillerColumns/__tests__/MillerColumns.uncontrolled-state.test.tsx +70 -0
- package/src/MillerColumns/hooks/index.ts +7 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +23 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumnContext.ts +1 -0
- package/src/MillerColumns/hooks/useMillerColumnsContext.ts +1 -0
- package/src/MillerColumns/hooks/useMillerColumnsItem.ts +157 -0
- package/src/MillerColumns/hooks/useMillerColumnsItemContext.ts +1 -0
- package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +76 -0
- package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
- package/src/MillerColumns/index.ts +3 -0
- package/src/MillerColumns/types.ts +93 -0
- package/src/MillerColumns/useMillerColumnsSelection.ts +31 -0
- package/src/MillerColumns/utils.ts +75 -0
- package/src/Modal/Modal.tsx +474 -0
- package/src/Modal/ModalContext.ts +13 -0
- package/src/Modal/README.md +207 -0
- package/src/Modal/__tests__/Modal.accessibility.test.tsx +167 -0
- package/src/Modal/__tests__/Modal.asChild.test.tsx +162 -0
- package/src/Modal/__tests__/Modal.click-outside.test.tsx +115 -0
- package/src/Modal/__tests__/Modal.content.test.tsx +193 -0
- package/src/Modal/__tests__/Modal.controlled-state.test.tsx +120 -0
- package/src/Modal/__tests__/Modal.error-handling.test.tsx +30 -0
- package/src/Modal/__tests__/Modal.escape-hatches.test.tsx +99 -0
- package/src/Modal/__tests__/Modal.imperative-api.test.tsx +119 -0
- package/src/Modal/__tests__/Modal.nested.test.tsx +106 -0
- package/src/Modal/__tests__/Modal.overlay.test.tsx +99 -0
- package/src/Modal/__tests__/Modal.portal.test.tsx +90 -0
- package/src/Modal/__tests__/Modal.presence.test.tsx +111 -0
- package/src/Modal/__tests__/Modal.trigger.test.tsx +70 -0
- package/src/Modal/__tests__/Modal.uncontrolled-state.test.tsx +72 -0
- package/src/Modal/__tests__/dialog-polyfill.ts +40 -0
- package/src/Modal/hooks/index.ts +4 -0
- package/src/Modal/hooks/useModalContent.ts +62 -0
- package/src/Modal/hooks/useModalContext.ts +1 -0
- package/src/Modal/hooks/useModalRoot.ts +81 -0
- package/src/Modal/hooks/useModalTrigger.ts +25 -0
- package/src/Modal/index.ts +3 -0
- package/src/Modal/types.ts +76 -0
- package/src/Portal/Portal.tsx +28 -0
- package/src/Portal/README.md +70 -0
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +17 -0
- package/src/Portal/index.ts +2 -0
- package/src/Portal/types.ts +6 -0
- package/src/Progress/Progress.tsx +178 -0
- package/src/Progress/ProgressContext.ts +15 -0
- package/src/Progress/README.md +112 -0
- package/src/Progress/__tests__/Progress.asChild.test.tsx +37 -0
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +65 -0
- package/src/Progress/__tests__/Progress.error-handling.test.tsx +40 -0
- package/src/Progress/__tests__/Progress.fixtures.ts +7 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +83 -0
- package/src/Progress/hooks/index.ts +2 -0
- package/src/Progress/hooks/useProgressContext.ts +1 -0
- package/src/Progress/hooks/useProgressRoot.ts +45 -0
- package/src/Progress/index.ts +3 -0
- package/src/Progress/types.ts +43 -0
- package/src/RadioCard/README.md +133 -0
- package/src/RadioCard/RadioCard.tsx +334 -0
- package/src/RadioCard/RadioCardContext.ts +23 -0
- package/src/RadioCard/RadioCardItemContext.ts +10 -0
- package/src/RadioCard/__tests__/RadioCard.asChild.test.tsx +76 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +87 -0
- package/src/RadioCard/__tests__/RadioCard.controlled-state.test.tsx +107 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +61 -0
- package/src/RadioCard/__tests__/RadioCard.error-handling.test.tsx +35 -0
- package/src/RadioCard/__tests__/RadioCard.indicator.test.tsx +119 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +158 -0
- package/src/RadioCard/__tests__/RadioCard.orientation.test.tsx +90 -0
- package/src/RadioCard/__tests__/RadioCard.reading-direction.test.tsx +65 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +108 -0
- package/src/RadioCard/hooks/index.ts +3 -0
- package/src/RadioCard/hooks/useRadioCardContext.ts +1 -0
- package/src/RadioCard/hooks/useRadioCardItemContext.ts +1 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +77 -0
- package/src/RadioCard/index.ts +4 -0
- package/src/RadioCard/types.ts +51 -0
- package/src/RadioGroup/README.md +185 -0
- package/src/RadioGroup/RadioGroup.tsx +353 -0
- package/src/RadioGroup/RadioGroupContext.ts +23 -0
- package/src/RadioGroup/RadioGroupItemContext.ts +10 -0
- package/src/RadioGroup/__tests__/RadioGroup.asChild.test.tsx +105 -0
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +72 -0
- package/src/RadioGroup/__tests__/RadioGroup.controlled-state.test.tsx +109 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled-keydown-guards.test.tsx +68 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +79 -0
- package/src/RadioGroup/__tests__/RadioGroup.error-handling.test.tsx +33 -0
- package/src/RadioGroup/__tests__/RadioGroup.indicator.test.tsx +85 -0
- package/src/RadioGroup/__tests__/RadioGroup.keyboard-interaction.test.tsx +135 -0
- package/src/RadioGroup/__tests__/RadioGroup.orientation.test.tsx +90 -0
- package/src/RadioGroup/__tests__/RadioGroup.reading-direction.test.tsx +65 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +45 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +105 -0
- package/src/RadioGroup/__tests__/RadioGroup.uncontrolled-state.test.tsx +96 -0
- package/src/RadioGroup/hooks/index.ts +3 -0
- package/src/RadioGroup/hooks/useRadioGroupContext.ts +1 -0
- package/src/RadioGroup/hooks/useRadioGroupItemContext.ts +1 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +87 -0
- package/src/RadioGroup/index.ts +1 -0
- package/src/RadioGroup/types.ts +51 -0
- package/src/Select/README.md +203 -0
- package/src/Select/Select.tsx +204 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +36 -0
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +17 -0
- package/src/Select/__tests__/Select.controlled-state.test.tsx +69 -0
- package/src/Select/__tests__/Select.data-attributes.test.tsx +29 -0
- package/src/Select/__tests__/Select.field-integration.test.tsx +150 -0
- package/src/Select/__tests__/Select.group.test.tsx +42 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +32 -0
- package/src/Select/index.ts +2 -0
- package/src/Select/types.ts +89 -0
- package/src/SkipNav/README.md +87 -0
- package/src/SkipNav/SkipNav.tsx +116 -0
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +23 -0
- package/src/SkipNav/__tests__/SkipNav.ids.test.tsx +19 -0
- package/src/SkipNav/index.ts +1 -0
- package/src/SkipNav/types.ts +26 -0
- package/src/Slider/README.md +215 -0
- package/src/Slider/Slider.tsx +308 -0
- package/src/Slider/SliderContext.ts +24 -0
- package/src/Slider/__tests__/Slider.asChild.test.tsx +119 -0
- package/src/Slider/__tests__/Slider.basic-rendering.test.tsx +157 -0
- package/src/Slider/__tests__/Slider.controlled-state.test.tsx +78 -0
- package/src/Slider/__tests__/Slider.disabled.test.tsx +82 -0
- package/src/Slider/__tests__/Slider.error-handling.test.tsx +45 -0
- package/src/Slider/__tests__/Slider.fixtures.ts +53 -0
- package/src/Slider/__tests__/Slider.form.test.tsx +67 -0
- package/src/Slider/__tests__/Slider.inverted.test.tsx +112 -0
- package/src/Slider/__tests__/Slider.keyboard-interaction.test.tsx +118 -0
- package/src/Slider/__tests__/Slider.multiple-thumbs.test.tsx +84 -0
- package/src/Slider/__tests__/Slider.orientation.test.tsx +101 -0
- package/src/Slider/__tests__/Slider.pointer-interaction.test.tsx +205 -0
- package/src/Slider/__tests__/Slider.reading-direction.test.tsx +99 -0
- package/src/Slider/__tests__/Slider.uncontrolled-state.test.tsx +69 -0
- package/src/Slider/__tests__/Slider.value-commit.test.tsx +103 -0
- package/src/Slider/hooks/index.ts +3 -0
- package/src/Slider/hooks/useSliderContext.ts +1 -0
- package/src/Slider/hooks/useSliderRoot.ts +197 -0
- package/src/Slider/hooks/useSliderThumb.ts +77 -0
- package/src/Slider/index.ts +3 -0
- package/src/Slider/types.ts +48 -0
- package/src/Slider/utils.ts +155 -0
- package/src/Slot/Slot.tsx +158 -0
- package/src/Slot/__tests__/Slot.test.tsx +163 -0
- package/src/Slot/__tests__/composeEventHandlers.test.ts +74 -0
- package/src/Slot/composeEventHandlers.ts +38 -0
- package/src/Slot/index.ts +3 -0
- package/src/Slot/types.ts +5 -0
- package/src/Status/README.md +50 -0
- package/src/Status/Status.tsx +44 -0
- package/src/Status/__tests__/Status.test.tsx +28 -0
- package/src/Status/index.ts +2 -0
- package/src/Status/types.ts +5 -0
- package/src/Switch/README.md +121 -0
- package/src/Switch/Switch.tsx +167 -0
- package/src/Switch/SwitchContext.ts +10 -0
- package/src/Switch/__tests__/Switch.asChild.test.tsx +56 -0
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +76 -0
- package/src/Switch/__tests__/Switch.contract.test.tsx +109 -0
- package/src/Switch/__tests__/Switch.controlled-state.test.tsx +79 -0
- package/src/Switch/__tests__/Switch.disabled.test.tsx +60 -0
- package/src/Switch/__tests__/Switch.error-handling.test.tsx +20 -0
- package/src/Switch/__tests__/Switch.keyboard-interaction.test.tsx +56 -0
- package/src/Switch/__tests__/Switch.thumb.test.tsx +84 -0
- package/src/Switch/__tests__/Switch.uncontrolled-state.test.tsx +83 -0
- package/src/Switch/hooks/index.ts +2 -0
- package/src/Switch/hooks/useSwitchContext.ts +1 -0
- package/src/Switch/hooks/useSwitchRoot.ts +28 -0
- package/src/Switch/index.ts +3 -0
- package/src/Switch/types.ts +37 -0
- package/src/Table/README.md +205 -0
- package/src/Table/Table.tsx +380 -0
- package/src/Table/__tests__/Table.Body.test.tsx +61 -0
- package/src/Table/__tests__/Table.Caption.test.tsx +70 -0
- package/src/Table/__tests__/Table.Cell.test.tsx +73 -0
- package/src/Table/__tests__/Table.Footer.test.tsx +61 -0
- package/src/Table/__tests__/Table.Head.test.tsx +61 -0
- package/src/Table/__tests__/Table.Header.test.tsx +73 -0
- package/src/Table/__tests__/Table.Root.test.tsx +49 -0
- package/src/Table/__tests__/Table.Row.test.tsx +67 -0
- package/src/Table/__tests__/Table.ScrollArea.test.tsx +83 -0
- package/src/Table/index.ts +1 -0
- package/src/Table/types.ts +63 -0
- package/src/Tabs/README.md +110 -0
- package/src/Tabs/Tabs.tsx +434 -0
- package/src/Tabs/TabsContext.ts +13 -0
- package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +114 -0
- package/src/Tabs/__tests__/Tabs.asChild.test.tsx +91 -0
- package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +483 -0
- package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +133 -0
- package/src/Tabs/__tests__/Tabs.controlled-state.test.tsx +152 -0
- package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +203 -0
- package/src/Tabs/__tests__/Tabs.error-handling.test.tsx +82 -0
- package/src/Tabs/__tests__/Tabs.fixtures.ts +171 -0
- package/src/Tabs/__tests__/Tabs.imperative-api.test.tsx +118 -0
- package/src/Tabs/__tests__/Tabs.keyboard-interaction.test.tsx +192 -0
- package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +61 -0
- package/src/Tabs/__tests__/Tabs.mouse-interaction.test.tsx +216 -0
- package/src/Tabs/__tests__/Tabs.reading-direction.test.tsx +58 -0
- package/src/Tabs/__tests__/Tabs.uncontrolled-state.test.tsx +197 -0
- package/src/Tabs/hooks/index.ts +4 -0
- package/src/Tabs/hooks/useTabsContent.ts +27 -0
- package/src/Tabs/hooks/useTabsContext.ts +1 -0
- package/src/Tabs/hooks/useTabsRoot.ts +148 -0
- package/src/Tabs/hooks/useTabsTrigger.ts +111 -0
- package/src/Tabs/index.ts +3 -0
- package/src/Tabs/types.ts +99 -0
- package/src/Tabs/utils.ts +8 -0
- package/src/Textarea/README.md +98 -0
- package/src/Textarea/Textarea.tsx +93 -0
- package/src/Textarea/__tests__/Textarea.asChild.test.tsx +85 -0
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +107 -0
- package/src/Textarea/__tests__/Textarea.disabled.test.tsx +49 -0
- package/src/Textarea/__tests__/Textarea.field-integration.test.tsx +134 -0
- package/src/Textarea/index.ts +2 -0
- package/src/Textarea/types.ts +7 -0
- package/src/Toggle/README.md +97 -0
- package/src/Toggle/Toggle.tsx +81 -0
- package/src/Toggle/__tests__/Toggle.asChild.test.tsx +42 -0
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +28 -0
- package/src/Toggle/__tests__/Toggle.controlled-state.test.tsx +60 -0
- package/src/Toggle/__tests__/Toggle.disabled.test.tsx +34 -0
- package/src/Toggle/__tests__/Toggle.keyboard-interaction.test.tsx +42 -0
- package/src/Toggle/__tests__/Toggle.uncontrolled-state.test.tsx +40 -0
- package/src/Toggle/index.ts +2 -0
- package/src/Toggle/types.ts +23 -0
- package/src/ToggleGroup/README.md +137 -0
- package/src/ToggleGroup/ToggleGroup.tsx +298 -0
- package/src/ToggleGroup/ToggleGroupContext.ts +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +65 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +50 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.disabled.test.tsx +54 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +151 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.multiple-mode.test.tsx +144 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.reading-direction.test.tsx +28 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.single-mode.test.tsx +139 -0
- package/src/ToggleGroup/hooks/index.ts +2 -0
- package/src/ToggleGroup/hooks/useToggleGroupContext.ts +1 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +110 -0
- package/src/ToggleGroup/index.ts +2 -0
- package/src/ToggleGroup/types.ts +72 -0
- package/src/Tooltip/README.md +214 -0
- package/src/Tooltip/Tooltip.tsx +260 -0
- package/src/Tooltip/TooltipContext.ts +20 -0
- package/src/Tooltip/__tests__/Tooltip.asChild.test.tsx +77 -0
- package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +180 -0
- package/src/Tooltip/__tests__/Tooltip.controlled-state.test.tsx +128 -0
- package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
- package/src/Tooltip/__tests__/Tooltip.focus-interaction.test.tsx +88 -0
- package/src/Tooltip/__tests__/Tooltip.hover-interaction.test.tsx +179 -0
- package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +85 -0
- package/src/Tooltip/__tests__/Tooltip.uncontrolled-state.test.tsx +67 -0
- package/src/Tooltip/hooks/index.ts +4 -0
- package/src/Tooltip/hooks/useTooltipContent.ts +53 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +41 -0
- package/src/Tooltip/hooks/useTooltipRoot.ts +106 -0
- package/src/Tooltip/hooks/useTooltipTrigger.ts +44 -0
- package/src/Tooltip/index.ts +1 -0
- package/src/Tooltip/types.ts +64 -0
- package/src/Tree/README.md +339 -0
- package/src/Tree/Tree.tsx +571 -0
- package/src/Tree/TreeContext.ts +24 -0
- package/src/Tree/__tests__/Tree.aria.test.tsx +53 -0
- package/src/Tree/__tests__/Tree.asChild.test.tsx +134 -0
- package/src/Tree/__tests__/Tree.basic-rendering.test.tsx +111 -0
- package/src/Tree/__tests__/Tree.branch-behaviour.test.tsx +87 -0
- package/src/Tree/__tests__/Tree.controlled-expansion.test.tsx +92 -0
- package/src/Tree/__tests__/Tree.data-attributes.test.tsx +88 -0
- package/src/Tree/__tests__/Tree.disabled-items.test.tsx +196 -0
- package/src/Tree/__tests__/Tree.error-handling.test.tsx +71 -0
- package/src/Tree/__tests__/Tree.forceMount.test.tsx +72 -0
- package/src/Tree/__tests__/Tree.keyboard-interaction.test.tsx +150 -0
- package/src/Tree/__tests__/Tree.multiple-selection.test.tsx +151 -0
- package/src/Tree/__tests__/Tree.range-selection.test.tsx +200 -0
- package/src/Tree/__tests__/Tree.recursion-depth.test.tsx +73 -0
- package/src/Tree/__tests__/Tree.roving-tabindex.test.tsx +117 -0
- package/src/Tree/__tests__/Tree.selection-path.test.tsx +404 -0
- package/src/Tree/__tests__/Tree.single-selection.test.tsx +108 -0
- package/src/Tree/__tests__/Tree.uncontrolled-expansion.test.tsx +69 -0
- package/src/Tree/hooks/index.ts +3 -0
- package/src/Tree/hooks/useTreeItemKeyboard.ts +86 -0
- package/src/Tree/hooks/useTreePath.ts +68 -0
- package/src/Tree/hooks/useTreeRoot.ts +279 -0
- package/src/Tree/index.ts +3 -0
- package/src/Tree/types.ts +224 -0
- package/src/Tree/utils.ts +59 -0
- package/src/VisuallyHidden/README.md +58 -0
- package/src/VisuallyHidden/VisuallyHidden.tsx +67 -0
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +59 -0
- package/src/VisuallyHidden/index.ts +2 -0
- package/src/VisuallyHidden/types.ts +5 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useCollection.ts +74 -0
- package/src/hooks/useControllableState.ts +81 -0
- package/src/hooks/useRovingTabindex.ts +178 -0
- package/src/index.ts +38 -0
- package/src/test/intersectionObserverPolyfill.ts +83 -0
- package/src/test/popoverPolyfill.ts +86 -0
- package/src/test/scrollPolyfill.ts +23 -0
- package/src/types.ts +13 -0
- package/src/utils/__tests__/createStrictContext.test.tsx +69 -0
- package/src/utils/__tests__/deriveId.test.ts +28 -0
- package/src/utils/__tests__/getKeyToActionMap.test.ts +106 -0
- package/src/utils/createStrictContext.ts +49 -0
- package/src/utils/deriveId.ts +31 -0
- package/src/utils/getKeyToActionMap.ts +95 -0
- package/src/utils/index.ts +3 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
A compound component implementing the
|
|
4
|
+
[WAI-ARIA Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import { Tabs } from "@primitiv-ui/react";
|
|
8
|
+
|
|
9
|
+
<Tabs.Root defaultValue="overview">
|
|
10
|
+
<Tabs.List label="Account sections">
|
|
11
|
+
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
|
12
|
+
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
|
13
|
+
</Tabs.List>
|
|
14
|
+
<Tabs.Content value="overview">Dashboard…</Tabs.Content>
|
|
15
|
+
<Tabs.Content value="settings">Preferences…</Tabs.Content>
|
|
16
|
+
</Tabs.Root>;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Sub-components
|
|
20
|
+
|
|
21
|
+
| Export | Role | Notes |
|
|
22
|
+
| -------------- | ----------- | ---------------------------------------------------------------------------- |
|
|
23
|
+
| `Tabs.Root` | State owner | Uncontrolled (`defaultValue`) or controlled (`value` + `onValueChange`) |
|
|
24
|
+
| `Tabs.List` | `tablist` | Requires `label` or `ariaLabelledBy` for accessibility |
|
|
25
|
+
| `Tabs.Trigger` | `tab` | Supports `asChild` to render any element with tab semantics |
|
|
26
|
+
| `Tabs.Content` | `tabpanel` | Stays mounted when inactive; use `lazyMount` or conditional rendering for deferred/unmount semantics |
|
|
27
|
+
|
|
28
|
+
## Keyboard interaction
|
|
29
|
+
|
|
30
|
+
| Key | Behaviour |
|
|
31
|
+
| -------------------------- | ------------------------------------------------- |
|
|
32
|
+
| `ArrowRight` / `ArrowLeft` | Move between triggers (horizontal tabs) |
|
|
33
|
+
| `ArrowDown` / `ArrowUp` | Move between triggers (vertical tabs) |
|
|
34
|
+
| `Home` / `End` | Jump to first / last trigger |
|
|
35
|
+
| `Enter` / `Space` | Activate focused trigger (manual activation mode) |
|
|
36
|
+
| `Tab` | Move from tablist into the active panel |
|
|
37
|
+
|
|
38
|
+
## Reading direction
|
|
39
|
+
|
|
40
|
+
Pass `dir` (`"ltr"` / `"rtl"`) to set the arrow-key direction for horizontal
|
|
41
|
+
tabs and the container's `dir` attribute. When `dir` is omitted, it is
|
|
42
|
+
inherited from the nearest [`DirectionProvider`](../DirectionProvider/README.md),
|
|
43
|
+
falling back to `"ltr"` when there is no provider. An explicit `dir` prop
|
|
44
|
+
always wins over the inherited value.
|
|
45
|
+
|
|
46
|
+
## State modes
|
|
47
|
+
|
|
48
|
+
- **Uncontrolled** — pass `defaultValue` (or omit for no initial selection).
|
|
49
|
+
- **Controlled** — pass `value` and `onValueChange` together.
|
|
50
|
+
|
|
51
|
+
## Activation modes
|
|
52
|
+
|
|
53
|
+
- `activationMode="automatic"` (default) — arrow keys immediately activate the panel.
|
|
54
|
+
- `activationMode="manual"` — arrow keys move focus only; `Enter`/`Space` confirms.
|
|
55
|
+
|
|
56
|
+
## Lazy mounting
|
|
57
|
+
|
|
58
|
+
Pass `lazyMount` on `Tabs.Root` to defer rendering a panel's children until
|
|
59
|
+
that tab is first activated. Once mounted, children remain in the DOM across
|
|
60
|
+
subsequent tab switches (lazy mount, not unmount-on-hide). The
|
|
61
|
+
`<div role="tabpanel">` wrapper always renders so the ARIA relationship between
|
|
62
|
+
trigger and panel is always present.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<Tabs.Root defaultValue="overview" lazyMount>
|
|
66
|
+
…
|
|
67
|
+
</Tabs.Root>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This is useful when a panel owns expensive initialisation that depends on the
|
|
71
|
+
element being visible — for example, a scroll-snap carousel whose initial scroll
|
|
72
|
+
position must be computed while the panel has real dimensions.
|
|
73
|
+
|
|
74
|
+
## Imperative API
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
const ref = useRef<TabsImperativeApi>(null);
|
|
78
|
+
<Tabs.Root ref={ref} defaultValue="a">
|
|
79
|
+
…
|
|
80
|
+
</Tabs.Root>;
|
|
81
|
+
ref.current?.setActiveTab("b");
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## `asChild` composition
|
|
85
|
+
|
|
86
|
+
`Tabs.Trigger` accepts an `asChild` prop to render any child element with
|
|
87
|
+
full tab semantics. All ARIA attributes, event handlers, and the roving
|
|
88
|
+
`tabIndex` are merged onto the child following the asChild composition
|
|
89
|
+
pattern (child handler runs first, then the trigger's):
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<Tabs.Trigger asChild value="settings">
|
|
93
|
+
<Link to="/settings">Settings</Link>
|
|
94
|
+
</Tabs.Trigger>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Styling hooks
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
[role="tab"][data-state="active"] {
|
|
101
|
+
border-bottom: 2px solid currentColor;
|
|
102
|
+
}
|
|
103
|
+
[role="tabpanel"][data-state="active"] {
|
|
104
|
+
display: block;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Both `data-state` (`"active"` | `"inactive"`) and
|
|
109
|
+
`data-orientation` (`"horizontal"` | `"vertical"`) are available on
|
|
110
|
+
every rendered element.
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { forwardRef, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
import { useDirection } from "../DirectionProvider";
|
|
4
|
+
import { Slot, composeRefs } from "../Slot";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
useTabsRoot,
|
|
8
|
+
useTabsContext,
|
|
9
|
+
useTabsTrigger,
|
|
10
|
+
useTabsContent,
|
|
11
|
+
} from "./hooks";
|
|
12
|
+
import { TabsProvider } from "./TabsContext";
|
|
13
|
+
import type {
|
|
14
|
+
TabsRootProps,
|
|
15
|
+
TabsListProps,
|
|
16
|
+
TabsTriggerProps,
|
|
17
|
+
TabsContentProps,
|
|
18
|
+
TabsImperativeApi,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The root of a Tabs widget — owns the active value, provides context to
|
|
23
|
+
* descendants, and renders a single container `<div>`.
|
|
24
|
+
*
|
|
25
|
+
* Supports two state modes, statically discriminated at the type level so
|
|
26
|
+
* only one of the two shapes is accepted by TypeScript:
|
|
27
|
+
*
|
|
28
|
+
* - **Uncontrolled** — pass {@link TabsRootProps.defaultValue | `defaultValue`}
|
|
29
|
+
* (or omit it and let the first click seed the state). The component
|
|
30
|
+
* owns and updates the active value internally.
|
|
31
|
+
* - **Controlled** — pass {@link TabsRootProps.value | `value`} *and*
|
|
32
|
+
* {@link TabsRootProps.onValueChange | `onValueChange`} together. The
|
|
33
|
+
* parent owns the active value; the component defers every state change
|
|
34
|
+
* back through the callback.
|
|
35
|
+
*
|
|
36
|
+
* An additional {@link TabsRootProps.onChange | `onChange({ index, name })`}
|
|
37
|
+
* callback fires on every user-driven activation (click or keyboard)
|
|
38
|
+
* independent of the control mode. Use it for analytics or side-effects
|
|
39
|
+
* that shouldn't re-enter the state update path.
|
|
40
|
+
*
|
|
41
|
+
* An imperative handle is exposed via `ref`, exposing
|
|
42
|
+
* {@link TabsImperativeApi.setActiveTab | `setActiveTab(value)`} for
|
|
43
|
+
* programmatic activation (e.g. restoring a remembered tab on mount, or
|
|
44
|
+
* reacting to a deep-linked hash):
|
|
45
|
+
*
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const tabsRef = useRef<TabsImperativeApi>(null);
|
|
48
|
+
* tabsRef.current?.setActiveTab("settings");
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* **Runtime validation.** If `value`/`defaultValue` doesn't match any
|
|
52
|
+
* registered `Tabs.Trigger`, a descriptive error is thrown during render
|
|
53
|
+
* and typos surface early in development.
|
|
54
|
+
*
|
|
55
|
+
* **Styling hooks.** `data-orientation="horizontal" | "vertical"` is set
|
|
56
|
+
* on the rendered container.
|
|
57
|
+
*
|
|
58
|
+
* **Reading direction.** `dir` (`"ltr"` / `"rtl"`) sets the arrow-key
|
|
59
|
+
* direction and the container's `dir` attribute. When omitted, it is
|
|
60
|
+
* inherited from the nearest {@link DirectionProvider}, falling back to
|
|
61
|
+
* `"ltr"` when there is no provider.
|
|
62
|
+
*
|
|
63
|
+
* @example Uncontrolled
|
|
64
|
+
* ```tsx
|
|
65
|
+
* <Tabs.Root defaultValue="overview">
|
|
66
|
+
* <Tabs.List label="Account sections">
|
|
67
|
+
* <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
|
68
|
+
* <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
|
69
|
+
* </Tabs.List>
|
|
70
|
+
* <Tabs.Content value="overview">Dashboard…</Tabs.Content>
|
|
71
|
+
* <Tabs.Content value="settings">Preferences…</Tabs.Content>
|
|
72
|
+
* </Tabs.Root>
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example Controlled with an analytics hook
|
|
76
|
+
* ```tsx
|
|
77
|
+
* const [tab, setTab] = useState("overview");
|
|
78
|
+
*
|
|
79
|
+
* <Tabs.Root
|
|
80
|
+
* value={tab}
|
|
81
|
+
* onValueChange={setTab}
|
|
82
|
+
* onChange={({ name, index }) => track("tab_view", { name, index })}
|
|
83
|
+
* >
|
|
84
|
+
* …
|
|
85
|
+
* </Tabs.Root>
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example Vertical orientation + imperative API
|
|
89
|
+
* ```tsx
|
|
90
|
+
* const ref = useRef<TabsImperativeApi>(null);
|
|
91
|
+
*
|
|
92
|
+
* <Tabs.Root ref={ref} orientation="vertical" defaultValue="one">
|
|
93
|
+
* …
|
|
94
|
+
* </Tabs.Root>
|
|
95
|
+
*
|
|
96
|
+
* <button onClick={() => ref.current?.setActiveTab("two")}>Go to two</button>
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
const TabsRoot = forwardRef<TabsImperativeApi, TabsRootProps>(function TabsRoot(
|
|
100
|
+
{
|
|
101
|
+
className = "",
|
|
102
|
+
orientation = "horizontal",
|
|
103
|
+
dir,
|
|
104
|
+
activationMode = "automatic",
|
|
105
|
+
defaultValue,
|
|
106
|
+
value,
|
|
107
|
+
onValueChange,
|
|
108
|
+
onChange,
|
|
109
|
+
lazyMount = false,
|
|
110
|
+
...rest
|
|
111
|
+
},
|
|
112
|
+
ref,
|
|
113
|
+
) {
|
|
114
|
+
const resolvedDir = dir ?? useDirection();
|
|
115
|
+
const { contextValue } = useTabsRoot(
|
|
116
|
+
{
|
|
117
|
+
orientation,
|
|
118
|
+
dir: resolvedDir,
|
|
119
|
+
activationMode,
|
|
120
|
+
defaultValue,
|
|
121
|
+
value,
|
|
122
|
+
onValueChange,
|
|
123
|
+
onChange,
|
|
124
|
+
lazyMount,
|
|
125
|
+
},
|
|
126
|
+
ref,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<TabsProvider value={contextValue}>
|
|
131
|
+
<div
|
|
132
|
+
dir={resolvedDir}
|
|
133
|
+
className={className}
|
|
134
|
+
data-orientation={contextValue.orientation}
|
|
135
|
+
{...rest}
|
|
136
|
+
/>
|
|
137
|
+
</TabsProvider>
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
TabsRoot.displayName = "TabsRoot";
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The tablist — an accessible container for `Tabs.Trigger` elements.
|
|
145
|
+
*
|
|
146
|
+
* Renders a `<div role="tablist">` with `aria-orientation` inherited from
|
|
147
|
+
* the nearest {@link TabsRoot | `Tabs.Root`}. The
|
|
148
|
+
* {@link TabsListProps.label | `label`} prop is **required** and becomes
|
|
149
|
+
* the `aria-label` announced to assistive technology when the tablist
|
|
150
|
+
* receives focus — pick a short, human-readable description of the set of
|
|
151
|
+
* tabs (e.g. `"Account sections"`, not `"Tabs"`).
|
|
152
|
+
*
|
|
153
|
+
* **Styling hooks.** `data-orientation="horizontal" | "vertical"`.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```tsx
|
|
157
|
+
* <Tabs.List label="Document sections">
|
|
158
|
+
* <Tabs.Trigger value="title">Title</Tabs.Trigger>
|
|
159
|
+
* <Tabs.Trigger value="body">Body</Tabs.Trigger>
|
|
160
|
+
* </Tabs.List>
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export function TabsList({
|
|
164
|
+
children,
|
|
165
|
+
className = "",
|
|
166
|
+
label,
|
|
167
|
+
ariaLabelledBy,
|
|
168
|
+
...rest
|
|
169
|
+
}: TabsListProps) {
|
|
170
|
+
const { orientation } = useTabsContext();
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div
|
|
174
|
+
role="tablist"
|
|
175
|
+
className={className}
|
|
176
|
+
aria-orientation={orientation}
|
|
177
|
+
{...(label && {
|
|
178
|
+
"aria-label": label,
|
|
179
|
+
})}
|
|
180
|
+
{...(ariaLabelledBy && {
|
|
181
|
+
"aria-labelledby": ariaLabelledBy,
|
|
182
|
+
})}
|
|
183
|
+
data-orientation={orientation}
|
|
184
|
+
{...rest}
|
|
185
|
+
>
|
|
186
|
+
{children}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
TabsList.displayName = "TabsList";
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* An individual tab button. Renders `<button role="tab">` with full ARIA
|
|
195
|
+
* linkage, roving tabindex, and keyboard navigation handled automatically.
|
|
196
|
+
*
|
|
197
|
+
* **Value linkage.** Each trigger is identified by a unique
|
|
198
|
+
* {@link TabsTriggerProps.value | `value`} string; the matching
|
|
199
|
+
* `Tabs.Content` must share the same value. The IDs used for
|
|
200
|
+
* `aria-controls` and `aria-labelledby` are derived from the root's
|
|
201
|
+
* `useId()` plus the trigger's `value`, so they're stable and unique even
|
|
202
|
+
* when multiple `Tabs` instances coexist on a page.
|
|
203
|
+
*
|
|
204
|
+
* **Keyboard support** (WAI-ARIA Tabs pattern):
|
|
205
|
+
*
|
|
206
|
+
* | Key | Behaviour |
|
|
207
|
+
* | ---------------------------- | -------------------------------------------------- |
|
|
208
|
+
* | `ArrowRight` / `ArrowLeft` | Move between triggers (horizontal) |
|
|
209
|
+
* | `ArrowDown` / `ArrowUp` | Move between triggers (vertical) |
|
|
210
|
+
* | `Home` / `End` | Jump to first / last trigger |
|
|
211
|
+
* | `Enter` / `Space` | Activate focused trigger (manual mode only) |
|
|
212
|
+
*
|
|
213
|
+
* Movement **wraps** at the ends. In **automatic** activation mode (the default),
|
|
214
|
+
* focus movement immediately activates the panel. In **manual** mode, arrow keys
|
|
215
|
+
* move focus without switching the panel — `Enter` or `Space` confirms the
|
|
216
|
+
* selection. Use manual mode for panels that are expensive to render.
|
|
217
|
+
*
|
|
218
|
+
* **`asChild` prop.** Pass `asChild` to render an arbitrary child element
|
|
219
|
+
* instead of the default `<button>`. All tab ARIA attributes, event handlers,
|
|
220
|
+
* and the roving `tabIndex` are merged onto the child element following the
|
|
221
|
+
* Composition pattern:
|
|
222
|
+
* - Event handlers compose — the child's handler runs first, then the trigger's.
|
|
223
|
+
* - `style` is shallow-merged (child wins on collisions).
|
|
224
|
+
* - `className` strings are concatenated.
|
|
225
|
+
* - Refs from both sides are composed via `composeRefs`.
|
|
226
|
+
*
|
|
227
|
+
* The child **must** be a single React element that accepts a `ref`.
|
|
228
|
+
*
|
|
229
|
+
* **Styling hooks.**
|
|
230
|
+
* - `data-state="active" | "inactive"` on the rendered element.
|
|
231
|
+
* - `data-orientation="horizontal" | "vertical"`.
|
|
232
|
+
*
|
|
233
|
+
* @example Basic usage
|
|
234
|
+
* ```tsx
|
|
235
|
+
* <Tabs.Trigger value="account">Account</Tabs.Trigger>
|
|
236
|
+
* ```
|
|
237
|
+
*
|
|
238
|
+
* @example Icon + label
|
|
239
|
+
* ```tsx
|
|
240
|
+
* <Tabs.Trigger value="billing">
|
|
241
|
+
* <CreditCardIcon aria-hidden />
|
|
242
|
+
* <span>Billing</span>
|
|
243
|
+
* </Tabs.Trigger>
|
|
244
|
+
* ```
|
|
245
|
+
*
|
|
246
|
+
* @example asChild — render a router link with tab semantics
|
|
247
|
+
* ```tsx
|
|
248
|
+
* <Tabs.Trigger asChild value="settings">
|
|
249
|
+
* <Link to="/settings">Settings</Link>
|
|
250
|
+
* </Tabs.Trigger>
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
export function TabsTrigger<T extends HTMLElement = HTMLButtonElement>({
|
|
254
|
+
ref: externalRef,
|
|
255
|
+
children,
|
|
256
|
+
className = "",
|
|
257
|
+
value,
|
|
258
|
+
onClick,
|
|
259
|
+
disabled = false,
|
|
260
|
+
asChild = false,
|
|
261
|
+
...rest
|
|
262
|
+
}: TabsTriggerProps<T>) {
|
|
263
|
+
const {
|
|
264
|
+
buttonRef,
|
|
265
|
+
triggerId,
|
|
266
|
+
panelId,
|
|
267
|
+
isActive,
|
|
268
|
+
orientation,
|
|
269
|
+
state,
|
|
270
|
+
tabIndex,
|
|
271
|
+
handleClick,
|
|
272
|
+
handleKeyDown,
|
|
273
|
+
} = useTabsTrigger({ value, onClick, disabled });
|
|
274
|
+
|
|
275
|
+
// Compose our internal ref with any external ref the consumer passes.
|
|
276
|
+
// The external ref is cast to match the internal ref's element type —
|
|
277
|
+
// RefObject<T> is invariant in React's types, but at runtime the callback
|
|
278
|
+
// receives whatever DOM element is actually rendered (button or asChild).
|
|
279
|
+
const composedRef = externalRef
|
|
280
|
+
? composeRefs(buttonRef, externalRef as Ref<HTMLButtonElement>)
|
|
281
|
+
: buttonRef;
|
|
282
|
+
|
|
283
|
+
const triggerProps = {
|
|
284
|
+
ref: composedRef,
|
|
285
|
+
role: "tab" as const,
|
|
286
|
+
className,
|
|
287
|
+
id: triggerId,
|
|
288
|
+
"aria-controls": panelId,
|
|
289
|
+
"aria-selected": isActive,
|
|
290
|
+
"aria-disabled": disabled,
|
|
291
|
+
"data-disabled": disabled,
|
|
292
|
+
"data-orientation": orientation,
|
|
293
|
+
"data-state": state,
|
|
294
|
+
tabIndex,
|
|
295
|
+
onClick: handleClick,
|
|
296
|
+
onKeyDown: handleKeyDown,
|
|
297
|
+
...rest,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
if (asChild) {
|
|
301
|
+
return <Slot {...triggerProps}>{children}</Slot>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<button type="button" {...triggerProps}>
|
|
306
|
+
{children}
|
|
307
|
+
</button>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
TabsTrigger.displayName = "TabsTrigger";
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* A panel associated with a `Tabs.Trigger` of the same
|
|
315
|
+
* {@link TabsContentProps.value | `value`}.
|
|
316
|
+
*
|
|
317
|
+
* Renders `<div role="tabpanel">` with `aria-labelledby` pointing at the
|
|
318
|
+
* matching trigger, and uses the native `hidden` attribute to toggle
|
|
319
|
+
* visibility. Inactive panels remain **mounted** — this preserves
|
|
320
|
+
* component state (scroll position, form input, animation state) across
|
|
321
|
+
* tab switches. If you need true unmount semantics (e.g. to tear down
|
|
322
|
+
* expensive subscriptions), render the `Tabs.Content` conditionally
|
|
323
|
+
* yourself based on the active value.
|
|
324
|
+
*
|
|
325
|
+
* When {@link TabsRootProps.lazyMount | `lazyMount`} is set on
|
|
326
|
+
* `Tabs.Root`, a panel's children are withheld until the tab is first
|
|
327
|
+
* activated. After that first activation the children remain mounted
|
|
328
|
+
* across subsequent tab switches (lazy mount, not unmount-on-hide).
|
|
329
|
+
* The `<div role="tabpanel">` wrapper always renders so the ARIA
|
|
330
|
+
* relationship between trigger and panel is always present in the DOM.
|
|
331
|
+
*
|
|
332
|
+
* **Styling hooks.**
|
|
333
|
+
* - `data-state="active" | "inactive"`.
|
|
334
|
+
* - `data-orientation="horizontal" | "vertical"`.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```tsx
|
|
338
|
+
* <Tabs.Content value="account">
|
|
339
|
+
* <AccountForm />
|
|
340
|
+
* </Tabs.Content>
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
export function TabsContent({
|
|
344
|
+
children,
|
|
345
|
+
className = "",
|
|
346
|
+
value,
|
|
347
|
+
...rest
|
|
348
|
+
}: TabsContentProps) {
|
|
349
|
+
const { panelId, triggerId, orientation, isActive, state, tabIndex, shouldRender } =
|
|
350
|
+
useTabsContent({ value });
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<div
|
|
354
|
+
role="tabpanel"
|
|
355
|
+
className={className}
|
|
356
|
+
id={panelId}
|
|
357
|
+
aria-labelledby={triggerId}
|
|
358
|
+
data-orientation={orientation}
|
|
359
|
+
data-state={state}
|
|
360
|
+
hidden={!isActive}
|
|
361
|
+
tabIndex={tabIndex}
|
|
362
|
+
{...rest}
|
|
363
|
+
>
|
|
364
|
+
{shouldRender ? children : null}
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
TabsContent.displayName = "TabsContent";
|
|
370
|
+
|
|
371
|
+
type TabsCompound = typeof TabsRoot & {
|
|
372
|
+
Root: typeof TabsRoot;
|
|
373
|
+
List: typeof TabsList;
|
|
374
|
+
Trigger: typeof TabsTrigger;
|
|
375
|
+
Content: typeof TabsContent;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Headless, accessible **Tabs** — a compound component implementing the
|
|
380
|
+
* WAI-ARIA Tabs pattern with zero styles.
|
|
381
|
+
*
|
|
382
|
+
* `Tabs` is both callable (it's an alias of {@link TabsRoot | `Tabs.Root`})
|
|
383
|
+
* and carries its sub-components as static properties. Prefer the
|
|
384
|
+
* namespaced form in application code for readability and grep-ability:
|
|
385
|
+
*
|
|
386
|
+
* - {@link TabsRoot | `Tabs.Root`} — state owner, context provider, imperative API holder.
|
|
387
|
+
* - {@link TabsList | `Tabs.List`} — `role="tablist"` container for triggers.
|
|
388
|
+
* - {@link TabsTrigger | `Tabs.Trigger`} — individual `role="tab"` button.
|
|
389
|
+
* - {@link TabsContent | `Tabs.Content`} — `role="tabpanel"` panel, linked to a trigger by `value`.
|
|
390
|
+
*
|
|
391
|
+
* @example Minimal usage
|
|
392
|
+
* ```tsx
|
|
393
|
+
* import { Tabs } from "@primitiv-ui/react";
|
|
394
|
+
*
|
|
395
|
+
* export function Demo() {
|
|
396
|
+
* return (
|
|
397
|
+
* <Tabs.Root defaultValue="a">
|
|
398
|
+
* <Tabs.List label="Demo tabs">
|
|
399
|
+
* <Tabs.Trigger value="a">First</Tabs.Trigger>
|
|
400
|
+
* <Tabs.Trigger value="b">Second</Tabs.Trigger>
|
|
401
|
+
* </Tabs.List>
|
|
402
|
+
* <Tabs.Content value="a">First panel</Tabs.Content>
|
|
403
|
+
* <Tabs.Content value="b">Second panel</Tabs.Content>
|
|
404
|
+
* </Tabs.Root>
|
|
405
|
+
* );
|
|
406
|
+
* }
|
|
407
|
+
* ```
|
|
408
|
+
*
|
|
409
|
+
* @example Styling with any system
|
|
410
|
+
* Because no styles ship with the component, target the rendered elements
|
|
411
|
+
* or the `data-state` / `data-orientation` hooks with whatever system you
|
|
412
|
+
* use (CSS, CSS-in-JS, Tailwind, design-token stylesheets, etc.):
|
|
413
|
+
*
|
|
414
|
+
* ```css
|
|
415
|
+
* [role="tab"][data-state="active"] { border-bottom: 2px solid currentColor; }
|
|
416
|
+
* [role="tabpanel"][data-state="inactive"] { display: none; }
|
|
417
|
+
* ```
|
|
418
|
+
*
|
|
419
|
+
* @see {@link TabsRoot} for state modes, validation behaviour, and the
|
|
420
|
+
* imperative API.
|
|
421
|
+
* @see {@link TabsList} for the required `label` prop.
|
|
422
|
+
* @see {@link TabsTrigger} for the full keyboard-interaction table.
|
|
423
|
+
* @see {@link TabsContent} for panel lifecycle and unmount semantics.
|
|
424
|
+
*/
|
|
425
|
+
const TabsCompound: TabsCompound = Object.assign(TabsRoot, {
|
|
426
|
+
Root: TabsRoot,
|
|
427
|
+
List: TabsList,
|
|
428
|
+
Trigger: TabsTrigger,
|
|
429
|
+
Content: TabsContent,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
TabsCompound.displayName = "Tabs";
|
|
433
|
+
|
|
434
|
+
export { TabsCompound as Tabs };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createStrictContext } from "../utils";
|
|
2
|
+
|
|
3
|
+
import { TabsContextValue } from "./types";
|
|
4
|
+
|
|
5
|
+
export const [TabsContext, useTabsContext] =
|
|
6
|
+
createStrictContext<TabsContextValue>(
|
|
7
|
+
"Component must be rendered as a child of Tabs.Root",
|
|
8
|
+
"TabsContext",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const TabsProvider = TabsContext.Provider;
|
|
12
|
+
|
|
13
|
+
export { TabsProvider };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { Tabs } from "../Tabs";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
describe("Activation mode tests", () => {
|
|
6
|
+
it("should be in automatic mode by default", async () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const user = userEvent.setup();
|
|
9
|
+
render(
|
|
10
|
+
<Tabs.Root defaultValue="tab1">
|
|
11
|
+
<Tabs.List label="Test tabs">
|
|
12
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
13
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
14
|
+
</Tabs.List>
|
|
15
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
16
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
17
|
+
</Tabs.Root>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
await user.tab();
|
|
22
|
+
await user.keyboard("{ArrowRight}");
|
|
23
|
+
|
|
24
|
+
// Assert
|
|
25
|
+
const secondTabPanel = screen.getByRole("tabpanel", {
|
|
26
|
+
name: "Tab 2",
|
|
27
|
+
});
|
|
28
|
+
expect(secondTabPanel).not.toHaveAttribute("hidden");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should not activate the second tab when focusing its tab in manual mode", async () => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const user = userEvent.setup();
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<Tabs.Root defaultValue="tab1" activationMode="manual">
|
|
36
|
+
<Tabs.List label="Test tabs">
|
|
37
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
38
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
39
|
+
</Tabs.List>
|
|
40
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
41
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
42
|
+
</Tabs.Root>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
await user.tab();
|
|
47
|
+
await user.keyboard("{ArrowRight}");
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
const secondTabPanel = container.querySelectorAll('[role="tabpanel"]')[1];
|
|
51
|
+
expect(secondTabPanel).not.toBeVisible();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
{
|
|
56
|
+
defaultValue: "tab1",
|
|
57
|
+
direction: "next",
|
|
58
|
+
directionKeys: ["{ArrowRight}"],
|
|
59
|
+
key: " ",
|
|
60
|
+
expectedSelectedTab: "Tab 2",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
defaultValue: "tab2",
|
|
64
|
+
direction: "previous",
|
|
65
|
+
directionKeys: ["{ArrowLeft}"],
|
|
66
|
+
key: " ",
|
|
67
|
+
expectedSelectedTab: "Tab 1",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
defaultValue: "tab1",
|
|
71
|
+
direction: "next",
|
|
72
|
+
directionKeys: ["{ArrowRight}"],
|
|
73
|
+
key: "{Enter}",
|
|
74
|
+
expectedSelectedTab: "Tab 2",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
defaultValue: "tab2",
|
|
78
|
+
direction: "previous",
|
|
79
|
+
directionKeys: ["{ArrowLeft}"],
|
|
80
|
+
key: "{Enter}",
|
|
81
|
+
expectedSelectedTab: "Tab 1",
|
|
82
|
+
},
|
|
83
|
+
])(
|
|
84
|
+
"should activate the $direction tab when focusing it in manual mode and pressing the $key key",
|
|
85
|
+
async ({ defaultValue, directionKeys, key, expectedSelectedTab }) => {
|
|
86
|
+
// Arrange
|
|
87
|
+
const user = userEvent.setup();
|
|
88
|
+
render(
|
|
89
|
+
<Tabs.Root defaultValue={defaultValue} activationMode="manual">
|
|
90
|
+
<Tabs.List label="Test tabs">
|
|
91
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
92
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
93
|
+
</Tabs.List>
|
|
94
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
95
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
96
|
+
</Tabs.Root>,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Act
|
|
100
|
+
await user.tab();
|
|
101
|
+
for (const directionKey of directionKeys) {
|
|
102
|
+
await user.keyboard(directionKey);
|
|
103
|
+
}
|
|
104
|
+
await user.keyboard(key);
|
|
105
|
+
|
|
106
|
+
// Assert
|
|
107
|
+
const tab = screen.getByRole("tab", {
|
|
108
|
+
name: expectedSelectedTab,
|
|
109
|
+
});
|
|
110
|
+
expect(tab).toHaveFocus();
|
|
111
|
+
expect(tab).toHaveAttribute("aria-selected", "true");
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
});
|