@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,204 @@
|
|
|
1
|
+
import { ChangeEvent, Children, isValidElement, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { useFieldProps } from "../Field/hooks";
|
|
4
|
+
import { Slot } from "../Slot";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
SelectGroupProps,
|
|
8
|
+
SelectOptionProps,
|
|
9
|
+
SelectPlaceholderProps,
|
|
10
|
+
SelectRootProps,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
const PLACEHOLDER_DISPLAY_NAME = "SelectPlaceholder";
|
|
14
|
+
|
|
15
|
+
function hasPlaceholderChild(children: ReactNode): boolean {
|
|
16
|
+
return Children.toArray(children).some((child) => {
|
|
17
|
+
if (!isValidElement(child)) return false;
|
|
18
|
+
const type = child.type as { displayName?: string };
|
|
19
|
+
return type.displayName === PLACEHOLDER_DISPLAY_NAME;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The root of a Select — renders a native `<select>` element and passes
|
|
25
|
+
* all `SelectHTMLAttributes` through to the DOM.
|
|
26
|
+
*
|
|
27
|
+
* Browser-native behaviour is preserved: keyboard navigation (arrow keys,
|
|
28
|
+
* Home/End, typeahead), the platform popup, mobile UX, and form
|
|
29
|
+
* submission all work without additional JS.
|
|
30
|
+
*
|
|
31
|
+
* Supports two state modes, statically discriminated at the type level:
|
|
32
|
+
*
|
|
33
|
+
* - **Uncontrolled** — pass `defaultValue` (or omit it). The browser owns
|
|
34
|
+
* the selection. `onValueChange` is optional.
|
|
35
|
+
* - **Controlled** — pass `value` and `onValueChange` together. Every
|
|
36
|
+
* transition defers back through `onValueChange`.
|
|
37
|
+
*
|
|
38
|
+
* `onValueChange` receives the new selection as a plain string. The
|
|
39
|
+
* consumer's own `onChange` (the raw `ChangeEvent`) still fires alongside
|
|
40
|
+
* it.
|
|
41
|
+
*
|
|
42
|
+
* **Placeholder integration.** When a {@link Select.Placeholder} appears
|
|
43
|
+
* among the direct children and neither `value` nor `defaultValue` is
|
|
44
|
+
* set, Root infers `defaultValue=""` so the placeholder — not the first
|
|
45
|
+
* selectable option — is the initial selection.
|
|
46
|
+
*
|
|
47
|
+
* **Field integration.** When rendered inside a `<Field.Root>`, Select
|
|
48
|
+
* opts into `FieldContext` and inherits `id`, `aria-describedby`,
|
|
49
|
+
* `aria-invalid`, `disabled`, and `required` from the field. Any prop
|
|
50
|
+
* the consumer passes wins; `aria-describedby` is composed (consumer
|
|
51
|
+
* ids first, then field-supplied description / error ids). Outside a
|
|
52
|
+
* `<Field.Root>`, behaviour is unchanged.
|
|
53
|
+
*/
|
|
54
|
+
function SelectRoot({
|
|
55
|
+
children,
|
|
56
|
+
asChild = false,
|
|
57
|
+
onChange,
|
|
58
|
+
onValueChange,
|
|
59
|
+
value,
|
|
60
|
+
defaultValue,
|
|
61
|
+
...consumer
|
|
62
|
+
}: SelectRootProps) {
|
|
63
|
+
const merged = useFieldProps(consumer);
|
|
64
|
+
|
|
65
|
+
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
|
66
|
+
onChange?.(event);
|
|
67
|
+
onValueChange?.(event.target.value);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const inferredDefaultValue =
|
|
71
|
+
!asChild &&
|
|
72
|
+
value === undefined &&
|
|
73
|
+
defaultValue === undefined &&
|
|
74
|
+
hasPlaceholderChild(children)
|
|
75
|
+
? ""
|
|
76
|
+
: defaultValue;
|
|
77
|
+
|
|
78
|
+
const controlProps =
|
|
79
|
+
value !== undefined
|
|
80
|
+
? { value }
|
|
81
|
+
: inferredDefaultValue !== undefined
|
|
82
|
+
? { defaultValue: inferredDefaultValue }
|
|
83
|
+
: {};
|
|
84
|
+
|
|
85
|
+
const rootProps = {
|
|
86
|
+
...merged,
|
|
87
|
+
...controlProps,
|
|
88
|
+
"data-disabled": merged.disabled ? "" : undefined,
|
|
89
|
+
onChange: handleChange,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (asChild) {
|
|
93
|
+
return <Slot {...rootProps}>{children}</Slot>;
|
|
94
|
+
}
|
|
95
|
+
return <select {...rootProps}>{children}</select>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
SelectRoot.displayName = "SelectRoot";
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* An individual choice inside a Select — renders a native `<option>`
|
|
102
|
+
* element and passes all `OptionHTMLAttributes` through to the DOM.
|
|
103
|
+
*
|
|
104
|
+
* Native `<option>` only renders text; rich content (icons, descriptions)
|
|
105
|
+
* is not supported.
|
|
106
|
+
*/
|
|
107
|
+
function SelectOption({ children, ...rest }: SelectOptionProps) {
|
|
108
|
+
return <option {...rest}>{children}</option>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
SelectOption.displayName = "SelectOption";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Visually groups related options inside the Select popup — renders a
|
|
115
|
+
* native `<optgroup>` element. The `label` is shown by the browser as a
|
|
116
|
+
* non-selectable heading and is announced as the group's accessible name.
|
|
117
|
+
*/
|
|
118
|
+
function SelectGroup({ children, ...rest }: SelectGroupProps) {
|
|
119
|
+
return <optgroup {...rest}>{children}</optgroup>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
SelectGroup.displayName = "SelectGroup";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A non-selectable hint shown as the initial selection of a Select.
|
|
126
|
+
* Renders a native `<option value="" disabled hidden>` so the browser
|
|
127
|
+
* displays it before the user picks anything but makes it unreachable
|
|
128
|
+
* from the dropdown afterwards. Render it as the first child of
|
|
129
|
+
* {@link Select.Root} (above any `Select.Option` or `Select.Group`).
|
|
130
|
+
*
|
|
131
|
+
* Pair with `required` on {@link Select.Root} to make the browser's
|
|
132
|
+
* native form validation catch an unchosen value at submission.
|
|
133
|
+
*/
|
|
134
|
+
function SelectPlaceholder({ children, ...rest }: SelectPlaceholderProps) {
|
|
135
|
+
return (
|
|
136
|
+
<option {...rest} value="" disabled hidden>
|
|
137
|
+
{children}
|
|
138
|
+
</option>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
SelectPlaceholder.displayName = "SelectPlaceholder";
|
|
143
|
+
|
|
144
|
+
type TSelectCompound = typeof SelectRoot & {
|
|
145
|
+
Root: typeof SelectRoot;
|
|
146
|
+
Option: typeof SelectOption;
|
|
147
|
+
Group: typeof SelectGroup;
|
|
148
|
+
Placeholder: typeof SelectPlaceholder;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Headless **Select** — a compound component wrapping the native
|
|
153
|
+
* `<select>` / `<option>` / `<optgroup>` elements. Zero styles ship.
|
|
154
|
+
*
|
|
155
|
+
* Because the underlying element is the real `<select>`, the browser
|
|
156
|
+
* owns the popup, keyboard interaction (arrow keys, Home/End,
|
|
157
|
+
* typeahead), mobile UX (wheel pickers), and form submission. No
|
|
158
|
+
* positioning JS or Portal is involved.
|
|
159
|
+
*
|
|
160
|
+
* `Select` is both callable (an alias of {@link SelectRoot | `Select.Root`})
|
|
161
|
+
* and carries its sub-components as static properties.
|
|
162
|
+
*
|
|
163
|
+
* - {@link SelectRoot | `Select.Root`} — state owner, renders `<select>`.
|
|
164
|
+
* - {@link SelectOption | `Select.Option`} — renders `<option>`.
|
|
165
|
+
* - {@link SelectGroup | `Select.Group`} — renders `<optgroup label>`.
|
|
166
|
+
* - {@link SelectPlaceholder | `Select.Placeholder`} — disabled+hidden
|
|
167
|
+
* first option used as the initial hint.
|
|
168
|
+
*
|
|
169
|
+
* @example Minimal usage
|
|
170
|
+
* ```tsx
|
|
171
|
+
* import { Select } from "@primitiv-ui/react";
|
|
172
|
+
*
|
|
173
|
+
* <Select.Root defaultValue="apple" aria-label="Pick a fruit">
|
|
174
|
+
* <Select.Option value="apple">Apple</Select.Option>
|
|
175
|
+
* <Select.Option value="banana">Banana</Select.Option>
|
|
176
|
+
* </Select.Root>
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* @example With placeholder and groups
|
|
180
|
+
* ```tsx
|
|
181
|
+
* <Select.Root required aria-label="Pick a food">
|
|
182
|
+
* <Select.Placeholder>Choose…</Select.Placeholder>
|
|
183
|
+
* <Select.Group label="Fruits">
|
|
184
|
+
* <Select.Option value="apple">Apple</Select.Option>
|
|
185
|
+
* </Select.Group>
|
|
186
|
+
* <Select.Group label="Vegetables">
|
|
187
|
+
* <Select.Option value="carrot">Carrot</Select.Option>
|
|
188
|
+
* </Select.Group>
|
|
189
|
+
* </Select.Root>
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* @see {@link SelectRoot} for state modes, placeholder integration, and `asChild`.
|
|
193
|
+
* @see {@link SelectPlaceholder} for the placeholder + `defaultValue` interaction.
|
|
194
|
+
*/
|
|
195
|
+
const SelectCompound: TSelectCompound = Object.assign(SelectRoot, {
|
|
196
|
+
Root: SelectRoot,
|
|
197
|
+
Option: SelectOption,
|
|
198
|
+
Group: SelectGroup,
|
|
199
|
+
Placeholder: SelectPlaceholder,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
SelectCompound.displayName = "Select";
|
|
203
|
+
|
|
204
|
+
export { SelectCompound as Select };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { Select } from "../Select";
|
|
5
|
+
|
|
6
|
+
function StyledSelect({
|
|
7
|
+
children,
|
|
8
|
+
...rest
|
|
9
|
+
}: ComponentProps<"select"> & { children: ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<select {...rest} className="styled-select" data-testid="styled">
|
|
12
|
+
{children}
|
|
13
|
+
</select>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("Select asChild", () => {
|
|
18
|
+
it("delegates to a consumer-supplied <select> wrapper, merging Root's props onto it", () => {
|
|
19
|
+
// Arrange & Act
|
|
20
|
+
render(
|
|
21
|
+
<Select.Root asChild disabled>
|
|
22
|
+
<StyledSelect>
|
|
23
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
24
|
+
</StyledSelect>
|
|
25
|
+
</Select.Root>,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Assert — the consumer's <select> is the rendered element, with
|
|
29
|
+
// Root's props merged in.
|
|
30
|
+
const select = screen.getByTestId("styled");
|
|
31
|
+
expect(select.tagName).toBe("SELECT");
|
|
32
|
+
expect(select).toHaveClass("styled-select");
|
|
33
|
+
expect(select).toHaveAttribute("data-disabled", "");
|
|
34
|
+
expect(select).toBeDisabled();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Select } from "../Select";
|
|
4
|
+
|
|
5
|
+
describe("Select basic rendering", () => {
|
|
6
|
+
it("renders an <option> for each Select.Option child so the value is in the DOM", () => {
|
|
7
|
+
// Arrange & Act
|
|
8
|
+
render(
|
|
9
|
+
<Select.Root>
|
|
10
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
11
|
+
</Select.Root>,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Assert
|
|
15
|
+
expect(screen.getByRole("option", { name: "Apple" })).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import { Select } from "../Select";
|
|
6
|
+
|
|
7
|
+
describe("Select controlled state", () => {
|
|
8
|
+
it("calls onValueChange with the new value string when the user selects a different option", async () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
const onValueChange = vi.fn();
|
|
11
|
+
const user = userEvent.setup();
|
|
12
|
+
|
|
13
|
+
function Wrapper() {
|
|
14
|
+
const [value, setValue] = useState("apple");
|
|
15
|
+
const handle = (next: string) => {
|
|
16
|
+
onValueChange(next);
|
|
17
|
+
setValue(next);
|
|
18
|
+
};
|
|
19
|
+
return (
|
|
20
|
+
<Select.Root value={value} onValueChange={handle}>
|
|
21
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
22
|
+
<Select.Option value="banana">Banana</Select.Option>
|
|
23
|
+
</Select.Root>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
render(<Wrapper />);
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
await user.selectOptions(screen.getByRole("combobox"), "banana");
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(onValueChange).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(onValueChange).toHaveBeenCalledWith("banana");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("invokes the consumer's own onChange handler alongside onValueChange", async () => {
|
|
38
|
+
// Arrange
|
|
39
|
+
const onChange = vi.fn();
|
|
40
|
+
const onValueChange = vi.fn();
|
|
41
|
+
const user = userEvent.setup();
|
|
42
|
+
|
|
43
|
+
function Wrapper() {
|
|
44
|
+
const [value, setValue] = useState("apple");
|
|
45
|
+
return (
|
|
46
|
+
<Select.Root
|
|
47
|
+
value={value}
|
|
48
|
+
onChange={(event) => {
|
|
49
|
+
onChange(event.target.value);
|
|
50
|
+
setValue(event.target.value);
|
|
51
|
+
}}
|
|
52
|
+
onValueChange={onValueChange}
|
|
53
|
+
>
|
|
54
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
55
|
+
<Select.Option value="banana">Banana</Select.Option>
|
|
56
|
+
</Select.Root>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
render(<Wrapper />);
|
|
61
|
+
|
|
62
|
+
// Act
|
|
63
|
+
await user.selectOptions(screen.getByRole("combobox"), "banana");
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
66
|
+
expect(onChange).toHaveBeenCalledWith("banana");
|
|
67
|
+
expect(onValueChange).toHaveBeenCalledWith("banana");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Select } from "../Select";
|
|
4
|
+
|
|
5
|
+
describe("Select data attributes", () => {
|
|
6
|
+
it("renders data-disabled='' on the root <select> when disabled is true so CSS can style the disabled state", () => {
|
|
7
|
+
// Arrange & Act
|
|
8
|
+
render(
|
|
9
|
+
<Select.Root disabled>
|
|
10
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
11
|
+
</Select.Root>,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Assert
|
|
15
|
+
expect(screen.getByRole("combobox")).toHaveAttribute("data-disabled", "");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("omits data-disabled entirely when disabled is false or absent so the enabled state has nothing to override", () => {
|
|
19
|
+
// Arrange & Act
|
|
20
|
+
render(
|
|
21
|
+
<Select.Root>
|
|
22
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
23
|
+
</Select.Root>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Assert
|
|
27
|
+
expect(screen.getByRole("combobox")).not.toHaveAttribute("data-disabled");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Field } from "../../Field";
|
|
4
|
+
import { Select } from "../Select";
|
|
5
|
+
|
|
6
|
+
function renderSelect(children?: React.ReactNode) {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
{children}
|
|
10
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
11
|
+
<Select.Option value="banana">Banana</Select.Option>
|
|
12
|
+
</>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("Select — Field integration", () => {
|
|
17
|
+
it("inherits the field id when no id prop is passed", () => {
|
|
18
|
+
// Arrange & Act
|
|
19
|
+
render(
|
|
20
|
+
<Field.Root id="fruit">
|
|
21
|
+
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
22
|
+
</Field.Root>,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
expect(screen.getByRole("combobox", { name: "Fruit" })).toHaveAttribute(
|
|
27
|
+
"id",
|
|
28
|
+
"fruit",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("consumer-supplied id wins over the field id", () => {
|
|
33
|
+
// Arrange & Act
|
|
34
|
+
render(
|
|
35
|
+
<Field.Root id="fruit">
|
|
36
|
+
<Select.Root id="my-fruit" aria-label="Fruit">
|
|
37
|
+
{renderSelect()}
|
|
38
|
+
</Select.Root>
|
|
39
|
+
</Field.Root>,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
43
|
+
expect(screen.getByRole("combobox", { name: "Fruit" })).toHaveAttribute(
|
|
44
|
+
"id",
|
|
45
|
+
"my-fruit",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("inherits aria-describedby pointing at the field's descriptionId", () => {
|
|
50
|
+
// Arrange & Act
|
|
51
|
+
render(
|
|
52
|
+
<Field.Root id="fruit">
|
|
53
|
+
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
54
|
+
</Field.Root>,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Assert
|
|
58
|
+
expect(screen.getByRole("combobox", { name: "Fruit" })).toHaveAttribute(
|
|
59
|
+
"aria-describedby",
|
|
60
|
+
"fruit-description",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("includes the errorId in aria-describedby when the field is invalid", () => {
|
|
65
|
+
// Arrange & Act
|
|
66
|
+
render(
|
|
67
|
+
<Field.Root id="fruit" invalid>
|
|
68
|
+
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
69
|
+
</Field.Root>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
expect(
|
|
74
|
+
screen
|
|
75
|
+
.getByRole("combobox", { name: "Fruit" })
|
|
76
|
+
.getAttribute("aria-describedby"),
|
|
77
|
+
).toBe("fruit-description fruit-error");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("appends consumer-supplied aria-describedby to the field-supplied ids", () => {
|
|
81
|
+
// Arrange & Act
|
|
82
|
+
render(
|
|
83
|
+
<Field.Root id="fruit">
|
|
84
|
+
<Select.Root aria-label="Fruit" aria-describedby="extra-hint">
|
|
85
|
+
{renderSelect()}
|
|
86
|
+
</Select.Root>
|
|
87
|
+
</Field.Root>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
expect(
|
|
92
|
+
screen
|
|
93
|
+
.getByRole("combobox", { name: "Fruit" })
|
|
94
|
+
.getAttribute("aria-describedby"),
|
|
95
|
+
).toBe("extra-hint fruit-description");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("inherits aria-invalid='true' when the field is invalid", () => {
|
|
99
|
+
// Arrange & Act
|
|
100
|
+
render(
|
|
101
|
+
<Field.Root id="fruit" invalid>
|
|
102
|
+
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
103
|
+
</Field.Root>,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Assert
|
|
107
|
+
expect(screen.getByRole("combobox", { name: "Fruit" })).toHaveAttribute(
|
|
108
|
+
"aria-invalid",
|
|
109
|
+
"true",
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("inherits disabled from the field", () => {
|
|
114
|
+
// Arrange & Act
|
|
115
|
+
render(
|
|
116
|
+
<Field.Root id="fruit" disabled>
|
|
117
|
+
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
118
|
+
</Field.Root>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Assert
|
|
122
|
+
expect(screen.getByRole("combobox", { name: "Fruit" })).toBeDisabled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("inherits required from the field", () => {
|
|
126
|
+
// Arrange & Act
|
|
127
|
+
render(
|
|
128
|
+
<Field.Root id="fruit" required>
|
|
129
|
+
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
130
|
+
</Field.Root>,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Assert
|
|
134
|
+
expect(screen.getByRole("combobox", { name: "Fruit" })).toBeRequired();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("Select outside Field.Root behaves identically to before — no field-derived attributes", () => {
|
|
138
|
+
// Arrange & Act
|
|
139
|
+
render(
|
|
140
|
+
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>,
|
|
141
|
+
);
|
|
142
|
+
const select = screen.getByRole("combobox", { name: "Fruit" });
|
|
143
|
+
|
|
144
|
+
// Assert
|
|
145
|
+
expect(select).not.toHaveAttribute("aria-describedby");
|
|
146
|
+
expect(select).not.toHaveAttribute("aria-invalid");
|
|
147
|
+
expect(select).not.toHaveAttribute("disabled");
|
|
148
|
+
expect(select).not.toHaveAttribute("required");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Select } from "../Select";
|
|
4
|
+
|
|
5
|
+
describe("Select group", () => {
|
|
6
|
+
it("renders an <optgroup> with the given label so options can be visually grouped", () => {
|
|
7
|
+
// Arrange & Act
|
|
8
|
+
render(
|
|
9
|
+
<Select.Root>
|
|
10
|
+
<Select.Group label="Fruits">
|
|
11
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
12
|
+
<Select.Option value="banana">Banana</Select.Option>
|
|
13
|
+
</Select.Group>
|
|
14
|
+
<Select.Group label="Vegetables">
|
|
15
|
+
<Select.Option value="carrot">Carrot</Select.Option>
|
|
16
|
+
</Select.Group>
|
|
17
|
+
</Select.Root>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Assert — native <optgroup> has implicit role="group" with the
|
|
21
|
+
// label attribute as its accessible name.
|
|
22
|
+
expect(screen.getByRole("group", { name: "Fruits" })).toBeInTheDocument();
|
|
23
|
+
expect(
|
|
24
|
+
screen.getByRole("group", { name: "Vegetables" }),
|
|
25
|
+
).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("nests its Option children inside the rendered <optgroup>", () => {
|
|
29
|
+
// Arrange & Act
|
|
30
|
+
render(
|
|
31
|
+
<Select.Root>
|
|
32
|
+
<Select.Group label="Fruits">
|
|
33
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
34
|
+
</Select.Group>
|
|
35
|
+
</Select.Root>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
const option = screen.getByRole("option", { name: "Apple" });
|
|
40
|
+
expect(option.parentElement?.tagName).toBe("OPTGROUP");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Select } from "../Select";
|
|
4
|
+
|
|
5
|
+
describe("Select placeholder", () => {
|
|
6
|
+
it("renders a non-selectable placeholder option that holds the initial selection until the user picks something", () => {
|
|
7
|
+
// Arrange & Act
|
|
8
|
+
const { container } = render(
|
|
9
|
+
<Select.Root>
|
|
10
|
+
<Select.Placeholder>Choose a fruit…</Select.Placeholder>
|
|
11
|
+
<Select.Option value="apple">Apple</Select.Option>
|
|
12
|
+
</Select.Root>,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// Assert — placeholder is in the DOM with the attributes that make it
|
|
16
|
+
// unreachable from the dropdown after the first selection. The
|
|
17
|
+
// `hidden` attribute also pulls it out of the ARIA tree, so query the
|
|
18
|
+
// DOM directly rather than via getByRole.
|
|
19
|
+
const placeholder = container.querySelector(
|
|
20
|
+
"option[hidden]",
|
|
21
|
+
) as HTMLOptionElement | null;
|
|
22
|
+
expect(placeholder).not.toBeNull();
|
|
23
|
+
expect(placeholder).toBeDisabled();
|
|
24
|
+
expect(placeholder).toHaveAttribute("value", "");
|
|
25
|
+
expect(placeholder).toHaveTextContent("Choose a fruit…");
|
|
26
|
+
|
|
27
|
+
// …and is the initial selection because it's the first option with an
|
|
28
|
+
// empty value.
|
|
29
|
+
const select = screen.getByRole("combobox") as HTMLSelectElement;
|
|
30
|
+
expect(select.value).toBe("");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ChangeEventHandler, ComponentProps, ReactNode, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
type SelectRootBaseProps = Omit<
|
|
4
|
+
ComponentProps<"select">,
|
|
5
|
+
"value" | "defaultValue" | "multiple" | "onChange"
|
|
6
|
+
> & {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
ref?: Ref<HTMLSelectElement>;
|
|
9
|
+
/**
|
|
10
|
+
* Native `change` handler. Fires alongside `onValueChange` whenever the
|
|
11
|
+
* user picks a different option. Use this when you want the raw
|
|
12
|
+
* `ChangeEvent` (e.g. to inspect `event.target.validity`).
|
|
13
|
+
*/
|
|
14
|
+
onChange?: ChangeEventHandler<HTMLSelectElement>;
|
|
15
|
+
/**
|
|
16
|
+
* When `true`, Root delegates to a single consumer-supplied element
|
|
17
|
+
* (expected to render a `<select>`) and merges its own props onto it
|
|
18
|
+
* via the {@link Slot} pattern. The placeholder-detection inside Root
|
|
19
|
+
* walks direct children only, so placeholder + `asChild` requires the
|
|
20
|
+
* consumer to set `defaultValue=""` explicitly.
|
|
21
|
+
*/
|
|
22
|
+
asChild?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type SelectRootUncontrolledProps = SelectRootBaseProps & {
|
|
26
|
+
defaultValue?: string;
|
|
27
|
+
value?: never;
|
|
28
|
+
onValueChange?: (value: string) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type SelectRootControlledProps = SelectRootBaseProps & {
|
|
32
|
+
defaultValue?: never;
|
|
33
|
+
value: string;
|
|
34
|
+
onValueChange: (value: string) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Props for {@link Select.Root}.
|
|
39
|
+
*
|
|
40
|
+
* Two state modes are statically discriminated at the type level so only
|
|
41
|
+
* one shape is accepted by TypeScript:
|
|
42
|
+
*
|
|
43
|
+
* - **Uncontrolled** — pass `defaultValue` (or omit it). The browser owns
|
|
44
|
+
* the selection state. `onValueChange` is optional.
|
|
45
|
+
* - **Controlled** — pass `value` and `onValueChange` together. The
|
|
46
|
+
* parent owns the selection; the component defers every transition
|
|
47
|
+
* back through the callback.
|
|
48
|
+
*
|
|
49
|
+
* Native `multiple`-selection mode is not supported in v1.
|
|
50
|
+
*/
|
|
51
|
+
export type SelectRootProps =
|
|
52
|
+
| SelectRootUncontrolledProps
|
|
53
|
+
| SelectRootControlledProps;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Props for {@link Select.Option} — all `OptionHTMLAttributes` on the
|
|
57
|
+
* underlying `<option>` element, plus a typed `ref`.
|
|
58
|
+
*/
|
|
59
|
+
export type SelectOptionProps = ComponentProps<"option"> & {
|
|
60
|
+
children?: ReactNode;
|
|
61
|
+
ref?: Ref<HTMLOptionElement>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Props for {@link Select.Group} — all `OptgroupHTMLAttributes` on the
|
|
66
|
+
* underlying `<optgroup>` element, plus a typed `ref`. `label` is
|
|
67
|
+
* required by the native element and is what assistive technology
|
|
68
|
+
* announces for the group.
|
|
69
|
+
*/
|
|
70
|
+
export type SelectGroupProps = ComponentProps<"optgroup"> & {
|
|
71
|
+
label: string;
|
|
72
|
+
children?: ReactNode;
|
|
73
|
+
ref?: Ref<HTMLOptGroupElement>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Props for {@link Select.Placeholder}.
|
|
78
|
+
*
|
|
79
|
+
* `value`, `disabled`, and `hidden` are owned by the component — the
|
|
80
|
+
* placeholder always has `value=""`, is always disabled, and is always
|
|
81
|
+
* hidden from the dropdown — so they can't be set by the consumer.
|
|
82
|
+
*/
|
|
83
|
+
export type SelectPlaceholderProps = Omit<
|
|
84
|
+
ComponentProps<"option">,
|
|
85
|
+
"value" | "disabled" | "hidden"
|
|
86
|
+
> & {
|
|
87
|
+
children?: ReactNode;
|
|
88
|
+
ref?: Ref<HTMLOptionElement>;
|
|
89
|
+
};
|