@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,68 @@
|
|
|
1
|
+
import { useTreeContext } from "../TreeContext";
|
|
2
|
+
|
|
3
|
+
import type { TreePathSegment } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the root-to-leaf ancestor chain of a tree value as an array
|
|
7
|
+
* of {@link TreePathSegment}s, or an empty array if the value has
|
|
8
|
+
* never been registered in the tree.
|
|
9
|
+
*
|
|
10
|
+
* Must be called from a component rendered inside `Tree.Root`.
|
|
11
|
+
*
|
|
12
|
+
* The segments are ordered **root → leaf** so the last entry is the
|
|
13
|
+
* queried value itself. Each segment exposes its `label` (the `label`
|
|
14
|
+
* prop passed to `Tree.Item` / `Tree.Branch`, or `null` when omitted),
|
|
15
|
+
* `isBranch`, `disabled` and `depth` — enough to render breadcrumbs,
|
|
16
|
+
* outline trails, or location indicators without a parallel data
|
|
17
|
+
* lookup.
|
|
18
|
+
*
|
|
19
|
+
* Paths survive a branch collapsing without `forceMount`: the tree
|
|
20
|
+
* keeps a durable copy of every node's metadata so ancestry remains
|
|
21
|
+
* resolvable even after descendants unmount. A value never mounted
|
|
22
|
+
* (e.g. a pre-selected item whose branch has not yet opened) returns
|
|
23
|
+
* an empty array.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* function CurrentPath({ value }: { value: string }) {
|
|
28
|
+
* const path = useTreePath(value);
|
|
29
|
+
* return <>{path.map((s) => s.label ?? s.value).join(" / ")}</>;
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useTreePath(value: string): TreePathSegment[] {
|
|
34
|
+
const { getPath } = useTreeContext();
|
|
35
|
+
return getPath(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns one path per currently-selected value, in selection order.
|
|
40
|
+
* In single-selection mode the array has zero or one entry; in
|
|
41
|
+
* multiple-selection mode it has one entry per selected value.
|
|
42
|
+
*
|
|
43
|
+
* Each inner array is the same shape as the return of
|
|
44
|
+
* {@link useTreePath} — root → leaf segments with labels carried from
|
|
45
|
+
* the `label` prop.
|
|
46
|
+
*
|
|
47
|
+
* Used by `Tree.SelectionPath` for its default rendering, and
|
|
48
|
+
* available directly for fully-custom breadcrumb UIs.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* function Crumbs() {
|
|
53
|
+
* const paths = useTreeSelectionPaths();
|
|
54
|
+
* if (paths.length === 0) return <span>Nothing selected</span>;
|
|
55
|
+
* return (
|
|
56
|
+
* <ul>
|
|
57
|
+
* {paths.map((path, i) => (
|
|
58
|
+
* <li key={i}>{path.map((s) => s.label ?? s.value).join(" / ")}</li>
|
|
59
|
+
* ))}
|
|
60
|
+
* </ul>
|
|
61
|
+
* );
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function useTreeSelectionPaths(): TreePathSegment[][] {
|
|
66
|
+
const { selectedOrder, getPath } = useTreeContext();
|
|
67
|
+
return selectedOrder.map((value) => getPath(value));
|
|
68
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { useCallback, useId, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { useCollection, useControllableState } from "../../hooks";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
SelectionMode,
|
|
7
|
+
TreeContextValue,
|
|
8
|
+
TreeNodeMeta,
|
|
9
|
+
TreePathSegment,
|
|
10
|
+
TreeSelectModifiers,
|
|
11
|
+
} from "../types";
|
|
12
|
+
|
|
13
|
+
/** Defensive cap to short-circuit a cycle in `parentValue` pointers. */
|
|
14
|
+
const MAX_PATH_DEPTH = 64;
|
|
15
|
+
|
|
16
|
+
export type UseTreeRootOptions = {
|
|
17
|
+
expandedValues: string[] | undefined;
|
|
18
|
+
defaultExpandedValues: string[] | undefined;
|
|
19
|
+
onExpandedChange: ((values: string[]) => void) | undefined;
|
|
20
|
+
selectionMode: SelectionMode;
|
|
21
|
+
selectedValue: string | null | undefined;
|
|
22
|
+
defaultSelectedValue: string | null | undefined;
|
|
23
|
+
onSelectedValueChange: ((value: string | null) => void) | undefined;
|
|
24
|
+
selectedValues: string[] | undefined;
|
|
25
|
+
defaultSelectedValues: string[] | undefined;
|
|
26
|
+
onSelectedValuesChange: ((values: string[]) => void) | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function singleToArray(value: string | null | undefined): string[] | undefined {
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return value === null ? [] : [value];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Owns the Tree's expansion and selection state, the item collection
|
|
38
|
+
* used to compute the visible DFS order, and the anchor that pins
|
|
39
|
+
* Shift+click range selection.
|
|
40
|
+
*/
|
|
41
|
+
export function useTreeRoot(options: UseTreeRootOptions): TreeContextValue {
|
|
42
|
+
const rootId = useId();
|
|
43
|
+
|
|
44
|
+
const [expandedValues, setExpandedValues] = useControllableState<string[]>(
|
|
45
|
+
options.expandedValues,
|
|
46
|
+
options.defaultExpandedValues ?? [],
|
|
47
|
+
options.onExpandedChange,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const normalisedSelectedValues =
|
|
51
|
+
options.selectionMode === "multiple"
|
|
52
|
+
? options.selectedValues
|
|
53
|
+
: singleToArray(options.selectedValue);
|
|
54
|
+
|
|
55
|
+
const normalisedDefaultSelectedValues =
|
|
56
|
+
options.selectionMode === "multiple"
|
|
57
|
+
? (options.defaultSelectedValues ?? [])
|
|
58
|
+
: (singleToArray(options.defaultSelectedValue) ?? []);
|
|
59
|
+
|
|
60
|
+
const handleSelectedValuesChange = useCallback(
|
|
61
|
+
(next: string[]) => {
|
|
62
|
+
if (options.selectionMode === "multiple") {
|
|
63
|
+
options.onSelectedValuesChange?.(next);
|
|
64
|
+
} else {
|
|
65
|
+
options.onSelectedValueChange?.(next[0]!);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
[
|
|
69
|
+
options.selectionMode,
|
|
70
|
+
options.onSelectedValueChange,
|
|
71
|
+
options.onSelectedValuesChange,
|
|
72
|
+
],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const [selectedValues, setSelectedValues] = useControllableState<string[]>(
|
|
76
|
+
normalisedSelectedValues,
|
|
77
|
+
normalisedDefaultSelectedValues,
|
|
78
|
+
handleSelectedValuesChange,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const {
|
|
82
|
+
register: registerCollectionNode,
|
|
83
|
+
itemsRef,
|
|
84
|
+
keys,
|
|
85
|
+
} = useCollection<string, TreeNodeMeta>();
|
|
86
|
+
|
|
87
|
+
// Durable copy of every node meta ever registered. Unlike the
|
|
88
|
+
// collection above (which deletes on unmount so visible-order /
|
|
89
|
+
// focus reflect mounted state) this map keeps the last-seen entry
|
|
90
|
+
// for each value so `getPath` resolves ancestry even when an
|
|
91
|
+
// ancestor branch has collapsed without `forceMount` and its
|
|
92
|
+
// descendants have unmounted.
|
|
93
|
+
const pathMetaRef = useRef<Map<string, TreeNodeMeta>>(new Map());
|
|
94
|
+
const [pathMetaVersion, setPathMetaVersion] = useState(0);
|
|
95
|
+
|
|
96
|
+
const registerNode = useCallback(
|
|
97
|
+
(value: string, meta: TreeNodeMeta | null) => {
|
|
98
|
+
if (meta !== null) {
|
|
99
|
+
pathMetaRef.current.set(value, meta);
|
|
100
|
+
setPathMetaVersion((current) => current + 1);
|
|
101
|
+
}
|
|
102
|
+
registerCollectionNode(value, meta);
|
|
103
|
+
},
|
|
104
|
+
[registerCollectionNode],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const getPath = useCallback(
|
|
108
|
+
(value: string): TreePathSegment[] => {
|
|
109
|
+
const segments: TreePathSegment[] = [];
|
|
110
|
+
let cursor: string | null = value;
|
|
111
|
+
let hops = 0;
|
|
112
|
+
while (cursor !== null && hops < MAX_PATH_DEPTH) {
|
|
113
|
+
const meta = pathMetaRef.current.get(cursor);
|
|
114
|
+
if (meta === undefined) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
segments.unshift({
|
|
118
|
+
value: meta.value,
|
|
119
|
+
label: meta.label,
|
|
120
|
+
isBranch: meta.isBranch,
|
|
121
|
+
disabled: meta.disabled,
|
|
122
|
+
depth: meta.depth,
|
|
123
|
+
});
|
|
124
|
+
cursor = meta.parentValue;
|
|
125
|
+
hops += 1;
|
|
126
|
+
}
|
|
127
|
+
return segments;
|
|
128
|
+
},
|
|
129
|
+
// `pathMetaVersion` bumps whenever the persistent map mutates,
|
|
130
|
+
// forcing context-value identity to change so consumers re-render
|
|
131
|
+
// and re-evaluate `getPath`.
|
|
132
|
+
[pathMetaVersion],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const anchorRef = useRef<string | null>(null);
|
|
136
|
+
const [activeValue, setActiveValue] = useState<string | null>(null);
|
|
137
|
+
|
|
138
|
+
const focusItem = useCallback(
|
|
139
|
+
(value: string) => {
|
|
140
|
+
itemsRef.current.get(value)?.element.focus();
|
|
141
|
+
},
|
|
142
|
+
[itemsRef],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const isNodeDisabled = useCallback(
|
|
146
|
+
(value: string) => itemsRef.current.get(value)?.disabled === true,
|
|
147
|
+
[itemsRef],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const isExpanded = useCallback(
|
|
151
|
+
(value: string) => expandedValues.includes(value),
|
|
152
|
+
[expandedValues],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const toggleExpanded = useCallback(
|
|
156
|
+
(value: string) => {
|
|
157
|
+
const open = expandedValues.includes(value);
|
|
158
|
+
setExpandedValues(
|
|
159
|
+
open
|
|
160
|
+
? expandedValues.filter((current) => current !== value)
|
|
161
|
+
: [...expandedValues, value],
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
[expandedValues, setExpandedValues],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const isSelected = useCallback(
|
|
168
|
+
(value: string) => selectedValues.includes(value),
|
|
169
|
+
[selectedValues],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const getVisibleOrder = useCallback((): string[] => {
|
|
173
|
+
const childrenByParent = new Map<string | null, string[]>();
|
|
174
|
+
for (const key of keys) {
|
|
175
|
+
const meta = itemsRef.current.get(key)!;
|
|
176
|
+
const bucket = childrenByParent.get(meta.parentValue) ?? [];
|
|
177
|
+
bucket.push(meta.value);
|
|
178
|
+
childrenByParent.set(meta.parentValue, bucket);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result: string[] = [];
|
|
182
|
+
const visit = (parent: string | null): void => {
|
|
183
|
+
for (const value of childrenByParent.get(parent) ?? []) {
|
|
184
|
+
result.push(value);
|
|
185
|
+
const meta = itemsRef.current.get(value);
|
|
186
|
+
if (meta?.isBranch && expandedValues.includes(value)) {
|
|
187
|
+
visit(value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
visit(null);
|
|
192
|
+
return result;
|
|
193
|
+
}, [keys, itemsRef, expandedValues]);
|
|
194
|
+
|
|
195
|
+
const select = useCallback(
|
|
196
|
+
(value: string, modifiers?: TreeSelectModifiers) => {
|
|
197
|
+
if (options.selectionMode === "single") {
|
|
198
|
+
if (selectedValues[0] === value) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setSelectedValues([value]);
|
|
202
|
+
anchorRef.current = value;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const shift = modifiers?.shift === true;
|
|
207
|
+
const additive = modifiers?.meta === true || modifiers?.ctrl === true;
|
|
208
|
+
const alreadySelected = selectedValues.includes(value);
|
|
209
|
+
|
|
210
|
+
if (shift) {
|
|
211
|
+
const anchor = anchorRef.current ?? value;
|
|
212
|
+
const order = getVisibleOrder();
|
|
213
|
+
const anchorIndex = order.indexOf(anchor);
|
|
214
|
+
const valueIndex = order.indexOf(value);
|
|
215
|
+
if (anchorIndex === -1 || valueIndex === -1) {
|
|
216
|
+
setSelectedValues([value]);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const [start, end] =
|
|
220
|
+
anchorIndex <= valueIndex
|
|
221
|
+
? [anchorIndex, valueIndex]
|
|
222
|
+
: [valueIndex, anchorIndex];
|
|
223
|
+
const range = order
|
|
224
|
+
.slice(start, end + 1)
|
|
225
|
+
.filter((candidate) => !isNodeDisabled(candidate));
|
|
226
|
+
setSelectedValues(range);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (additive) {
|
|
231
|
+
setSelectedValues(
|
|
232
|
+
alreadySelected
|
|
233
|
+
? selectedValues.filter((current) => current !== value)
|
|
234
|
+
: [...selectedValues, value],
|
|
235
|
+
);
|
|
236
|
+
anchorRef.current = value;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (selectedValues.length === 1 && alreadySelected) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
setSelectedValues([value]);
|
|
244
|
+
anchorRef.current = value;
|
|
245
|
+
},
|
|
246
|
+
[
|
|
247
|
+
options.selectionMode,
|
|
248
|
+
selectedValues,
|
|
249
|
+
setSelectedValues,
|
|
250
|
+
getVisibleOrder,
|
|
251
|
+
isNodeDisabled,
|
|
252
|
+
],
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const visibleOrder = getVisibleOrder();
|
|
256
|
+
const defaultTabStop =
|
|
257
|
+
visibleOrder.find((candidate) => !isNodeDisabled(candidate)) ?? null;
|
|
258
|
+
const tabStop =
|
|
259
|
+
activeValue !== null && visibleOrder.includes(activeValue)
|
|
260
|
+
? activeValue
|
|
261
|
+
: defaultTabStop;
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
rootId,
|
|
265
|
+
selectionMode: options.selectionMode,
|
|
266
|
+
isExpanded,
|
|
267
|
+
toggleExpanded,
|
|
268
|
+
isSelected,
|
|
269
|
+
select,
|
|
270
|
+
registerNode,
|
|
271
|
+
getVisibleOrder,
|
|
272
|
+
isNodeDisabled,
|
|
273
|
+
tabStop,
|
|
274
|
+
setActiveValue,
|
|
275
|
+
focusItem,
|
|
276
|
+
getPath,
|
|
277
|
+
selectedOrder: selectedValues,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
type TreeRootBaseProps = ComponentProps<"div"> & {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type TreeRootUncontrolledExpansionProps = {
|
|
8
|
+
/** Branch values expanded on first render when uncontrolled. */
|
|
9
|
+
defaultExpandedValues?: string[];
|
|
10
|
+
expandedValues?: never;
|
|
11
|
+
onExpandedChange?: (values: string[]) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type TreeRootControlledExpansionProps = {
|
|
15
|
+
defaultExpandedValues?: never;
|
|
16
|
+
/** The set of expanded branch values, owned by the consumer. */
|
|
17
|
+
expandedValues: string[];
|
|
18
|
+
onExpandedChange: (values: string[]) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type TreeRootSingleUncontrolledSelectionProps = {
|
|
22
|
+
selectionMode?: "single";
|
|
23
|
+
/** The value selected on first render when uncontrolled. */
|
|
24
|
+
defaultSelectedValue?: string | null;
|
|
25
|
+
selectedValue?: never;
|
|
26
|
+
onSelectedValueChange?: (value: string | null) => void;
|
|
27
|
+
defaultSelectedValues?: never;
|
|
28
|
+
selectedValues?: never;
|
|
29
|
+
onSelectedValuesChange?: never;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type TreeRootSingleControlledSelectionProps = {
|
|
33
|
+
selectionMode?: "single";
|
|
34
|
+
defaultSelectedValue?: never;
|
|
35
|
+
/** The selected value, owned by the consumer. */
|
|
36
|
+
selectedValue: string | null;
|
|
37
|
+
onSelectedValueChange: (value: string | null) => void;
|
|
38
|
+
defaultSelectedValues?: never;
|
|
39
|
+
selectedValues?: never;
|
|
40
|
+
onSelectedValuesChange?: never;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type TreeRootMultipleUncontrolledSelectionProps = {
|
|
44
|
+
selectionMode: "multiple";
|
|
45
|
+
/** The values selected on first render when uncontrolled. */
|
|
46
|
+
defaultSelectedValues?: string[];
|
|
47
|
+
selectedValues?: never;
|
|
48
|
+
onSelectedValuesChange?: (values: string[]) => void;
|
|
49
|
+
defaultSelectedValue?: never;
|
|
50
|
+
selectedValue?: never;
|
|
51
|
+
onSelectedValueChange?: never;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type TreeRootMultipleControlledSelectionProps = {
|
|
55
|
+
selectionMode: "multiple";
|
|
56
|
+
defaultSelectedValues?: never;
|
|
57
|
+
/** The selected values, owned by the consumer. */
|
|
58
|
+
selectedValues: string[];
|
|
59
|
+
onSelectedValuesChange: (values: string[]) => void;
|
|
60
|
+
defaultSelectedValue?: never;
|
|
61
|
+
selectedValue?: never;
|
|
62
|
+
onSelectedValueChange?: never;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type TreeRootProps = TreeRootBaseProps &
|
|
66
|
+
(TreeRootUncontrolledExpansionProps | TreeRootControlledExpansionProps) &
|
|
67
|
+
(
|
|
68
|
+
| TreeRootSingleUncontrolledSelectionProps
|
|
69
|
+
| TreeRootSingleControlledSelectionProps
|
|
70
|
+
| TreeRootMultipleUncontrolledSelectionProps
|
|
71
|
+
| TreeRootMultipleControlledSelectionProps
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
export type TreeItemProps = ComponentProps<"div"> & {
|
|
75
|
+
/** Stable identifier for this item, unique within the tree. */
|
|
76
|
+
value: string;
|
|
77
|
+
/**
|
|
78
|
+
* Optional display label for this item. Stored alongside the value
|
|
79
|
+
* in the tree's node registry so {@link useTreePath} and
|
|
80
|
+
* `Tree.SelectionPath` can surface it without an external lookup.
|
|
81
|
+
* Has no effect on what `Tree.Item` renders.
|
|
82
|
+
*/
|
|
83
|
+
label?: string;
|
|
84
|
+
/** Disable selection and remove the item from roving navigation. */
|
|
85
|
+
disabled?: boolean;
|
|
86
|
+
/** Render the item as the supplied child element instead of `<div>`. */
|
|
87
|
+
asChild?: boolean;
|
|
88
|
+
children: ReactNode;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type TreeBranchProps = Omit<ComponentProps<"div">, "ref"> & {
|
|
92
|
+
/** Stable identifier for this branch, unique within the tree. */
|
|
93
|
+
value: string;
|
|
94
|
+
/**
|
|
95
|
+
* Optional display label for this branch. Stored alongside the value
|
|
96
|
+
* in the tree's node registry so {@link useTreePath} and
|
|
97
|
+
* `Tree.SelectionPath` can surface it without an external lookup.
|
|
98
|
+
* Has no effect on what `Tree.Branch` renders.
|
|
99
|
+
*/
|
|
100
|
+
label?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Disable selection, expansion-toggling, and roving navigation for
|
|
103
|
+
* this branch. The branch and its current content remain rendered.
|
|
104
|
+
*/
|
|
105
|
+
disabled?: boolean;
|
|
106
|
+
children: ReactNode;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type TreeBranchControlProps = ComponentProps<"div"> & {
|
|
110
|
+
/** Render the control as the supplied child element instead of `<div>`. */
|
|
111
|
+
asChild?: boolean;
|
|
112
|
+
children: ReactNode;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type TreeBranchContentProps = ComponentProps<"div"> & {
|
|
116
|
+
children: ReactNode;
|
|
117
|
+
/**
|
|
118
|
+
* Keep the content mounted while the branch is collapsed so CSS can
|
|
119
|
+
* animate it in and out. When collapsed it is hidden from assistive
|
|
120
|
+
* technology with `aria-hidden`.
|
|
121
|
+
*/
|
|
122
|
+
forceMount?: boolean;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type TreeBranchIndicatorProps = ComponentProps<"span"> & {
|
|
126
|
+
/** Render as the supplied child element instead of `<span>`. */
|
|
127
|
+
asChild?: boolean;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Arguments passed to the `Tree.SelectionPath` render-prop form. */
|
|
131
|
+
export type TreeSelectionPathRenderProps = {
|
|
132
|
+
/** One root-to-leaf chain per currently-selected value, in selection order. */
|
|
133
|
+
paths: TreePathSegment[][];
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type TreeSelectionPathProps = Omit<ComponentProps<"div">, "children"> & {
|
|
137
|
+
/**
|
|
138
|
+
* Either standard React children (ignored — the subcomponent does its
|
|
139
|
+
* own rendering) or a render-prop receiving the resolved selection
|
|
140
|
+
* paths so consumers can lay out custom markup.
|
|
141
|
+
*/
|
|
142
|
+
children?: ReactNode | ((args: TreeSelectionPathRenderProps) => ReactNode);
|
|
143
|
+
/**
|
|
144
|
+
* Node passed to each `Breadcrumb.Separator` in the default rendering.
|
|
145
|
+
* Defaults to Breadcrumb's built-in `"/"` glyph.
|
|
146
|
+
*/
|
|
147
|
+
separator?: ReactNode;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type TreeLevelContextValue = {
|
|
151
|
+
/** Zero-based nesting depth — `0` for items directly inside `Tree.Root`. */
|
|
152
|
+
depth: number;
|
|
153
|
+
/** The value of the enclosing branch, or `null` at the tree root. */
|
|
154
|
+
parentValue: string | null;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type TreeNodeMeta = {
|
|
158
|
+
value: string;
|
|
159
|
+
element: HTMLElement;
|
|
160
|
+
isBranch: boolean;
|
|
161
|
+
disabled: boolean;
|
|
162
|
+
depth: number;
|
|
163
|
+
parentValue: string | null;
|
|
164
|
+
label: string | null;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* One segment of an ancestor chain returned by {@link useTreePath} or
|
|
169
|
+
* `TreeContextValue.getPath`. The array is ordered **root → leaf**, so
|
|
170
|
+
* the last segment is the queried item itself.
|
|
171
|
+
*/
|
|
172
|
+
export type TreePathSegment = {
|
|
173
|
+
value: string;
|
|
174
|
+
label: string | null;
|
|
175
|
+
isBranch: boolean;
|
|
176
|
+
disabled: boolean;
|
|
177
|
+
depth: number;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export type SelectionMode = "single" | "multiple";
|
|
181
|
+
|
|
182
|
+
export type TreeSelectModifiers = {
|
|
183
|
+
meta: boolean;
|
|
184
|
+
ctrl: boolean;
|
|
185
|
+
shift: boolean;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export type TreeContextValue = {
|
|
189
|
+
/** Stable id shared across the tree, used to derive ARIA wiring ids. */
|
|
190
|
+
rootId: string;
|
|
191
|
+
selectionMode: SelectionMode;
|
|
192
|
+
isExpanded: (value: string) => boolean;
|
|
193
|
+
toggleExpanded: (value: string) => void;
|
|
194
|
+
isSelected: (value: string) => boolean;
|
|
195
|
+
select: (value: string, modifiers?: TreeSelectModifiers) => void;
|
|
196
|
+
registerNode: (value: string, meta: TreeNodeMeta | null) => void;
|
|
197
|
+
getVisibleOrder: () => string[];
|
|
198
|
+
isNodeDisabled: (value: string) => boolean;
|
|
199
|
+
/** Value of the treeitem currently holding the single roving tabstop. */
|
|
200
|
+
tabStop: string | null;
|
|
201
|
+
setActiveValue: (value: string) => void;
|
|
202
|
+
focusItem: (value: string) => void;
|
|
203
|
+
/**
|
|
204
|
+
* Returns the root-to-leaf chain of segments for the given value, or
|
|
205
|
+
* an empty array if the value has never been registered. Walks the
|
|
206
|
+
* persistent node registry so paths remain resolvable even when an
|
|
207
|
+
* ancestor branch has collapsed and its descendants unmounted.
|
|
208
|
+
*/
|
|
209
|
+
getPath: (value: string) => TreePathSegment[];
|
|
210
|
+
/**
|
|
211
|
+
* The currently-selected values in selection order. Mirrors
|
|
212
|
+
* `selectedValues` in multiple mode and `[selectedValue]` (or `[]`)
|
|
213
|
+
* in single mode.
|
|
214
|
+
*/
|
|
215
|
+
selectedOrder: readonly string[];
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export type TreeItemContextValue = {
|
|
219
|
+
value: string;
|
|
220
|
+
expanded: boolean;
|
|
221
|
+
disabled: boolean;
|
|
222
|
+
/** DOM `id` of the branch's `BranchControl`, for `aria-labelledby`. */
|
|
223
|
+
controlId: string;
|
|
224
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Children, Fragment, isValidElement } from "react";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { TreeBranchControl, TreeBranchContent } from "./Tree";
|
|
5
|
+
|
|
6
|
+
/** Whether `node` is a `Tree.BranchControl` element. */
|
|
7
|
+
export function isBranchControlElement(node: ReactNode): node is ReactElement {
|
|
8
|
+
return isValidElement(node) && node.type === TreeBranchControl;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Whether `node` is a `Tree.BranchContent` element. */
|
|
12
|
+
export function isBranchContentElement(node: ReactNode): node is ReactElement {
|
|
13
|
+
return isValidElement(node) && node.type === TreeBranchContent;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Splits a `Tree.Branch`'s children into its single `<Tree.BranchControl>`
|
|
18
|
+
* (the clickable row) and its optional `<Tree.BranchContent>` (the nested
|
|
19
|
+
* group). Both are matched however deeply they are wrapped in fragments,
|
|
20
|
+
* since `Children.toArray` does not descend into fragments — so
|
|
21
|
+
* `{open && <BranchContent/>}` is partitioned the same as a bare element.
|
|
22
|
+
*/
|
|
23
|
+
export function partitionBranchChildren(children: ReactNode): {
|
|
24
|
+
control: ReactElement;
|
|
25
|
+
content: ReactElement | null;
|
|
26
|
+
} {
|
|
27
|
+
let control: ReactElement | null = null;
|
|
28
|
+
let content: ReactElement | null = null;
|
|
29
|
+
|
|
30
|
+
const visit = (nodes: ReactNode): void => {
|
|
31
|
+
for (const child of Children.toArray(nodes)) {
|
|
32
|
+
if (isBranchControlElement(child)) {
|
|
33
|
+
if (control !== null) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"A Tree.Branch may contain at most one <Tree.BranchControl>.",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
control = child;
|
|
39
|
+
} else if (isBranchContentElement(child)) {
|
|
40
|
+
if (content !== null) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"A Tree.Branch may contain at most one <Tree.BranchContent>.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
content = child;
|
|
46
|
+
} else if (isValidElement(child) && child.type === Fragment) {
|
|
47
|
+
visit((child.props as { children?: ReactNode }).children);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
visit(children);
|
|
53
|
+
|
|
54
|
+
if (control === null) {
|
|
55
|
+
throw new Error("A Tree.Branch must contain a <Tree.BranchControl>.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { control, content };
|
|
59
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# VisuallyHidden
|
|
2
|
+
|
|
3
|
+
Visually hides its children while keeping them in the accessibility tree,
|
|
4
|
+
implementing the standard
|
|
5
|
+
[screen-reader-only pattern](https://www.w3.org/WAI/WCAG21/Techniques/css/C7).
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { VisuallyHidden } from "@primitiv-ui/react";
|
|
9
|
+
|
|
10
|
+
<button>
|
|
11
|
+
<SearchIcon aria-hidden="true" />
|
|
12
|
+
<VisuallyHidden>Search</VisuallyHidden>
|
|
13
|
+
</button>;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Notes |
|
|
19
|
+
| ----------- | ------------------------ | ------- | -------------------------------------------------- |
|
|
20
|
+
| `asChild` | `boolean` | `false` | Render the consumer's element instead of a `<span>` |
|
|
21
|
+
| `style` | `CSSProperties` | — | Merged on top of the clip styles |
|
|
22
|
+
| `className` | `string` | — | Applied directly to the rendered element |
|
|
23
|
+
| `...rest` | `ComponentProps<"span">` | — | All other `<span>` props, including `aria-*` |
|
|
24
|
+
|
|
25
|
+
## Functional styles
|
|
26
|
+
|
|
27
|
+
Unlike other `@primitiv-ui/react` components, `VisuallyHidden` ships inline
|
|
28
|
+
styles — the clip rectangle that removes content from the visual layout
|
|
29
|
+
*is* the component's behaviour, not decoration:
|
|
30
|
+
|
|
31
|
+
```css
|
|
32
|
+
position: absolute;
|
|
33
|
+
width: 1px;
|
|
34
|
+
height: 1px;
|
|
35
|
+
padding: 0;
|
|
36
|
+
margin: -1px;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
clip: rect(0 0 0 0);
|
|
39
|
+
clip-path: inset(50%);
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
border-width: 0;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A consumer `style` is merged on top, so any individual property can still
|
|
45
|
+
be overridden.
|
|
46
|
+
|
|
47
|
+
## asChild
|
|
48
|
+
|
|
49
|
+
Pass `asChild` to hide the consumer's own element instead of a `<span>` —
|
|
50
|
+
useful when the hidden content needs specific semantics:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<VisuallyHidden asChild>
|
|
54
|
+
<h2>Section heading</h2>
|
|
55
|
+
</VisuallyHidden>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The clip styles are merged onto the child element via the `Slot` utility.
|