@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,228 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import { Slot, composeEventHandlers } from "../Slot";
|
|
4
|
+
|
|
5
|
+
import { CheckboxContext } from "./CheckboxContext";
|
|
6
|
+
import { useCheckboxContext, useCheckboxRoot } from "./hooks";
|
|
7
|
+
import {
|
|
8
|
+
CheckboxIndicatorProps,
|
|
9
|
+
CheckboxRootProps,
|
|
10
|
+
CheckedState,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
function dataStateOf(checked: CheckedState) {
|
|
14
|
+
if (checked === "indeterminate") return "indeterminate" as const;
|
|
15
|
+
return checked ? ("checked" as const) : ("unchecked" as const);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The root of a Checkbox — a native `<button role="checkbox">` that
|
|
20
|
+
* owns the tri-state checked value and provides
|
|
21
|
+
* {@link CheckboxContext | `CheckboxContext`} to descendant
|
|
22
|
+
* {@link CheckboxIndicator | `Checkbox.Indicator`}s.
|
|
23
|
+
*
|
|
24
|
+
* Supports two state modes, statically discriminated at the type level:
|
|
25
|
+
*
|
|
26
|
+
* - **Uncontrolled** — pass
|
|
27
|
+
* {@link CheckboxRootProps.defaultChecked | `defaultChecked`} (or omit
|
|
28
|
+
* for unchecked-on-mount). The component owns the value internally.
|
|
29
|
+
* - **Controlled** — pass
|
|
30
|
+
* {@link CheckboxRootProps.checked | `checked`} *and*
|
|
31
|
+
* {@link CheckboxRootProps.onCheckedChange | `onCheckedChange`}
|
|
32
|
+
* together. The parent owns the value; the component defers every
|
|
33
|
+
* change back through the callback.
|
|
34
|
+
*
|
|
35
|
+
* Both `checked` and `defaultChecked` accept `boolean | "indeterminate"`.
|
|
36
|
+
* Clicking an indeterminate checkbox resolves it to `true` per the
|
|
37
|
+
* WAI-ARIA tri-state convention, then flips boolean on subsequent clicks.
|
|
38
|
+
*
|
|
39
|
+
* **ARIA.** `role="checkbox"` and `aria-checked` are set automatically;
|
|
40
|
+
* `aria-checked="mixed"` represents the indeterminate state.
|
|
41
|
+
*
|
|
42
|
+
* **Styling hooks.** `data-state="checked" | "unchecked" | "indeterminate"`
|
|
43
|
+
* on the root, plus `data-disabled=""` when disabled.
|
|
44
|
+
*
|
|
45
|
+
* **`asChild` prop.** Pass `asChild` to render any consumer-supplied
|
|
46
|
+
* element (e.g. `<li role="menuitemcheckbox">` for menu composition)
|
|
47
|
+
* with the checkbox's ARIA attributes, data-state, composed onClick, and
|
|
48
|
+
* ref merged in. The native `<button>` is dropped; consumers who want
|
|
49
|
+
* keyboard activation on a non-button element are responsible for
|
|
50
|
+
* providing it.
|
|
51
|
+
*
|
|
52
|
+
* @example Uncontrolled
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <Checkbox.Root defaultChecked aria-label="Accept terms">
|
|
55
|
+
* <Checkbox.Indicator>
|
|
56
|
+
* <CheckIcon />
|
|
57
|
+
* </Checkbox.Indicator>
|
|
58
|
+
* </Checkbox.Root>
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @example Controlled
|
|
62
|
+
* ```tsx
|
|
63
|
+
* const [checked, setChecked] = useState<CheckedState>(false);
|
|
64
|
+
*
|
|
65
|
+
* <Checkbox.Root checked={checked} onCheckedChange={setChecked} aria-label="…">
|
|
66
|
+
* <Checkbox.Indicator>
|
|
67
|
+
* <CheckIcon />
|
|
68
|
+
* </Checkbox.Indicator>
|
|
69
|
+
* </Checkbox.Root>
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example Composed into a menu item via `asChild`
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <Checkbox.Root asChild aria-label="Show hidden files">
|
|
75
|
+
* <li role="menuitemcheckbox">Show hidden files</li>
|
|
76
|
+
* </Checkbox.Root>
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
function CheckboxRoot(props: CheckboxRootProps) {
|
|
80
|
+
const {
|
|
81
|
+
defaultChecked,
|
|
82
|
+
checked,
|
|
83
|
+
onCheckedChange,
|
|
84
|
+
onClick,
|
|
85
|
+
disabled,
|
|
86
|
+
asChild = false,
|
|
87
|
+
children,
|
|
88
|
+
...rest
|
|
89
|
+
} = props;
|
|
90
|
+
const { checked: isChecked, toggle } = useCheckboxRoot({
|
|
91
|
+
defaultChecked,
|
|
92
|
+
checked,
|
|
93
|
+
onCheckedChange,
|
|
94
|
+
});
|
|
95
|
+
const contextValue = useMemo(() => ({ checked: isChecked }), [isChecked]);
|
|
96
|
+
const rootProps = {
|
|
97
|
+
...rest,
|
|
98
|
+
role: "checkbox" as const,
|
|
99
|
+
"aria-checked":
|
|
100
|
+
isChecked === "indeterminate"
|
|
101
|
+
? ("mixed" as const)
|
|
102
|
+
: (isChecked as boolean),
|
|
103
|
+
"data-state": dataStateOf(isChecked),
|
|
104
|
+
"data-disabled": disabled ? "" : undefined,
|
|
105
|
+
disabled,
|
|
106
|
+
onClick: composeEventHandlers(onClick, toggle),
|
|
107
|
+
};
|
|
108
|
+
return (
|
|
109
|
+
<CheckboxContext.Provider value={contextValue}>
|
|
110
|
+
{asChild ? (
|
|
111
|
+
<Slot {...rootProps}>{children}</Slot>
|
|
112
|
+
) : (
|
|
113
|
+
<button type="button" {...rootProps}>
|
|
114
|
+
{children}
|
|
115
|
+
</button>
|
|
116
|
+
)}
|
|
117
|
+
</CheckboxContext.Provider>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
CheckboxRoot.displayName = "CheckboxRoot";
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A decorative `<span aria-hidden="true">` that renders its children
|
|
125
|
+
* only while the parent {@link CheckboxRoot | `Checkbox.Root`} is
|
|
126
|
+
* **checked** or **indeterminate** — never when unchecked. The
|
|
127
|
+
* checkbox's accessible state is already conveyed by `aria-checked`
|
|
128
|
+
* on the root, so the indicator is purely visual.
|
|
129
|
+
*
|
|
130
|
+
* **Styling hook.** Mirrors the root's
|
|
131
|
+
* `data-state="checked" | "unchecked" | "indeterminate"` so the same
|
|
132
|
+
* CSS rules can target both.
|
|
133
|
+
*
|
|
134
|
+
* **`asChild` prop.** Pass `asChild` to render the consumer's own
|
|
135
|
+
* element (typically an `<svg>` tick icon) as the indicator itself,
|
|
136
|
+
* with `aria-hidden` and `data-state` merged onto that element rather
|
|
137
|
+
* than a wrapper.
|
|
138
|
+
*
|
|
139
|
+
* **`forceMount` prop.** Pass `forceMount` to keep the indicator in
|
|
140
|
+
* the DOM while unchecked so a CSS exit animation can play against
|
|
141
|
+
* `data-state="unchecked"`. Consumers who use `forceMount` own the
|
|
142
|
+
* exit lifecycle themselves.
|
|
143
|
+
*
|
|
144
|
+
* @example Default span wrapper
|
|
145
|
+
* ```tsx
|
|
146
|
+
* <Checkbox.Indicator>
|
|
147
|
+
* <CheckIcon />
|
|
148
|
+
* </Checkbox.Indicator>
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* @example Icon as the indicator via `asChild`
|
|
152
|
+
* ```tsx
|
|
153
|
+
* <Checkbox.Indicator asChild>
|
|
154
|
+
* <svg viewBox="0 0 10 10"><path d="M1 5l3 3 5-7" /></svg>
|
|
155
|
+
* </Checkbox.Indicator>
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @example Force-mounted for exit animation
|
|
159
|
+
* ```tsx
|
|
160
|
+
* <Checkbox.Indicator forceMount>
|
|
161
|
+
* <CheckIcon />
|
|
162
|
+
* </Checkbox.Indicator>
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* @throws if rendered outside a `Checkbox.Root`.
|
|
166
|
+
*/
|
|
167
|
+
function CheckboxIndicator({
|
|
168
|
+
children,
|
|
169
|
+
forceMount,
|
|
170
|
+
asChild = false,
|
|
171
|
+
...rest
|
|
172
|
+
}: CheckboxIndicatorProps) {
|
|
173
|
+
const { checked } = useCheckboxContext();
|
|
174
|
+
const isVisible = checked !== false;
|
|
175
|
+
if (!isVisible && !forceMount) return null;
|
|
176
|
+
const indicatorProps = {
|
|
177
|
+
...rest,
|
|
178
|
+
"aria-hidden": "true" as const,
|
|
179
|
+
"data-state": dataStateOf(checked),
|
|
180
|
+
};
|
|
181
|
+
if (asChild) {
|
|
182
|
+
return <Slot {...indicatorProps}>{children}</Slot>;
|
|
183
|
+
}
|
|
184
|
+
return <span {...indicatorProps}>{children}</span>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
CheckboxIndicator.displayName = "CheckboxIndicator";
|
|
188
|
+
|
|
189
|
+
type TCheckboxCompound = typeof CheckboxRoot & {
|
|
190
|
+
Root: typeof CheckboxRoot;
|
|
191
|
+
Indicator: typeof CheckboxIndicator;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Headless, accessible **Checkbox** — a compound component built on a
|
|
196
|
+
* native `<button role="checkbox">` that implements the
|
|
197
|
+
* [WAI-ARIA Checkbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/)
|
|
198
|
+
* including the tri-state ("mixed") variant. Zero styles ship.
|
|
199
|
+
*
|
|
200
|
+
* `Checkbox` is both callable (an alias of {@link CheckboxRoot | `Checkbox.Root`})
|
|
201
|
+
* and carries its sub-components as static properties. Prefer the
|
|
202
|
+
* namespaced form in application code for readability and grep-ability.
|
|
203
|
+
*
|
|
204
|
+
* - {@link CheckboxRoot | `Checkbox.Root`} — state owner, context provider, toggle button.
|
|
205
|
+
* - {@link CheckboxIndicator | `Checkbox.Indicator`} — decorative tick, conditional on checked state.
|
|
206
|
+
*
|
|
207
|
+
* @example Minimal usage
|
|
208
|
+
* ```tsx
|
|
209
|
+
* import { Checkbox } from "@primitiv-ui/react";
|
|
210
|
+
*
|
|
211
|
+
* <Checkbox.Root aria-label="Accept terms">
|
|
212
|
+
* <Checkbox.Indicator>
|
|
213
|
+
* <CheckIcon />
|
|
214
|
+
* </Checkbox.Indicator>
|
|
215
|
+
* </Checkbox.Root>;
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* @see {@link CheckboxRoot} for state modes and tri-state semantics.
|
|
219
|
+
* @see {@link CheckboxIndicator} for the mount gate and animation hooks.
|
|
220
|
+
*/
|
|
221
|
+
const CheckboxCompound: TCheckboxCompound = Object.assign(CheckboxRoot, {
|
|
222
|
+
Root: CheckboxRoot,
|
|
223
|
+
Indicator: CheckboxIndicator,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
CheckboxCompound.displayName = "Checkbox";
|
|
227
|
+
|
|
228
|
+
export { CheckboxCompound as Checkbox };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createStrictContext } from "../utils";
|
|
2
|
+
|
|
3
|
+
import { CheckedState } from "./types";
|
|
4
|
+
|
|
5
|
+
export type CheckboxContextValue = {
|
|
6
|
+
checked: CheckedState;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const [CheckboxContext, useCheckboxContext] =
|
|
10
|
+
createStrictContext<CheckboxContextValue>(
|
|
11
|
+
"Checkbox sub-components must be rendered inside a <Checkbox.Root>.",
|
|
12
|
+
);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Checkbox
|
|
2
|
+
|
|
3
|
+
A headless, accessible compound component implementing the
|
|
4
|
+
[WAI-ARIA Checkbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/),
|
|
5
|
+
including the tri-state ("mixed") variant.
|
|
6
|
+
|
|
7
|
+
Checkbox renders a native `<button role="checkbox">` so it gets
|
|
8
|
+
keyboard activation (`Space` / `Enter`), focus ring, and disabled
|
|
9
|
+
semantics for free from the browser. The React layer adds three-state
|
|
10
|
+
support, `Indicator` mounting driven by the checked state, and the
|
|
11
|
+
`asChild` composition every primitive in this package supports.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Checkbox } from "@primitiv-ui/react";
|
|
15
|
+
|
|
16
|
+
<Checkbox.Root defaultChecked aria-label="Accept terms">
|
|
17
|
+
<Checkbox.Indicator>
|
|
18
|
+
<CheckIcon />
|
|
19
|
+
</Checkbox.Indicator>
|
|
20
|
+
</Checkbox.Root>;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Sub-components
|
|
24
|
+
|
|
25
|
+
| Export | Element | Notes |
|
|
26
|
+
| -------------------- | ---------- | ------------------------------------------------------------------------------------------ |
|
|
27
|
+
| `Checkbox.Root` | `<button>` | `role="checkbox"`, `aria-checked`, `data-state`, `data-disabled`. `asChild` |
|
|
28
|
+
| `Checkbox.Indicator` | `<span>` | `aria-hidden="true"`. Renders only while checked or indeterminate. `asChild`, `forceMount` |
|
|
29
|
+
|
|
30
|
+
## Checked state
|
|
31
|
+
|
|
32
|
+
Checkbox is tri-state. The `checked` / `defaultChecked` value is
|
|
33
|
+
`boolean | "indeterminate"`:
|
|
34
|
+
|
|
35
|
+
| Value | `aria-checked` | `data-state` | Indicator renders? |
|
|
36
|
+
| ----------------- | -------------- | ----------------- | ------------------------ |
|
|
37
|
+
| `true` | `"true"` | `"checked"` | Yes |
|
|
38
|
+
| `false` | `"false"` | `"unchecked"` | No (unless `forceMount`) |
|
|
39
|
+
| `"indeterminate"` | `"mixed"` | `"indeterminate"` | Yes |
|
|
40
|
+
|
|
41
|
+
Clicking an indeterminate checkbox resolves it to `true` per the
|
|
42
|
+
WAI-ARIA tri-state convention; subsequent clicks flip between `true`
|
|
43
|
+
and `false`.
|
|
44
|
+
|
|
45
|
+
## State modes
|
|
46
|
+
|
|
47
|
+
- **Uncontrolled** — pass `defaultChecked` (or omit for unchecked on mount).
|
|
48
|
+
- **Controlled** — pass `checked` **and** `onCheckedChange` together.
|
|
49
|
+
|
|
50
|
+
The two shapes are statically discriminated at the type level;
|
|
51
|
+
TypeScript rejects mixing them.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// Uncontrolled
|
|
55
|
+
<Checkbox.Root defaultChecked>…</Checkbox.Root>;
|
|
56
|
+
|
|
57
|
+
// Controlled
|
|
58
|
+
const [checked, setChecked] = useState<CheckedState>(false);
|
|
59
|
+
<Checkbox.Root checked={checked} onCheckedChange={setChecked}>
|
|
60
|
+
…
|
|
61
|
+
</Checkbox.Root>;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Keyboard interaction
|
|
65
|
+
|
|
66
|
+
| Key | Behaviour |
|
|
67
|
+
| ------- | ---------------------------------------- |
|
|
68
|
+
| `Space` | Toggles the checkbox (native `<button>`) |
|
|
69
|
+
| `Enter` | Toggles the checkbox (native `<button>`) |
|
|
70
|
+
|
|
71
|
+
Keyboard handling comes from the underlying `<button>` element — no
|
|
72
|
+
custom `keydown` listeners are needed.
|
|
73
|
+
|
|
74
|
+
## Disabled
|
|
75
|
+
|
|
76
|
+
Passing `disabled` forwards the native `disabled` attribute to the
|
|
77
|
+
button (removing it from the tab order and suppressing clicks) **and**
|
|
78
|
+
sets `data-disabled=""` on the root so CSS can target
|
|
79
|
+
`[data-disabled]` without reaching for `:disabled`.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<Checkbox.Root disabled aria-label="Locked setting">
|
|
83
|
+
<Checkbox.Indicator>
|
|
84
|
+
<CheckIcon />
|
|
85
|
+
</Checkbox.Indicator>
|
|
86
|
+
</Checkbox.Root>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## `asChild` composition
|
|
90
|
+
|
|
91
|
+
Both `Checkbox.Root` and `Checkbox.Indicator` accept an `asChild`
|
|
92
|
+
boolean. When set, the component delegates rendering to its single
|
|
93
|
+
child element and merges its own ARIA attributes, data-state, composed
|
|
94
|
+
event handlers, and ref onto the child (the asChild contract: the
|
|
95
|
+
child's handler runs first, the library's runs second unless the child
|
|
96
|
+
calls `preventDefault`).
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// Menu-item checkbox — the same state machinery, different element + role.
|
|
100
|
+
<Checkbox.Root asChild aria-label="Show hidden files">
|
|
101
|
+
<li role="menuitemcheckbox">Show hidden files</li>
|
|
102
|
+
</Checkbox.Root>
|
|
103
|
+
|
|
104
|
+
// Icon-only indicator — the svg itself becomes the indicator.
|
|
105
|
+
<Checkbox.Indicator asChild>
|
|
106
|
+
<svg viewBox="0 0 10 10">
|
|
107
|
+
<path d="M1 5l3 3 5-7" />
|
|
108
|
+
</svg>
|
|
109
|
+
</Checkbox.Indicator>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`Root`'s `asChild` is what lets `Dropdown.CheckboxItem` (in a later
|
|
113
|
+
phase) wrap an `<li role="menuitemcheckbox">` around the Checkbox
|
|
114
|
+
state / toggle behaviour without re-implementing any of it.
|
|
115
|
+
|
|
116
|
+
## Animation hooks
|
|
117
|
+
|
|
118
|
+
`Checkbox.Indicator` accepts a `forceMount` boolean. When set, the
|
|
119
|
+
indicator stays in the DOM regardless of checked state so a CSS
|
|
120
|
+
animation can play against `data-state="unchecked"`:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
<Checkbox.Indicator forceMount>
|
|
124
|
+
<CheckIcon />
|
|
125
|
+
</Checkbox.Indicator>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```css
|
|
129
|
+
[data-state="checked"] {
|
|
130
|
+
animation: tick-in 120ms ease-out;
|
|
131
|
+
}
|
|
132
|
+
[data-state="unchecked"] {
|
|
133
|
+
animation: tick-out 100ms ease-in forwards;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Consumers using `forceMount` own the exit timing themselves.
|
|
138
|
+
|
|
139
|
+
## Styling hooks
|
|
140
|
+
|
|
141
|
+
`data-state="checked" | "unchecked" | "indeterminate"` is set on both
|
|
142
|
+
`Checkbox.Root` and `Checkbox.Indicator`, letting any CSS system target
|
|
143
|
+
the three phases.
|
|
144
|
+
|
|
145
|
+
```css
|
|
146
|
+
button[data-state="checked"] {
|
|
147
|
+
background: oklch(65% 0.18 145);
|
|
148
|
+
}
|
|
149
|
+
button[data-state="indeterminate"] {
|
|
150
|
+
background: oklch(70% 0.1 250);
|
|
151
|
+
}
|
|
152
|
+
button[data-disabled] {
|
|
153
|
+
opacity: 0.5;
|
|
154
|
+
cursor: not-allowed;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Checkbox } from "../Checkbox";
|
|
5
|
+
|
|
6
|
+
describe("Checkbox asChild composition", () => {
|
|
7
|
+
it("Root asChild delegates to the child element while keeping ARIA and toggle wiring", async () => {
|
|
8
|
+
// Arrange
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
const onCheckedChange = vi.fn();
|
|
11
|
+
render(
|
|
12
|
+
<Checkbox.Root
|
|
13
|
+
asChild
|
|
14
|
+
onCheckedChange={onCheckedChange}
|
|
15
|
+
aria-label="Accept terms"
|
|
16
|
+
>
|
|
17
|
+
<li>Accept</li>
|
|
18
|
+
</Checkbox.Root>,
|
|
19
|
+
);
|
|
20
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
21
|
+
|
|
22
|
+
// Assert element is the consumer's <li>
|
|
23
|
+
expect(checkbox.tagName).toBe("LI");
|
|
24
|
+
// ARIA + data-state merged onto the child
|
|
25
|
+
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
26
|
+
expect(checkbox).toHaveAttribute("data-state", "unchecked");
|
|
27
|
+
|
|
28
|
+
// Act
|
|
29
|
+
await user.click(checkbox);
|
|
30
|
+
|
|
31
|
+
// Assert toggle still fires through composed onClick
|
|
32
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
33
|
+
expect(checkbox).toHaveAttribute("aria-checked", "true");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("Root asChild lets the consumer override the role for menu-item contexts", () => {
|
|
37
|
+
// Arrange & Act
|
|
38
|
+
render(
|
|
39
|
+
<Checkbox.Root asChild aria-label="Accept terms">
|
|
40
|
+
<li role="menuitemcheckbox">Accept</li>
|
|
41
|
+
</Checkbox.Root>,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
const item = screen.getByRole("menuitemcheckbox", { name: "Accept terms" });
|
|
46
|
+
expect(item).toHaveAttribute("aria-checked", "false");
|
|
47
|
+
expect(item).toHaveAttribute("data-state", "unchecked");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("Indicator asChild delegates rendering to the consumer's element", () => {
|
|
51
|
+
// Arrange & Act
|
|
52
|
+
render(
|
|
53
|
+
<Checkbox.Root defaultChecked aria-label="Accept terms">
|
|
54
|
+
<Checkbox.Indicator asChild>
|
|
55
|
+
<svg data-testid="tick" viewBox="0 0 10 10">
|
|
56
|
+
<path d="M1 5l3 3 5-7" />
|
|
57
|
+
</svg>
|
|
58
|
+
</Checkbox.Indicator>
|
|
59
|
+
</Checkbox.Root>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
const tick = screen.getByTestId("tick");
|
|
64
|
+
expect(tick.tagName.toLowerCase()).toBe("svg");
|
|
65
|
+
expect(tick).toHaveAttribute("aria-hidden", "true");
|
|
66
|
+
expect(tick).toHaveAttribute("data-state", "checked");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Checkbox } from "../Checkbox";
|
|
4
|
+
|
|
5
|
+
describe("Checkbox basic rendering", () => {
|
|
6
|
+
it('renders a <button> with role="checkbox"', () => {
|
|
7
|
+
// Arrange & Act
|
|
8
|
+
render(<Checkbox.Root aria-label="Accept terms" />);
|
|
9
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
10
|
+
|
|
11
|
+
// Assert
|
|
12
|
+
expect(checkbox.tagName).toBe("BUTTON");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('defaults aria-checked to "false"', () => {
|
|
16
|
+
// Arrange & Act
|
|
17
|
+
render(<Checkbox.Root aria-label="Accept terms" />);
|
|
18
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
19
|
+
|
|
20
|
+
// Assert
|
|
21
|
+
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('defaults type="button" so the checkbox never submits an enclosing form', () => {
|
|
25
|
+
// Arrange & Act
|
|
26
|
+
render(<Checkbox.Root aria-label="Accept terms" />);
|
|
27
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
expect(checkbox).toHaveAttribute("type", "button");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('sets data-state="unchecked" on the root when unchecked', () => {
|
|
34
|
+
// Arrange & Act
|
|
35
|
+
render(<Checkbox.Root aria-label="Accept terms" />);
|
|
36
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
expect(checkbox).toHaveAttribute("data-state", "unchecked");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { Checkbox } from "../Checkbox";
|
|
6
|
+
|
|
7
|
+
describe("Checkbox controlled state", () => {
|
|
8
|
+
it("reflects the controlled `checked` prop", () => {
|
|
9
|
+
// Arrange & Act
|
|
10
|
+
const { rerender } = render(
|
|
11
|
+
<Checkbox.Root
|
|
12
|
+
checked={false}
|
|
13
|
+
onCheckedChange={() => {}}
|
|
14
|
+
aria-label="Accept terms"
|
|
15
|
+
/>,
|
|
16
|
+
);
|
|
17
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
18
|
+
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
19
|
+
|
|
20
|
+
rerender(
|
|
21
|
+
<Checkbox.Root
|
|
22
|
+
checked
|
|
23
|
+
onCheckedChange={() => {}}
|
|
24
|
+
aria-label="Accept terms"
|
|
25
|
+
/>,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(checkbox).toHaveAttribute("aria-checked", "true");
|
|
30
|
+
expect(checkbox).toHaveAttribute("data-state", "checked");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("does not update its rendered state when the parent refuses to update `checked`", async () => {
|
|
34
|
+
// Arrange
|
|
35
|
+
const user = userEvent.setup();
|
|
36
|
+
const onCheckedChange = vi.fn();
|
|
37
|
+
render(
|
|
38
|
+
<Checkbox.Root
|
|
39
|
+
checked={false}
|
|
40
|
+
onCheckedChange={onCheckedChange}
|
|
41
|
+
aria-label="Accept terms"
|
|
42
|
+
/>,
|
|
43
|
+
);
|
|
44
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
await user.click(checkbox);
|
|
48
|
+
|
|
49
|
+
// Assert: callback fired but the rendered state stays false because the
|
|
50
|
+
// parent did not flip the controlled prop.
|
|
51
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
52
|
+
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
53
|
+
expect(checkbox).toHaveAttribute("data-state", "unchecked");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("lets a parent drive the value end to end", async () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
function Harness() {
|
|
60
|
+
// Start true so the pre-click state can only be correct if the
|
|
61
|
+
// controlled prop is honoured (a broken impl would fall back to
|
|
62
|
+
// defaultChecked=false and render the opposite).
|
|
63
|
+
const [checked, setChecked] = useState(true);
|
|
64
|
+
return (
|
|
65
|
+
<Checkbox.Root
|
|
66
|
+
checked={checked}
|
|
67
|
+
onCheckedChange={setChecked}
|
|
68
|
+
aria-label="Accept terms"
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
render(<Harness />);
|
|
73
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
74
|
+
expect(checkbox).toHaveAttribute("aria-checked", "true");
|
|
75
|
+
|
|
76
|
+
// Act & Assert
|
|
77
|
+
await user.click(checkbox);
|
|
78
|
+
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
79
|
+
await user.click(checkbox);
|
|
80
|
+
expect(checkbox).toHaveAttribute("aria-checked", "true");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Checkbox } from "../Checkbox";
|
|
4
|
+
|
|
5
|
+
describe("Checkbox disabled state", () => {
|
|
6
|
+
it('sets data-disabled="" on the root so CSS can target the disabled state', () => {
|
|
7
|
+
// Arrange & Act
|
|
8
|
+
render(<Checkbox.Root disabled aria-label="Accept terms" />);
|
|
9
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
10
|
+
|
|
11
|
+
// Assert
|
|
12
|
+
expect(checkbox).toHaveAttribute("data-disabled", "");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Checkbox } from "../Checkbox";
|
|
5
|
+
|
|
6
|
+
describe("Checkbox indeterminate state", () => {
|
|
7
|
+
it('exposes aria-checked="mixed" when defaultChecked is "indeterminate"', () => {
|
|
8
|
+
// Arrange & Act
|
|
9
|
+
render(
|
|
10
|
+
<Checkbox.Root
|
|
11
|
+
defaultChecked="indeterminate"
|
|
12
|
+
aria-label="Accept terms"
|
|
13
|
+
/>,
|
|
14
|
+
);
|
|
15
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
16
|
+
|
|
17
|
+
// Assert
|
|
18
|
+
expect(checkbox).toHaveAttribute("aria-checked", "mixed");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('sets data-state="indeterminate" on the root in indeterminate mode', () => {
|
|
22
|
+
// Arrange & Act
|
|
23
|
+
render(
|
|
24
|
+
<Checkbox.Root
|
|
25
|
+
defaultChecked="indeterminate"
|
|
26
|
+
aria-label="Accept terms"
|
|
27
|
+
/>,
|
|
28
|
+
);
|
|
29
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
32
|
+
expect(checkbox).toHaveAttribute("data-state", "indeterminate");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("resolves to checked=true on the first click (WAI-ARIA tri-state convention)", async () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const user = userEvent.setup();
|
|
38
|
+
const onCheckedChange = vi.fn();
|
|
39
|
+
render(
|
|
40
|
+
<Checkbox.Root
|
|
41
|
+
defaultChecked="indeterminate"
|
|
42
|
+
onCheckedChange={onCheckedChange}
|
|
43
|
+
aria-label="Accept terms"
|
|
44
|
+
/>,
|
|
45
|
+
);
|
|
46
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
await user.click(checkbox);
|
|
50
|
+
|
|
51
|
+
// Assert
|
|
52
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
53
|
+
expect(checkbox).toHaveAttribute("aria-checked", "true");
|
|
54
|
+
expect(checkbox).toHaveAttribute("data-state", "checked");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('honours controlled checked="indeterminate" across re-renders', () => {
|
|
58
|
+
// Arrange
|
|
59
|
+
const { rerender } = render(
|
|
60
|
+
<Checkbox.Root
|
|
61
|
+
checked={false}
|
|
62
|
+
onCheckedChange={() => {}}
|
|
63
|
+
aria-label="Accept terms"
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
67
|
+
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
rerender(
|
|
71
|
+
<Checkbox.Root
|
|
72
|
+
checked="indeterminate"
|
|
73
|
+
onCheckedChange={() => {}}
|
|
74
|
+
aria-label="Accept terms"
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect(checkbox).toHaveAttribute("aria-checked", "mixed");
|
|
80
|
+
expect(checkbox).toHaveAttribute("data-state", "indeterminate");
|
|
81
|
+
});
|
|
82
|
+
});
|