@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,842 @@
|
|
|
1
|
+
import { useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { useCheckboxRoot } from "../Checkbox/hooks";
|
|
4
|
+
import { useDirection } from "../DirectionProvider";
|
|
5
|
+
import { useRadioGroupRoot } from "../RadioGroup/hooks";
|
|
6
|
+
import { composeEventHandlers, Slot } from "../Slot";
|
|
7
|
+
|
|
8
|
+
import { DropdownContext } from "./DropdownContext";
|
|
9
|
+
import { DropdownContentContext } from "./DropdownContentContext";
|
|
10
|
+
import { DropdownGroupContext } from "./DropdownGroupContext";
|
|
11
|
+
import { DropdownItemIndicatorContext } from "./DropdownItemIndicatorContext";
|
|
12
|
+
import { DropdownRadioGroupContext } from "./DropdownRadioGroupContext";
|
|
13
|
+
import { DropdownSubContext } from "./DropdownSubContext";
|
|
14
|
+
import {
|
|
15
|
+
useCloseSiblingSub,
|
|
16
|
+
useDropdownContent,
|
|
17
|
+
useDropdownContext,
|
|
18
|
+
useDropdownGroup,
|
|
19
|
+
useDropdownItem,
|
|
20
|
+
useDropdownLabel,
|
|
21
|
+
useDropdownRoot,
|
|
22
|
+
useDropdownSubContext,
|
|
23
|
+
useDropdownTrigger,
|
|
24
|
+
} from "./hooks";
|
|
25
|
+
import {
|
|
26
|
+
DropdownCheckboxItemProps,
|
|
27
|
+
DropdownContentProps,
|
|
28
|
+
DropdownGroupProps,
|
|
29
|
+
DropdownItemIndicatorProps,
|
|
30
|
+
DropdownItemProps,
|
|
31
|
+
DropdownLabelProps,
|
|
32
|
+
DropdownRadioGroupProps,
|
|
33
|
+
DropdownRadioItemProps,
|
|
34
|
+
DropdownRootProps,
|
|
35
|
+
DropdownSeparatorProps,
|
|
36
|
+
DropdownSubContentProps,
|
|
37
|
+
DropdownSubProps,
|
|
38
|
+
DropdownSubTriggerProps,
|
|
39
|
+
DropdownTriggerProps,
|
|
40
|
+
} from "./types";
|
|
41
|
+
import { MENUITEM_SELECTOR } from "./constants";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The root of a Dropdown menu — owns the open state and provides context to
|
|
45
|
+
* descendants. Renders no DOM of its own; it is a context boundary.
|
|
46
|
+
*
|
|
47
|
+
* Supports two state modes, statically discriminated at the type level so
|
|
48
|
+
* only one shape is accepted by TypeScript:
|
|
49
|
+
*
|
|
50
|
+
* - **Uncontrolled** — pass {@link DropdownRootProps.defaultOpen | `defaultOpen`}
|
|
51
|
+
* (or omit it to start closed). The component owns and updates the open
|
|
52
|
+
* state internally. Optional {@link DropdownRootProps.onOpenChange | `onOpenChange`}
|
|
53
|
+
* observes transitions.
|
|
54
|
+
* - **Controlled** — pass {@link DropdownRootProps.open | `open`} *and*
|
|
55
|
+
* {@link DropdownRootProps.onOpenChange | `onOpenChange`} together. The
|
|
56
|
+
* parent owns the state; the component defers every transition back through
|
|
57
|
+
* the callback.
|
|
58
|
+
*
|
|
59
|
+
* Only user-driven transitions (trigger clicks, Escape, selection) invoke
|
|
60
|
+
* `onOpenChange`. External state flips made by the parent do not.
|
|
61
|
+
*
|
|
62
|
+
* **Reading direction.** Pass {@link DropdownRootProps.dir | `dir`} to set
|
|
63
|
+
* `"ltr"` or `"rtl"`, which inverts the submenu open / close arrow keys
|
|
64
|
+
* (`ArrowRight` ↔ `ArrowLeft`). When omitted, the component reads the
|
|
65
|
+
* inherited {@link DirectionProvider} value, falling back to `"ltr"`.
|
|
66
|
+
*
|
|
67
|
+
* @example Uncontrolled
|
|
68
|
+
* ```tsx
|
|
69
|
+
* <Dropdown.Root>
|
|
70
|
+
* <Dropdown.Trigger>Options</Dropdown.Trigger>
|
|
71
|
+
* <Dropdown.Content>
|
|
72
|
+
* <Dropdown.Item>Rename</Dropdown.Item>
|
|
73
|
+
* <Dropdown.Item>Delete</Dropdown.Item>
|
|
74
|
+
* </Dropdown.Content>
|
|
75
|
+
* </Dropdown.Root>
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @example Controlled
|
|
79
|
+
* ```tsx
|
|
80
|
+
* const [open, setOpen] = useState(false);
|
|
81
|
+
*
|
|
82
|
+
* <Dropdown.Root open={open} onOpenChange={setOpen}>
|
|
83
|
+
* …
|
|
84
|
+
* </Dropdown.Root>
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
function DropdownRoot({
|
|
88
|
+
defaultOpen,
|
|
89
|
+
open,
|
|
90
|
+
onOpenChange,
|
|
91
|
+
dir,
|
|
92
|
+
children,
|
|
93
|
+
}: DropdownRootProps) {
|
|
94
|
+
const inheritedDir = useDirection();
|
|
95
|
+
const { contextValue } = useDropdownRoot({
|
|
96
|
+
defaultOpen,
|
|
97
|
+
open,
|
|
98
|
+
onOpenChange,
|
|
99
|
+
dir: dir ?? inheritedDir,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<DropdownContext.Provider value={contextValue}>
|
|
104
|
+
{children}
|
|
105
|
+
</DropdownContext.Provider>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
DropdownRoot.displayName = "DropdownRoot";
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The menu button. Toggles the Dropdown open/closed on click and exposes
|
|
113
|
+
* the ARIA contract for WAI-ARIA Menu Button: `aria-haspopup="menu"`,
|
|
114
|
+
* `aria-expanded` reflecting the open state, and `aria-controls` pointing
|
|
115
|
+
* at the content's id.
|
|
116
|
+
*
|
|
117
|
+
* Renders a `<button type="button">` by default. Pass `asChild` to render
|
|
118
|
+
* the composed child element instead (e.g. a link, or a custom button
|
|
119
|
+
* component). All ARIA attributes and event handlers are merged onto the
|
|
120
|
+
* child following the {@link Slot} composition rules.
|
|
121
|
+
*
|
|
122
|
+
* @example With an anchor via `asChild`
|
|
123
|
+
* ```tsx
|
|
124
|
+
* <Dropdown.Trigger asChild>
|
|
125
|
+
* <a href="#options">Options</a>
|
|
126
|
+
* </Dropdown.Trigger>
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
function DropdownTrigger({
|
|
130
|
+
children,
|
|
131
|
+
onClick,
|
|
132
|
+
asChild = false,
|
|
133
|
+
...rest
|
|
134
|
+
}: DropdownTriggerProps) {
|
|
135
|
+
const { triggerProps } = useDropdownTrigger({ onClick, restProps: rest });
|
|
136
|
+
|
|
137
|
+
if (asChild) {
|
|
138
|
+
return <Slot {...triggerProps}>{children}</Slot>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<button type="button" {...triggerProps}>
|
|
143
|
+
{children}
|
|
144
|
+
</button>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
DropdownTrigger.displayName = "DropdownTrigger";
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The menu panel rendered with the native HTML
|
|
152
|
+
* [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
|
|
153
|
+
* (`popover="auto"`) — no portal, no floating-ui. The browser manages
|
|
154
|
+
* layering via the top layer and dispatches a light-dismiss on outside
|
|
155
|
+
* click and Escape in the light-dismiss flow.
|
|
156
|
+
*
|
|
157
|
+
* Renders a `<menu role="menu">` by default; pass `asChild` to render
|
|
158
|
+
* any element with menu semantics.
|
|
159
|
+
*
|
|
160
|
+
* **Keyboard interaction.** While the menu is open:
|
|
161
|
+
*
|
|
162
|
+
* | Key | Behaviour |
|
|
163
|
+
* | ----------------------- | -------------------------------------------------- |
|
|
164
|
+
* | `ArrowDown` / `ArrowUp` | Move focus to next / previous item (wraps) |
|
|
165
|
+
* | `Home` / `End` | Jump to first / last item |
|
|
166
|
+
* | `Enter` / `Space` | Activate the focused item |
|
|
167
|
+
* | `Escape` | Close the menu and return focus to the trigger |
|
|
168
|
+
* | any printable character | Typeahead — focuses the next item matching prefix |
|
|
169
|
+
*
|
|
170
|
+
* Typeahead accumulates keystrokes within a 500 ms window; pressing the
|
|
171
|
+
* same character repeatedly cycles through items that share that first
|
|
172
|
+
* letter. Disabled items are skipped during arrow navigation and typeahead.
|
|
173
|
+
*
|
|
174
|
+
* Arrow navigation is scoped to the popover that currently holds focus —
|
|
175
|
+
* a {@link DropdownSubContent | `Dropdown.SubContent`} rendered inside
|
|
176
|
+
* this menu does not pull its items into the parent's navigation cycle,
|
|
177
|
+
* so `ArrowDown` past a {@link DropdownSubTrigger | `Dropdown.SubTrigger`}
|
|
178
|
+
* lands on the next sibling item in the parent rather than getting
|
|
179
|
+
* stuck on a non-focusable item inside a closed sub-popover.
|
|
180
|
+
*/
|
|
181
|
+
function DropdownContent({
|
|
182
|
+
children,
|
|
183
|
+
onKeyDown,
|
|
184
|
+
asChild = false,
|
|
185
|
+
...rest
|
|
186
|
+
}: DropdownContentProps) {
|
|
187
|
+
const { contentContextValue, contentProps } = useDropdownContent({
|
|
188
|
+
onKeyDown,
|
|
189
|
+
restProps: rest,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<DropdownContentContext.Provider value={contentContextValue}>
|
|
194
|
+
{asChild ? (
|
|
195
|
+
<Slot {...contentProps}>{children}</Slot>
|
|
196
|
+
) : (
|
|
197
|
+
<menu {...contentProps}>{children}</menu>
|
|
198
|
+
)}
|
|
199
|
+
</DropdownContentContext.Provider>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
DropdownContent.displayName = "DropdownContent";
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* A standard menu item. Renders a `<li role="menuitem">` by default; pass
|
|
207
|
+
* `asChild` to render any element with menuitem semantics.
|
|
208
|
+
*
|
|
209
|
+
* Clicking the item (or pressing Enter / Space while focused) fires
|
|
210
|
+
* {@link DropdownItemProps.onSelect | `onSelect`} with a cancellable
|
|
211
|
+
* `Event`. The menu auto-closes after selection; call
|
|
212
|
+
* `event.preventDefault()` inside `onSelect` to keep it open — useful
|
|
213
|
+
* for actions that perform in-place mutations (e.g. copying to clipboard).
|
|
214
|
+
*
|
|
215
|
+
* Disabled items receive `aria-disabled="true"` and are skipped by arrow
|
|
216
|
+
* navigation, typeahead, and activation handlers.
|
|
217
|
+
*/
|
|
218
|
+
function DropdownItem({
|
|
219
|
+
children,
|
|
220
|
+
onClick,
|
|
221
|
+
onSelect,
|
|
222
|
+
disabled,
|
|
223
|
+
asChild = false,
|
|
224
|
+
...rest
|
|
225
|
+
}: DropdownItemProps) {
|
|
226
|
+
const { itemProps } = useDropdownItem({
|
|
227
|
+
disabled,
|
|
228
|
+
onClick,
|
|
229
|
+
onSelect,
|
|
230
|
+
restProps: rest,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (asChild) {
|
|
234
|
+
return <Slot {...itemProps}>{children}</Slot>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return <li {...itemProps}>{children}</li>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
DropdownItem.displayName = "DropdownItem";
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A visual separator between groups of items. Renders a `<li role="separator">`
|
|
244
|
+
* by default; pass `asChild` to render any element with separator semantics.
|
|
245
|
+
* Non-interactive — skipped by focus, arrow navigation, and typeahead.
|
|
246
|
+
*/
|
|
247
|
+
function DropdownSeparator({
|
|
248
|
+
asChild = false,
|
|
249
|
+
children,
|
|
250
|
+
...rest
|
|
251
|
+
}: DropdownSeparatorProps) {
|
|
252
|
+
const separatorProps = { ...rest, role: "separator" as const };
|
|
253
|
+
|
|
254
|
+
if (asChild) {
|
|
255
|
+
return <Slot {...separatorProps}>{children}</Slot>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return <li {...separatorProps} />;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
DropdownSeparator.displayName = "DropdownSeparator";
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* A semantic grouping of related items. Renders as a `<li role="group">`
|
|
265
|
+
* wrapping an inner `<ul role="none">`, or — with `asChild` — a single
|
|
266
|
+
* grouping element composed onto the provided child.
|
|
267
|
+
*
|
|
268
|
+
* Generates a stable id for its accompanying {@link DropdownLabel | `Dropdown.Label`},
|
|
269
|
+
* wired automatically via `aria-labelledby`. Nest a `Dropdown.Label` as the
|
|
270
|
+
* first child to provide the accessible name; screen readers will announce
|
|
271
|
+
* the group when arrowing into it.
|
|
272
|
+
*/
|
|
273
|
+
function DropdownGroup({
|
|
274
|
+
children,
|
|
275
|
+
asChild = false,
|
|
276
|
+
...rest
|
|
277
|
+
}: DropdownGroupProps) {
|
|
278
|
+
const { contextValue, groupProps } = useDropdownGroup({ restProps: rest });
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<DropdownGroupContext.Provider value={contextValue}>
|
|
282
|
+
{asChild ? (
|
|
283
|
+
<Slot {...groupProps}>{children}</Slot>
|
|
284
|
+
) : (
|
|
285
|
+
<li {...groupProps}>
|
|
286
|
+
<ul role="none">{children}</ul>
|
|
287
|
+
</li>
|
|
288
|
+
)}
|
|
289
|
+
</DropdownGroupContext.Provider>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
DropdownGroup.displayName = "DropdownGroup";
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* A non-interactive label, typically used inside a {@link DropdownGroup |
|
|
297
|
+
* `Dropdown.Group`} to give that group an accessible name. When nested in
|
|
298
|
+
* a group, the label's `id` is auto-wired to the group's `aria-labelledby`
|
|
299
|
+
* — consumers don't need to thread ids manually.
|
|
300
|
+
*
|
|
301
|
+
* Renders a `<li>` by default; pass `asChild` to render any element. A
|
|
302
|
+
* caller-supplied `id` takes precedence over the auto-generated one.
|
|
303
|
+
*/
|
|
304
|
+
function DropdownLabel({
|
|
305
|
+
id,
|
|
306
|
+
children,
|
|
307
|
+
asChild = false,
|
|
308
|
+
...rest
|
|
309
|
+
}: DropdownLabelProps) {
|
|
310
|
+
const { labelProps } = useDropdownLabel({ id, restProps: rest });
|
|
311
|
+
|
|
312
|
+
if (asChild) {
|
|
313
|
+
return <Slot {...labelProps}>{children}</Slot>;
|
|
314
|
+
}
|
|
315
|
+
return <li {...labelProps}>{children}</li>;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
DropdownLabel.displayName = "DropdownLabel";
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* A toggleable menu item. Renders a `<li role="menuitemcheckbox">` with
|
|
322
|
+
* `aria-checked` reflecting the current state. `asChild` is supported.
|
|
323
|
+
*
|
|
324
|
+
* Supports a WAI-ARIA tri-state: `true`, `false`, or `"indeterminate"`
|
|
325
|
+
* (encoded as `aria-checked="mixed"`). An indeterminate item resolves to
|
|
326
|
+
* `true` on the next activation, matching the native `<input type="checkbox">`
|
|
327
|
+
* behaviour.
|
|
328
|
+
*
|
|
329
|
+
* State modes are discriminated at the type level:
|
|
330
|
+
*
|
|
331
|
+
* - **Uncontrolled** — pass `defaultChecked` (or omit to start unchecked).
|
|
332
|
+
* Optional `onCheckedChange` observes toggles.
|
|
333
|
+
* - **Controlled** — pass `checked` *and* `onCheckedChange` together.
|
|
334
|
+
*
|
|
335
|
+
* Activation (click / Enter / Space) toggles the checked state, then
|
|
336
|
+
* fires {@link DropdownCheckboxItemProps.onSelect | `onSelect`} with a
|
|
337
|
+
* cancellable `Event`. Call `event.preventDefault()` to keep the menu
|
|
338
|
+
* open — useful for rapidly toggling multiple checkboxes.
|
|
339
|
+
*
|
|
340
|
+
* Nest a {@link DropdownItemIndicator | `Dropdown.ItemIndicator`} to
|
|
341
|
+
* render the visible check mark; it reads its parent's state from
|
|
342
|
+
* context and exposes `data-state` for styling.
|
|
343
|
+
*
|
|
344
|
+
* Disabled items receive `aria-disabled="true"` and no-op on activation.
|
|
345
|
+
*/
|
|
346
|
+
function DropdownCheckboxItem({
|
|
347
|
+
children,
|
|
348
|
+
onClick,
|
|
349
|
+
onSelect,
|
|
350
|
+
disabled,
|
|
351
|
+
defaultChecked,
|
|
352
|
+
checked: controlledChecked,
|
|
353
|
+
onCheckedChange,
|
|
354
|
+
asChild = false,
|
|
355
|
+
...rest
|
|
356
|
+
}: DropdownCheckboxItemProps) {
|
|
357
|
+
const { setOpen, triggerRef } = useDropdownContext();
|
|
358
|
+
const closeSiblingSub = useCloseSiblingSub();
|
|
359
|
+
const [highlighted, setHighlighted] = useState(false);
|
|
360
|
+
const { checked, toggle } = useCheckboxRoot({
|
|
361
|
+
defaultChecked,
|
|
362
|
+
checked: controlledChecked,
|
|
363
|
+
onCheckedChange,
|
|
364
|
+
});
|
|
365
|
+
const ariaChecked: "mixed" | "true" | "false" =
|
|
366
|
+
checked === "indeterminate" ? "mixed" : checked ? "true" : "false";
|
|
367
|
+
const handleClick = () => {
|
|
368
|
+
if (disabled) return;
|
|
369
|
+
toggle();
|
|
370
|
+
const event = new Event("dropdown.select", { cancelable: true });
|
|
371
|
+
onSelect?.(event);
|
|
372
|
+
if (!event.defaultPrevented) {
|
|
373
|
+
setOpen(false);
|
|
374
|
+
triggerRef.current?.focus();
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const itemProps = {
|
|
378
|
+
...rest,
|
|
379
|
+
role: "menuitemcheckbox" as const,
|
|
380
|
+
tabIndex: -1,
|
|
381
|
+
"aria-checked": ariaChecked,
|
|
382
|
+
"aria-disabled": disabled || undefined,
|
|
383
|
+
"data-highlighted": highlighted ? "" : undefined,
|
|
384
|
+
onClick: composeEventHandlers(onClick, handleClick),
|
|
385
|
+
onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
|
|
386
|
+
setHighlighted(true);
|
|
387
|
+
closeSiblingSub();
|
|
388
|
+
}),
|
|
389
|
+
onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
|
|
390
|
+
setHighlighted(false),
|
|
391
|
+
),
|
|
392
|
+
};
|
|
393
|
+
const indicatorContextValue = useMemo(() => ({ checked }), [checked]);
|
|
394
|
+
const content = asChild ? (
|
|
395
|
+
<Slot {...itemProps}>{children}</Slot>
|
|
396
|
+
) : (
|
|
397
|
+
<li {...itemProps}>{children}</li>
|
|
398
|
+
);
|
|
399
|
+
return (
|
|
400
|
+
<DropdownItemIndicatorContext.Provider value={indicatorContextValue}>
|
|
401
|
+
{content}
|
|
402
|
+
</DropdownItemIndicatorContext.Provider>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
DropdownCheckboxItem.displayName = "DropdownCheckboxItem";
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* A single-selection group of menu items. Children must be
|
|
410
|
+
* {@link DropdownRadioItem | `Dropdown.RadioItem`} elements. Renders a
|
|
411
|
+
* `<li role="group">` wrapping `<ul role="none">`, or — with `asChild` —
|
|
412
|
+
* composes onto the provided child.
|
|
413
|
+
*
|
|
414
|
+
* State modes are discriminated at the type level:
|
|
415
|
+
*
|
|
416
|
+
* - **Uncontrolled** — pass `defaultValue` (or omit for no initial selection).
|
|
417
|
+
* Optional `onValueChange` observes selections.
|
|
418
|
+
* - **Controlled** — pass `value` *and* `onValueChange` together.
|
|
419
|
+
*/
|
|
420
|
+
function DropdownRadioGroup({
|
|
421
|
+
defaultValue,
|
|
422
|
+
value: controlledValue,
|
|
423
|
+
onValueChange,
|
|
424
|
+
children,
|
|
425
|
+
asChild = false,
|
|
426
|
+
...rest
|
|
427
|
+
}: DropdownRadioGroupProps) {
|
|
428
|
+
const { value, select } = useRadioGroupRoot({
|
|
429
|
+
defaultValue,
|
|
430
|
+
value: controlledValue,
|
|
431
|
+
onValueChange,
|
|
432
|
+
});
|
|
433
|
+
const contextValue = useMemo(() => ({ value, select }), [value, select]);
|
|
434
|
+
const groupProps = { ...rest, role: "group" as const };
|
|
435
|
+
return (
|
|
436
|
+
<DropdownRadioGroupContext.Provider value={contextValue}>
|
|
437
|
+
{asChild ? (
|
|
438
|
+
<Slot {...groupProps}>{children}</Slot>
|
|
439
|
+
) : (
|
|
440
|
+
<li {...groupProps}>
|
|
441
|
+
<ul role="none">{children}</ul>
|
|
442
|
+
</li>
|
|
443
|
+
)}
|
|
444
|
+
</DropdownRadioGroupContext.Provider>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
DropdownRadioGroup.displayName = "DropdownRadioGroup";
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* A single radio choice. Must be rendered inside a
|
|
452
|
+
* {@link DropdownRadioGroup | `Dropdown.RadioGroup`}; rendering it outside
|
|
453
|
+
* one throws a descriptive error.
|
|
454
|
+
*
|
|
455
|
+
* Renders a `<li role="menuitemradio">` with `aria-checked` reflecting
|
|
456
|
+
* whether this item's `value` matches the group's active value. `asChild`
|
|
457
|
+
* is supported.
|
|
458
|
+
*
|
|
459
|
+
* Activation (click / Enter / Space) selects this item, updating the
|
|
460
|
+
* group's value, then fires {@link DropdownRadioItemProps.onSelect |
|
|
461
|
+
* `onSelect`} with a cancellable `Event`. Call `event.preventDefault()`
|
|
462
|
+
* to keep the menu open.
|
|
463
|
+
*
|
|
464
|
+
* Nest a {@link DropdownItemIndicator | `Dropdown.ItemIndicator`} to
|
|
465
|
+
* render the visible bullet; it reads its parent's state from context
|
|
466
|
+
* and exposes `data-state` for styling.
|
|
467
|
+
*
|
|
468
|
+
* Disabled items receive `aria-disabled="true"` and no-op on activation.
|
|
469
|
+
*/
|
|
470
|
+
function DropdownRadioItem({
|
|
471
|
+
children,
|
|
472
|
+
onClick,
|
|
473
|
+
onSelect,
|
|
474
|
+
disabled,
|
|
475
|
+
value: itemValue,
|
|
476
|
+
asChild = false,
|
|
477
|
+
...rest
|
|
478
|
+
}: DropdownRadioItemProps) {
|
|
479
|
+
const { setOpen, triggerRef } = useDropdownContext();
|
|
480
|
+
const closeSiblingSub = useCloseSiblingSub();
|
|
481
|
+
const [highlighted, setHighlighted] = useState(false);
|
|
482
|
+
const group = useContext(DropdownRadioGroupContext);
|
|
483
|
+
if (!group) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
"Dropdown.RadioItem must be rendered inside a <Dropdown.RadioGroup>.",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const checked = group.value === itemValue;
|
|
489
|
+
const handleClick = () => {
|
|
490
|
+
if (disabled) return;
|
|
491
|
+
group.select(itemValue);
|
|
492
|
+
const event = new Event("dropdown.select", { cancelable: true });
|
|
493
|
+
onSelect?.(event);
|
|
494
|
+
if (!event.defaultPrevented) {
|
|
495
|
+
setOpen(false);
|
|
496
|
+
triggerRef.current?.focus();
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const itemProps = {
|
|
500
|
+
...rest,
|
|
501
|
+
role: "menuitemradio" as const,
|
|
502
|
+
tabIndex: -1,
|
|
503
|
+
"aria-checked": checked,
|
|
504
|
+
"aria-disabled": disabled || undefined,
|
|
505
|
+
"data-highlighted": highlighted ? "" : undefined,
|
|
506
|
+
onClick: composeEventHandlers(onClick, handleClick),
|
|
507
|
+
onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
|
|
508
|
+
setHighlighted(true);
|
|
509
|
+
closeSiblingSub();
|
|
510
|
+
}),
|
|
511
|
+
onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
|
|
512
|
+
setHighlighted(false),
|
|
513
|
+
),
|
|
514
|
+
};
|
|
515
|
+
const indicatorContextValue = useMemo(() => ({ checked }), [checked]);
|
|
516
|
+
const content = asChild ? (
|
|
517
|
+
<Slot {...itemProps}>{children}</Slot>
|
|
518
|
+
) : (
|
|
519
|
+
<li {...itemProps}>{children}</li>
|
|
520
|
+
);
|
|
521
|
+
return (
|
|
522
|
+
<DropdownItemIndicatorContext.Provider value={indicatorContextValue}>
|
|
523
|
+
{content}
|
|
524
|
+
</DropdownItemIndicatorContext.Provider>
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
DropdownRadioItem.displayName = "DropdownRadioItem";
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* The visible mark (usually a checkmark or a bullet) rendered inside a
|
|
532
|
+
* {@link DropdownCheckboxItem | `Dropdown.CheckboxItem`} or
|
|
533
|
+
* {@link DropdownRadioItem | `Dropdown.RadioItem`}. Must be a descendant
|
|
534
|
+
* of one of those; rendering it anywhere else throws a descriptive error.
|
|
535
|
+
*
|
|
536
|
+
* Renders a `<span>` by default; pass `asChild` to compose onto any
|
|
537
|
+
* element (commonly an SVG icon). Exposes `data-state` reflecting the
|
|
538
|
+
* parent item's live state: `"checked"`, `"unchecked"`, or
|
|
539
|
+
* `"indeterminate"` — the last being reachable only through a tri-state
|
|
540
|
+
* `Dropdown.CheckboxItem`.
|
|
541
|
+
*
|
|
542
|
+
* By default the indicator unmounts when its parent is unchecked. Pass
|
|
543
|
+
* {@link DropdownItemIndicatorProps.forceMount | `forceMount`} to keep
|
|
544
|
+
* the DOM node mounted in both states, which is handy when animating the
|
|
545
|
+
* indicator in and out via CSS transitions or a React animation library.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```tsx
|
|
549
|
+
* <Dropdown.CheckboxItem checked={showBookmarks} onCheckedChange={setShowBookmarks}>
|
|
550
|
+
* <Dropdown.ItemIndicator>
|
|
551
|
+
* <CheckIcon />
|
|
552
|
+
* </Dropdown.ItemIndicator>
|
|
553
|
+
* Show bookmarks
|
|
554
|
+
* </Dropdown.CheckboxItem>
|
|
555
|
+
* ```
|
|
556
|
+
*/
|
|
557
|
+
function DropdownItemIndicator({
|
|
558
|
+
children,
|
|
559
|
+
asChild = false,
|
|
560
|
+
forceMount = false,
|
|
561
|
+
...rest
|
|
562
|
+
}: DropdownItemIndicatorProps) {
|
|
563
|
+
const context = useContext(DropdownItemIndicatorContext);
|
|
564
|
+
if (!context) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
"Dropdown.ItemIndicator must be rendered inside a <Dropdown.CheckboxItem> or <Dropdown.RadioItem>.",
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const { checked } = context;
|
|
570
|
+
const dataState =
|
|
571
|
+
checked === "indeterminate"
|
|
572
|
+
? "indeterminate"
|
|
573
|
+
: checked
|
|
574
|
+
? "checked"
|
|
575
|
+
: "unchecked";
|
|
576
|
+
if (!forceMount && checked === false) return null;
|
|
577
|
+
const indicatorProps = { ...rest, "data-state": dataState };
|
|
578
|
+
if (asChild) {
|
|
579
|
+
return <Slot {...indicatorProps}>{children}</Slot>;
|
|
580
|
+
}
|
|
581
|
+
return <span {...indicatorProps}>{children}</span>;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
DropdownItemIndicator.displayName = "DropdownItemIndicator";
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* A submenu boundary. Wrap a {@link DropdownSubTrigger | `Dropdown.SubTrigger`}
|
|
588
|
+
* and its {@link DropdownSubContent | `Dropdown.SubContent`} in a
|
|
589
|
+
* `Dropdown.Sub` to establish an independent open state for the nested menu.
|
|
590
|
+
*
|
|
591
|
+
* Supports uncontrolled (`defaultOpen`) and controlled (`open` +
|
|
592
|
+
* `onOpenChange`) modes, discriminated at the type level — same contract
|
|
593
|
+
* as {@link DropdownRoot | `Dropdown.Root`}.
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```tsx
|
|
597
|
+
* <Dropdown.Root>
|
|
598
|
+
* <Dropdown.Trigger>File</Dropdown.Trigger>
|
|
599
|
+
* <Dropdown.Content>
|
|
600
|
+
* <Dropdown.Sub>
|
|
601
|
+
* <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
|
|
602
|
+
* <Dropdown.SubContent>
|
|
603
|
+
* <Dropdown.Item>Project A</Dropdown.Item>
|
|
604
|
+
* <Dropdown.Item>Project B</Dropdown.Item>
|
|
605
|
+
* </Dropdown.SubContent>
|
|
606
|
+
* </Dropdown.Sub>
|
|
607
|
+
* </Dropdown.Content>
|
|
608
|
+
* </Dropdown.Root>
|
|
609
|
+
* ```
|
|
610
|
+
*/
|
|
611
|
+
function DropdownSub({
|
|
612
|
+
defaultOpen,
|
|
613
|
+
open: controlledOpen,
|
|
614
|
+
onOpenChange,
|
|
615
|
+
children,
|
|
616
|
+
}: DropdownSubProps) {
|
|
617
|
+
const { open, setOpen } = useDropdownRoot({
|
|
618
|
+
defaultOpen,
|
|
619
|
+
open: controlledOpen,
|
|
620
|
+
onOpenChange,
|
|
621
|
+
});
|
|
622
|
+
const contentId = useId();
|
|
623
|
+
const triggerRef = useRef<HTMLLIElement | null>(null);
|
|
624
|
+
const contextValue = useMemo(
|
|
625
|
+
() => ({ open, setOpen, contentId, triggerRef }),
|
|
626
|
+
[open, setOpen, contentId],
|
|
627
|
+
);
|
|
628
|
+
// Register with the enclosing Content so sibling items can close this sub
|
|
629
|
+
// on hover (mirroring the keyboard behaviour where returning focus to the
|
|
630
|
+
// parent menu dismisses the sub). If another sibling sub is already
|
|
631
|
+
// registered as open, close it first — a hover-to-open transition onto
|
|
632
|
+
// our SubTrigger should supplant the prior sub, not stack it.
|
|
633
|
+
const parentContent = useContext(DropdownContentContext);
|
|
634
|
+
useEffect(() => {
|
|
635
|
+
if (!open || !parentContent) return;
|
|
636
|
+
const close = () => setOpen(false);
|
|
637
|
+
const prev = parentContent.closeOpenSubRef.current;
|
|
638
|
+
if (prev && prev !== close) prev();
|
|
639
|
+
parentContent.closeOpenSubRef.current = close;
|
|
640
|
+
return () => {
|
|
641
|
+
if (parentContent.closeOpenSubRef.current === close) {
|
|
642
|
+
parentContent.closeOpenSubRef.current = null;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}, [open, parentContent, setOpen]);
|
|
646
|
+
return (
|
|
647
|
+
<DropdownSubContext.Provider value={contextValue}>
|
|
648
|
+
{children}
|
|
649
|
+
</DropdownSubContext.Provider>
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
DropdownSub.displayName = "DropdownSub";
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* The submenu trigger. Must be rendered inside a {@link DropdownSub |
|
|
657
|
+
* `Dropdown.Sub`}; rendering it outside one throws a descriptive error.
|
|
658
|
+
*
|
|
659
|
+
* Renders a `<li role="menuitem">` with `aria-haspopup="menu"`,
|
|
660
|
+
* `aria-expanded`, and `aria-controls` wiring it to the sibling
|
|
661
|
+
* {@link DropdownSubContent | `Dropdown.SubContent`}. `asChild` is supported.
|
|
662
|
+
*
|
|
663
|
+
* Opens the submenu on click, the inline-forward arrow key, or pointer
|
|
664
|
+
* hover; all other keys bubble to the parent {@link DropdownContent |
|
|
665
|
+
* `Dropdown.Content`} so its roving focus and typeahead continue to work
|
|
666
|
+
* while a submenu is in play. The open key follows the resolved reading
|
|
667
|
+
* direction — `ArrowRight` in `"ltr"`, `ArrowLeft` in `"rtl"`. Hovering
|
|
668
|
+
* onto a sibling item in the parent menu closes the submenu — mirroring
|
|
669
|
+
* the keyboard contract where focus returning to the parent dismisses it.
|
|
670
|
+
*
|
|
671
|
+
* Disabled triggers receive `aria-disabled="true"` and ignore both click
|
|
672
|
+
* and the open arrow key.
|
|
673
|
+
*/
|
|
674
|
+
function DropdownSubTrigger({
|
|
675
|
+
children,
|
|
676
|
+
onClick,
|
|
677
|
+
onKeyDown,
|
|
678
|
+
disabled,
|
|
679
|
+
asChild = false,
|
|
680
|
+
...rest
|
|
681
|
+
}: DropdownSubTriggerProps) {
|
|
682
|
+
const sub = useDropdownSubContext();
|
|
683
|
+
const { dir } = useDropdownContext();
|
|
684
|
+
const openKey = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
|
|
685
|
+
const [hovered, setHovered] = useState(false);
|
|
686
|
+
const toggle = () => {
|
|
687
|
+
if (disabled) return;
|
|
688
|
+
sub.setOpen(true);
|
|
689
|
+
};
|
|
690
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLLIElement>) => {
|
|
691
|
+
if (disabled) return;
|
|
692
|
+
if (event.key !== openKey) return;
|
|
693
|
+
event.preventDefault();
|
|
694
|
+
event.stopPropagation();
|
|
695
|
+
sub.setOpen(true);
|
|
696
|
+
};
|
|
697
|
+
const subTriggerProps = {
|
|
698
|
+
...rest,
|
|
699
|
+
ref: sub.triggerRef,
|
|
700
|
+
role: "menuitem" as const,
|
|
701
|
+
tabIndex: -1,
|
|
702
|
+
"aria-haspopup": "menu" as const,
|
|
703
|
+
"aria-expanded": sub.open,
|
|
704
|
+
"aria-controls": sub.contentId,
|
|
705
|
+
"aria-disabled": disabled || undefined,
|
|
706
|
+
"data-highlighted": hovered || sub.open ? "" : undefined,
|
|
707
|
+
onClick: composeEventHandlers(onClick, toggle),
|
|
708
|
+
onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
|
|
709
|
+
onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
|
|
710
|
+
setHovered(true);
|
|
711
|
+
if (!disabled) sub.setOpen(true);
|
|
712
|
+
}),
|
|
713
|
+
onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
|
|
714
|
+
setHovered(false),
|
|
715
|
+
),
|
|
716
|
+
};
|
|
717
|
+
if (asChild) {
|
|
718
|
+
return <Slot {...subTriggerProps}>{children}</Slot>;
|
|
719
|
+
}
|
|
720
|
+
return <li {...subTriggerProps}>{children}</li>;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
DropdownSubTrigger.displayName = "DropdownSubTrigger";
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* The submenu panel. Must be rendered inside a {@link DropdownSub |
|
|
727
|
+
* `Dropdown.Sub`}; rendering it outside one throws a descriptive error.
|
|
728
|
+
*
|
|
729
|
+
* Renders a `<menu role="menu" popover="auto">` by default; pass `asChild`
|
|
730
|
+
* to render any element with menu semantics. When the submenu opens, focus
|
|
731
|
+
* moves to its first enabled item.
|
|
732
|
+
*
|
|
733
|
+
* Presses of the inline-backward arrow key close the submenu and return
|
|
734
|
+
* focus to the {@link DropdownSubTrigger | `Dropdown.SubTrigger`} —
|
|
735
|
+
* `ArrowLeft` in `"ltr"`, `ArrowRight` in `"rtl"`. All other keys bubble
|
|
736
|
+
* to the parent {@link DropdownContent | `Dropdown.Content`} so arrow
|
|
737
|
+
* navigation and typeahead apply to the submenu's items.
|
|
738
|
+
*/
|
|
739
|
+
function DropdownSubContent({
|
|
740
|
+
children,
|
|
741
|
+
onKeyDown,
|
|
742
|
+
asChild = false,
|
|
743
|
+
...rest
|
|
744
|
+
}: DropdownSubContentProps) {
|
|
745
|
+
const sub = useDropdownSubContext();
|
|
746
|
+
const { dir } = useDropdownContext();
|
|
747
|
+
const closeKey = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
|
|
748
|
+
const menuRef = useRef<HTMLMenuElement | null>(null);
|
|
749
|
+
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
const menu = menuRef.current!;
|
|
752
|
+
if (sub.open) {
|
|
753
|
+
menu.showPopover();
|
|
754
|
+
const firstItem = menu.querySelector<HTMLElement>(MENUITEM_SELECTOR);
|
|
755
|
+
firstItem?.focus();
|
|
756
|
+
} else {
|
|
757
|
+
try {
|
|
758
|
+
menu.hidePopover();
|
|
759
|
+
} catch {
|
|
760
|
+
// already hidden — no-op
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}, [sub.open]);
|
|
764
|
+
|
|
765
|
+
useEffect(() => {
|
|
766
|
+
const menu = menuRef.current!;
|
|
767
|
+
const handleToggle = (event: ToggleEvent) => {
|
|
768
|
+
if (event.newState === "closed") sub.setOpen(false);
|
|
769
|
+
};
|
|
770
|
+
menu.addEventListener("toggle", handleToggle);
|
|
771
|
+
return () => menu.removeEventListener("toggle", handleToggle);
|
|
772
|
+
}, [sub.setOpen]);
|
|
773
|
+
|
|
774
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLMenuElement>) => {
|
|
775
|
+
if (event.key !== closeKey) return;
|
|
776
|
+
event.preventDefault();
|
|
777
|
+
event.stopPropagation();
|
|
778
|
+
sub.setOpen(false);
|
|
779
|
+
sub.triggerRef.current?.focus();
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const closeOpenSubRef = useRef<(() => void) | null>(null);
|
|
783
|
+
const contentContextValue = useMemo(() => ({ closeOpenSubRef }), []);
|
|
784
|
+
|
|
785
|
+
const subContentProps = {
|
|
786
|
+
...rest,
|
|
787
|
+
ref: menuRef,
|
|
788
|
+
id: sub.contentId,
|
|
789
|
+
role: "menu" as const,
|
|
790
|
+
popover: "auto" as const,
|
|
791
|
+
onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
|
|
792
|
+
};
|
|
793
|
+
return (
|
|
794
|
+
<DropdownContentContext.Provider value={contentContextValue}>
|
|
795
|
+
{asChild ? (
|
|
796
|
+
<Slot {...subContentProps}>{children}</Slot>
|
|
797
|
+
) : (
|
|
798
|
+
<menu {...subContentProps}>{children}</menu>
|
|
799
|
+
)}
|
|
800
|
+
</DropdownContentContext.Provider>
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
DropdownSubContent.displayName = "DropdownSubContent";
|
|
805
|
+
|
|
806
|
+
type TDropdownCompound = typeof DropdownRoot & {
|
|
807
|
+
Root: typeof DropdownRoot;
|
|
808
|
+
Trigger: typeof DropdownTrigger;
|
|
809
|
+
Content: typeof DropdownContent;
|
|
810
|
+
Item: typeof DropdownItem;
|
|
811
|
+
Separator: typeof DropdownSeparator;
|
|
812
|
+
Group: typeof DropdownGroup;
|
|
813
|
+
Label: typeof DropdownLabel;
|
|
814
|
+
CheckboxItem: typeof DropdownCheckboxItem;
|
|
815
|
+
RadioGroup: typeof DropdownRadioGroup;
|
|
816
|
+
RadioItem: typeof DropdownRadioItem;
|
|
817
|
+
ItemIndicator: typeof DropdownItemIndicator;
|
|
818
|
+
Sub: typeof DropdownSub;
|
|
819
|
+
SubTrigger: typeof DropdownSubTrigger;
|
|
820
|
+
SubContent: typeof DropdownSubContent;
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
const DropdownCompound: TDropdownCompound = Object.assign(DropdownRoot, {
|
|
824
|
+
Root: DropdownRoot,
|
|
825
|
+
Trigger: DropdownTrigger,
|
|
826
|
+
Content: DropdownContent,
|
|
827
|
+
Item: DropdownItem,
|
|
828
|
+
Separator: DropdownSeparator,
|
|
829
|
+
Group: DropdownGroup,
|
|
830
|
+
Label: DropdownLabel,
|
|
831
|
+
CheckboxItem: DropdownCheckboxItem,
|
|
832
|
+
RadioGroup: DropdownRadioGroup,
|
|
833
|
+
RadioItem: DropdownRadioItem,
|
|
834
|
+
ItemIndicator: DropdownItemIndicator,
|
|
835
|
+
Sub: DropdownSub,
|
|
836
|
+
SubTrigger: DropdownSubTrigger,
|
|
837
|
+
SubContent: DropdownSubContent,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
DropdownCompound.displayName = "Dropdown";
|
|
841
|
+
|
|
842
|
+
export { DropdownCompound as Dropdown };
|