@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,133 @@
|
|
|
1
|
+
# RadioCard
|
|
2
|
+
|
|
3
|
+
Headless, accessible **RadioCard** — a card/tile-shaped radio group variant
|
|
4
|
+
implementing the
|
|
5
|
+
[WAI-ARIA Radio Group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/).
|
|
6
|
+
The entire card surface is the interactive element. Zero styles ship.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { RadioCard } from "@primitiv-ui/react";
|
|
10
|
+
|
|
11
|
+
<RadioCard.Root defaultValue="pro" aria-label="Plan">
|
|
12
|
+
<RadioCard.Item value="starter">
|
|
13
|
+
<RadioCard.Indicator />
|
|
14
|
+
<h3>Starter</h3>
|
|
15
|
+
<p>Free forever</p>
|
|
16
|
+
</RadioCard.Item>
|
|
17
|
+
<RadioCard.Item value="pro">
|
|
18
|
+
<RadioCard.Indicator />
|
|
19
|
+
<h3>Pro</h3>
|
|
20
|
+
<p>$9/month</p>
|
|
21
|
+
</RadioCard.Item>
|
|
22
|
+
</RadioCard.Root>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Sub-components
|
|
26
|
+
|
|
27
|
+
| Export | Element | ARIA / data hooks | `asChild` |
|
|
28
|
+
|--------|---------|------------------|-----------|
|
|
29
|
+
| `RadioCard.Root` | `<div>` | `role="radiogroup"` | yes |
|
|
30
|
+
| `RadioCard.Item` | `<button>` | `role="radio"`, `aria-checked`, `data-state`, `tabIndex` | yes |
|
|
31
|
+
| `RadioCard.Indicator` | `<span>` | `aria-hidden="true"`, `data-state` | yes |
|
|
32
|
+
|
|
33
|
+
## State modes
|
|
34
|
+
|
|
35
|
+
### Uncontrolled
|
|
36
|
+
|
|
37
|
+
Pass `defaultValue` (or omit for nothing selected on mount). The component owns
|
|
38
|
+
the value internally.
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<RadioCard.Root defaultValue="pro" aria-label="Plan">
|
|
42
|
+
<RadioCard.Item value="starter">Starter</RadioCard.Item>
|
|
43
|
+
<RadioCard.Item value="pro">Pro</RadioCard.Item>
|
|
44
|
+
</RadioCard.Root>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Controlled
|
|
48
|
+
|
|
49
|
+
Pass `value` and `onValueChange` together. The parent owns the value.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
const [plan, setPlan] = useState("pro");
|
|
53
|
+
|
|
54
|
+
<RadioCard.Root value={plan} onValueChange={setPlan} aria-label="Plan">
|
|
55
|
+
<RadioCard.Item value="starter">Starter</RadioCard.Item>
|
|
56
|
+
<RadioCard.Item value="pro">Pro</RadioCard.Item>
|
|
57
|
+
</RadioCard.Root>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Keyboard interaction
|
|
61
|
+
|
|
62
|
+
| Key | Behaviour |
|
|
63
|
+
|-----|-----------|
|
|
64
|
+
| `ArrowDown` / `ArrowRight` | Select and focus the next enabled Item, wrapping at the end |
|
|
65
|
+
| `ArrowUp` / `ArrowLeft` | Select and focus the previous enabled Item, wrapping at the start |
|
|
66
|
+
| `Space` / `Enter` | Select the focused Item (native `<button>` behaviour) |
|
|
67
|
+
| `Tab` | Move focus out of the group (only one Item is in the tab sequence at a time) |
|
|
68
|
+
|
|
69
|
+
## Orientation
|
|
70
|
+
|
|
71
|
+
By default (`orientation="both"`) all four arrow keys navigate the group.
|
|
72
|
+
Pass `orientation` to restrict navigation to a single axis:
|
|
73
|
+
|
|
74
|
+
- `orientation="horizontal"` — only `ArrowLeft` / `ArrowRight` navigate.
|
|
75
|
+
- `orientation="vertical"` — only `ArrowUp` / `ArrowDown` navigate.
|
|
76
|
+
|
|
77
|
+
A `horizontal` or `vertical` group also reflects the axis to assistive
|
|
78
|
+
technology via `aria-orientation` on the `radiogroup` element.
|
|
79
|
+
|
|
80
|
+
## Reading direction
|
|
81
|
+
|
|
82
|
+
Pass `dir` (`"ltr"` / `"rtl"`) to swap the horizontal arrow pair, so
|
|
83
|
+
`ArrowLeft` moves forward and `ArrowRight` moves backward in RTL. The
|
|
84
|
+
vertical pair is never swapped. When `dir` is omitted, it is inherited
|
|
85
|
+
from the nearest [`DirectionProvider`](../DirectionProvider/README.md),
|
|
86
|
+
falling back to `"ltr"` when there is no provider. An explicit `dir` prop
|
|
87
|
+
always wins over the inherited value.
|
|
88
|
+
|
|
89
|
+
## Disabled items
|
|
90
|
+
|
|
91
|
+
Pass `disabled` on any `RadioCard.Item`. The native attribute suppresses clicks,
|
|
92
|
+
removes the item from the focus ring, and excludes it from arrow-key navigation
|
|
93
|
+
and the roving-tabindex home base.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<RadioCard.Root aria-label="Plan">
|
|
97
|
+
<RadioCard.Item value="starter" disabled>Starter</RadioCard.Item>
|
|
98
|
+
<RadioCard.Item value="pro">Pro</RadioCard.Item>
|
|
99
|
+
</RadioCard.Root>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## `asChild` composition
|
|
103
|
+
|
|
104
|
+
All three sub-components accept `asChild`. The library's ARIA attributes,
|
|
105
|
+
`data-state`, event handlers, and ref are merged onto the consumer's element;
|
|
106
|
+
the default element is dropped.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
<RadioCard.Root asChild aria-label="Plan">
|
|
110
|
+
<ul>
|
|
111
|
+
<RadioCard.Item value="pro" asChild>
|
|
112
|
+
<li>Pro</li>
|
|
113
|
+
</RadioCard.Item>
|
|
114
|
+
</ul>
|
|
115
|
+
</RadioCard.Root>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Indicator animation hooks
|
|
119
|
+
|
|
120
|
+
`RadioCard.Indicator` mounts only while its Item is selected. Pass `forceMount`
|
|
121
|
+
to keep it in the DOM — `data-state="unchecked"` lets a CSS exit animation play.
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<RadioCard.Indicator forceMount>
|
|
125
|
+
<svg viewBox="0 0 10 10"><circle cx="5" cy="5" r="3" /></svg>
|
|
126
|
+
</RadioCard.Indicator>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Styling hooks
|
|
130
|
+
|
|
131
|
+
| Attribute | Values | Set on |
|
|
132
|
+
|-----------|--------|--------|
|
|
133
|
+
| `data-state` | `"checked"` \| `"unchecked"` | `RadioCard.Item`, `RadioCard.Indicator` |
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { useDirection } from "../DirectionProvider";
|
|
4
|
+
import { useRovingTabindex } from "../hooks";
|
|
5
|
+
import { Slot, composeEventHandlers, composeRefs } from "../Slot";
|
|
6
|
+
|
|
7
|
+
import { RadioCardContext } from "./RadioCardContext";
|
|
8
|
+
import { RadioCardItemContext } from "./RadioCardItemContext";
|
|
9
|
+
import {
|
|
10
|
+
useRadioCardContext,
|
|
11
|
+
useRadioCardItemContext,
|
|
12
|
+
useRadioCardRoot,
|
|
13
|
+
} from "./hooks";
|
|
14
|
+
import {
|
|
15
|
+
RadioCardIndicatorProps,
|
|
16
|
+
RadioCardItemProps,
|
|
17
|
+
RadioCardRootProps,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The root of a RadioCard group — a `<div role="radiogroup">` that owns the
|
|
22
|
+
* selected value and provides {@link RadioCardContext} to descendant
|
|
23
|
+
* {@link RadioCardItem | `RadioCard.Item`}s.
|
|
24
|
+
*
|
|
25
|
+
* Supports two state modes, statically discriminated at the type level:
|
|
26
|
+
*
|
|
27
|
+
* - **Uncontrolled** — pass
|
|
28
|
+
* {@link RadioCardRootProps.defaultValue | `defaultValue`} (or omit
|
|
29
|
+
* for nothing selected on mount). The component owns the value.
|
|
30
|
+
* - **Controlled** — pass
|
|
31
|
+
* {@link RadioCardRootProps.value | `value`} *and*
|
|
32
|
+
* {@link RadioCardRootProps.onValueChange | `onValueChange`}
|
|
33
|
+
* together. The parent owns the value; the component defers every
|
|
34
|
+
* change back through the callback.
|
|
35
|
+
*
|
|
36
|
+
* **ARIA.** `role="radiogroup"` is set automatically. Provide an
|
|
37
|
+
* accessible name via `aria-label` or `aria-labelledby`.
|
|
38
|
+
*
|
|
39
|
+
* **Orientation.** By default (`orientation="both"`) all four arrow keys
|
|
40
|
+
* navigate. Pass `orientation="horizontal"` or `"vertical"` to restrict
|
|
41
|
+
* navigation to a single axis.
|
|
42
|
+
*
|
|
43
|
+
* **Reading direction.** `dir` (`"ltr"` / `"rtl"`) swaps the horizontal
|
|
44
|
+
* arrow pair so Arrow Left moves forward in RTL. When omitted, it is
|
|
45
|
+
* inherited from the nearest {@link DirectionProvider}, falling back to
|
|
46
|
+
* `"ltr"`.
|
|
47
|
+
*
|
|
48
|
+
* **`asChild` prop.** Pass `asChild` to render any consumer-supplied
|
|
49
|
+
* element with the RadioCard's props merged in. The native `<div>` is dropped.
|
|
50
|
+
*
|
|
51
|
+
* @example Uncontrolled
|
|
52
|
+
* ```tsx
|
|
53
|
+
* <RadioCard.Root defaultValue="pro" aria-label="Plan">
|
|
54
|
+
* <RadioCard.Item value="starter">
|
|
55
|
+
* <h3>Starter</h3>
|
|
56
|
+
* <RadioCard.Indicator />
|
|
57
|
+
* </RadioCard.Item>
|
|
58
|
+
* <RadioCard.Item value="pro">
|
|
59
|
+
* <h3>Pro</h3>
|
|
60
|
+
* <RadioCard.Indicator />
|
|
61
|
+
* </RadioCard.Item>
|
|
62
|
+
* </RadioCard.Root>
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example Controlled
|
|
66
|
+
* ```tsx
|
|
67
|
+
* const [plan, setPlan] = useState("pro");
|
|
68
|
+
*
|
|
69
|
+
* <RadioCard.Root value={plan} onValueChange={setPlan} aria-label="Plan">
|
|
70
|
+
* <RadioCard.Item value="starter">Starter</RadioCard.Item>
|
|
71
|
+
* <RadioCard.Item value="pro">Pro</RadioCard.Item>
|
|
72
|
+
* </RadioCard.Root>
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function RadioCardRoot({
|
|
76
|
+
defaultValue,
|
|
77
|
+
value: controlledValue,
|
|
78
|
+
onValueChange,
|
|
79
|
+
orientation = "both",
|
|
80
|
+
dir,
|
|
81
|
+
asChild = false,
|
|
82
|
+
children,
|
|
83
|
+
...rest
|
|
84
|
+
}: RadioCardRootProps) {
|
|
85
|
+
const resolvedDir = dir ?? useDirection();
|
|
86
|
+
const { value, select, registerItem, itemValues, disabledValues, focusItem } =
|
|
87
|
+
useRadioCardRoot({
|
|
88
|
+
defaultValue,
|
|
89
|
+
value: controlledValue,
|
|
90
|
+
onValueChange,
|
|
91
|
+
});
|
|
92
|
+
const contextValue = useMemo(
|
|
93
|
+
() => ({
|
|
94
|
+
value,
|
|
95
|
+
select,
|
|
96
|
+
registerItem,
|
|
97
|
+
itemValues,
|
|
98
|
+
disabledValues,
|
|
99
|
+
focusItem,
|
|
100
|
+
orientation,
|
|
101
|
+
dir: resolvedDir,
|
|
102
|
+
}),
|
|
103
|
+
[
|
|
104
|
+
value,
|
|
105
|
+
select,
|
|
106
|
+
registerItem,
|
|
107
|
+
itemValues,
|
|
108
|
+
disabledValues,
|
|
109
|
+
focusItem,
|
|
110
|
+
orientation,
|
|
111
|
+
resolvedDir,
|
|
112
|
+
],
|
|
113
|
+
);
|
|
114
|
+
const rootProps = {
|
|
115
|
+
role: "radiogroup" as const,
|
|
116
|
+
"aria-orientation": orientation === "both" ? undefined : orientation,
|
|
117
|
+
dir: resolvedDir,
|
|
118
|
+
...rest,
|
|
119
|
+
};
|
|
120
|
+
return (
|
|
121
|
+
<RadioCardContext.Provider value={contextValue}>
|
|
122
|
+
{asChild ? (
|
|
123
|
+
<Slot {...rootProps}>{children}</Slot>
|
|
124
|
+
) : (
|
|
125
|
+
<div {...rootProps}>{children}</div>
|
|
126
|
+
)}
|
|
127
|
+
</RadioCardContext.Provider>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
RadioCardRoot.displayName = "RadioCardRoot";
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* An individual card option inside a RadioCard group — a native
|
|
135
|
+
* `<button role="radio">` whose entire surface is the interactive area.
|
|
136
|
+
* Participates in the roving tabindex and handles arrow-key navigation.
|
|
137
|
+
*
|
|
138
|
+
* **Selection.** Clicking an Item (or pressing Space / Enter via native
|
|
139
|
+
* `<button>` behaviour) selects it. The arrow keys enabled by the group's
|
|
140
|
+
* `orientation` move focus and selection to the next or previous
|
|
141
|
+
* non-disabled Item, wrapping at the ends.
|
|
142
|
+
*
|
|
143
|
+
* **Roving tabindex.** Only one Item per group is in the document tab
|
|
144
|
+
* sequence at a time: the selected one if any, otherwise the first
|
|
145
|
+
* non-disabled Item.
|
|
146
|
+
*
|
|
147
|
+
* **Disabled.** Passing `disabled` forwards the native attribute and
|
|
148
|
+
* excludes the Item from arrow-key navigation and the roving-tabindex
|
|
149
|
+
* home base.
|
|
150
|
+
*
|
|
151
|
+
* **Styling hook.** `data-state="checked" | "unchecked"` mirrors the
|
|
152
|
+
* selection state for CSS targeting.
|
|
153
|
+
*
|
|
154
|
+
* **`asChild` prop.** Pass `asChild` to render any consumer element with
|
|
155
|
+
* the Item's ARIA, data-state, tabIndex, onClick, onKeyDown, disabled, and
|
|
156
|
+
* ref merged onto it.
|
|
157
|
+
*
|
|
158
|
+
* @throws if rendered outside a `RadioCard.Root`.
|
|
159
|
+
*/
|
|
160
|
+
function RadioCardItem({
|
|
161
|
+
value,
|
|
162
|
+
children,
|
|
163
|
+
onClick,
|
|
164
|
+
onKeyDown,
|
|
165
|
+
disabled,
|
|
166
|
+
asChild = false,
|
|
167
|
+
ref,
|
|
168
|
+
...rest
|
|
169
|
+
}: RadioCardItemProps) {
|
|
170
|
+
const {
|
|
171
|
+
value: selectedValue,
|
|
172
|
+
select,
|
|
173
|
+
registerItem,
|
|
174
|
+
itemValues,
|
|
175
|
+
disabledValues,
|
|
176
|
+
focusItem,
|
|
177
|
+
orientation,
|
|
178
|
+
dir,
|
|
179
|
+
} = useRadioCardContext();
|
|
180
|
+
const isChecked = selectedValue === value;
|
|
181
|
+
const enabledValues = useMemo(
|
|
182
|
+
() => itemValues.filter((v) => !disabledValues.has(v)),
|
|
183
|
+
[itemValues, disabledValues],
|
|
184
|
+
);
|
|
185
|
+
const isTabStop =
|
|
186
|
+
selectedValue !== undefined ? isChecked : enabledValues[0] === value;
|
|
187
|
+
|
|
188
|
+
const localRef = useRef<HTMLButtonElement | null>(null);
|
|
189
|
+
const setRef = useMemo(() => composeRefs(localRef, ref), [ref]);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
registerItem(value, localRef.current, disabled);
|
|
193
|
+
return () => registerItem(value, null);
|
|
194
|
+
}, [value, disabled, registerItem]);
|
|
195
|
+
|
|
196
|
+
const { handleKeyDown } = useRovingTabindex<string>({
|
|
197
|
+
orientation,
|
|
198
|
+
dir,
|
|
199
|
+
navigable: enabledValues,
|
|
200
|
+
currentKey: value,
|
|
201
|
+
onNavigate: (target) => {
|
|
202
|
+
select(target);
|
|
203
|
+
focusItem(target);
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const itemContextValue = useMemo(() => ({ checked: isChecked }), [isChecked]);
|
|
208
|
+
|
|
209
|
+
const itemProps = {
|
|
210
|
+
...rest,
|
|
211
|
+
ref: setRef,
|
|
212
|
+
role: "radio" as const,
|
|
213
|
+
"aria-checked": isChecked,
|
|
214
|
+
"data-state": isChecked ? ("checked" as const) : ("unchecked" as const),
|
|
215
|
+
tabIndex: isTabStop ? 0 : -1,
|
|
216
|
+
disabled,
|
|
217
|
+
onClick: composeEventHandlers(onClick, () => select(value)),
|
|
218
|
+
onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<RadioCardItemContext.Provider value={itemContextValue}>
|
|
223
|
+
{asChild ? (
|
|
224
|
+
<Slot {...itemProps}>{children}</Slot>
|
|
225
|
+
) : (
|
|
226
|
+
<button type="button" {...itemProps}>
|
|
227
|
+
{children}
|
|
228
|
+
</button>
|
|
229
|
+
)}
|
|
230
|
+
</RadioCardItemContext.Provider>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
RadioCardItem.displayName = "RadioCardItem";
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A decorative `<span aria-hidden="true">` that renders its children
|
|
238
|
+
* only while the enclosing {@link RadioCardItem | `RadioCard.Item`}
|
|
239
|
+
* is the selected one. Purely visual — accessible state is conveyed by
|
|
240
|
+
* `aria-checked` on the Item.
|
|
241
|
+
*
|
|
242
|
+
* **Styling hook.** Mirrors the parent Item's
|
|
243
|
+
* `data-state="checked" | "unchecked"` so the same CSS rule can target both.
|
|
244
|
+
*
|
|
245
|
+
* **`asChild` prop.** Pass `asChild` to render the consumer's own element
|
|
246
|
+
* as the indicator itself, with `aria-hidden` and `data-state` merged in.
|
|
247
|
+
*
|
|
248
|
+
* **`forceMount` prop.** Pass `forceMount` to keep the indicator in the DOM
|
|
249
|
+
* while unchecked so a CSS exit animation can play against
|
|
250
|
+
* `data-state="unchecked"`.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```tsx
|
|
254
|
+
* <RadioCard.Item value="pro">
|
|
255
|
+
* <RadioCard.Indicator />
|
|
256
|
+
* Pro
|
|
257
|
+
* </RadioCard.Item>
|
|
258
|
+
* ```
|
|
259
|
+
*
|
|
260
|
+
* @throws if rendered outside a `RadioCard.Item`.
|
|
261
|
+
*/
|
|
262
|
+
function RadioCardIndicator({
|
|
263
|
+
children,
|
|
264
|
+
forceMount,
|
|
265
|
+
asChild = false,
|
|
266
|
+
...rest
|
|
267
|
+
}: RadioCardIndicatorProps) {
|
|
268
|
+
const { checked } = useRadioCardItemContext();
|
|
269
|
+
if (!checked && !forceMount) return null;
|
|
270
|
+
const indicatorProps = {
|
|
271
|
+
...rest,
|
|
272
|
+
"aria-hidden": "true" as const,
|
|
273
|
+
"data-state": checked ? ("checked" as const) : ("unchecked" as const),
|
|
274
|
+
};
|
|
275
|
+
if (asChild) {
|
|
276
|
+
return <Slot {...indicatorProps}>{children}</Slot>;
|
|
277
|
+
}
|
|
278
|
+
return <span {...indicatorProps}>{children}</span>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
RadioCardIndicator.displayName = "RadioCardIndicator";
|
|
282
|
+
|
|
283
|
+
type TRadioCardCompound = typeof RadioCardRoot & {
|
|
284
|
+
Root: typeof RadioCardRoot;
|
|
285
|
+
Item: typeof RadioCardItem;
|
|
286
|
+
Indicator: typeof RadioCardIndicator;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Headless, accessible **RadioCard** — a card/tile-shaped radio group
|
|
291
|
+
* variant implementing the
|
|
292
|
+
* [WAI-ARIA Radio Group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/).
|
|
293
|
+
* The entire card surface is the interactive element. Zero styles ship.
|
|
294
|
+
*
|
|
295
|
+
* `RadioCard` is both callable (an alias of
|
|
296
|
+
* {@link RadioCardRoot | `RadioCard.Root`}) and carries its sub-components
|
|
297
|
+
* as static properties.
|
|
298
|
+
*
|
|
299
|
+
* - {@link RadioCardRoot | `RadioCard.Root`} — state owner, context provider,
|
|
300
|
+
* `<div role="radiogroup">` wrapper.
|
|
301
|
+
* - {@link RadioCardItem | `RadioCard.Item`} — a selectable card participating
|
|
302
|
+
* in the roving tabindex.
|
|
303
|
+
* - {@link RadioCardIndicator | `RadioCard.Indicator`} — decorative indicator,
|
|
304
|
+
* mounted only while the parent Item is selected.
|
|
305
|
+
*
|
|
306
|
+
* @example Minimal usage
|
|
307
|
+
* ```tsx
|
|
308
|
+
* import { RadioCard } from "@primitiv-ui/react";
|
|
309
|
+
*
|
|
310
|
+
* <RadioCard.Root defaultValue="pro" aria-label="Plan">
|
|
311
|
+
* <RadioCard.Item value="starter">
|
|
312
|
+
* <RadioCard.Indicator />
|
|
313
|
+
* Starter
|
|
314
|
+
* </RadioCard.Item>
|
|
315
|
+
* <RadioCard.Item value="pro">
|
|
316
|
+
* <RadioCard.Indicator />
|
|
317
|
+
* Pro
|
|
318
|
+
* </RadioCard.Item>
|
|
319
|
+
* </RadioCard.Root>
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* @see {@link RadioCardRoot} for state modes.
|
|
323
|
+
* @see {@link RadioCardItem} for selection, roving tabindex, and keyboard navigation.
|
|
324
|
+
* @see {@link RadioCardIndicator} for the mount gate and animation hooks.
|
|
325
|
+
*/
|
|
326
|
+
const RadioCardCompound: TRadioCardCompound = Object.assign(RadioCardRoot, {
|
|
327
|
+
Root: RadioCardRoot,
|
|
328
|
+
Item: RadioCardItem,
|
|
329
|
+
Indicator: RadioCardIndicator,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
RadioCardCompound.displayName = "RadioCard";
|
|
333
|
+
|
|
334
|
+
export { RadioCardCompound as RadioCard };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createStrictContext } from "../utils";
|
|
2
|
+
|
|
3
|
+
import { RadioCardOrientation, RadioCardReadingDirection } from "./types";
|
|
4
|
+
|
|
5
|
+
export type RadioCardContextValue = {
|
|
6
|
+
value: string | undefined;
|
|
7
|
+
select: (value: string) => void;
|
|
8
|
+
registerItem: (
|
|
9
|
+
value: string,
|
|
10
|
+
element: HTMLButtonElement | null,
|
|
11
|
+
disabled?: boolean,
|
|
12
|
+
) => void;
|
|
13
|
+
itemValues: string[];
|
|
14
|
+
disabledValues: Set<string>;
|
|
15
|
+
focusItem: (value: string) => void;
|
|
16
|
+
orientation: RadioCardOrientation;
|
|
17
|
+
dir: RadioCardReadingDirection;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const [RadioCardContext, useRadioCardContext] =
|
|
21
|
+
createStrictContext<RadioCardContextValue>(
|
|
22
|
+
"RadioCard sub-components must be rendered inside a <RadioCard.Root>.",
|
|
23
|
+
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createStrictContext } from "../utils";
|
|
2
|
+
|
|
3
|
+
export type RadioCardItemContextValue = {
|
|
4
|
+
checked: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const [RadioCardItemContext, useRadioCardItemContext] =
|
|
8
|
+
createStrictContext<RadioCardItemContextValue>(
|
|
9
|
+
"RadioCard.Indicator must be rendered inside a <RadioCard.Item>.",
|
|
10
|
+
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { RadioCard } from "../RadioCard";
|
|
5
|
+
|
|
6
|
+
describe("RadioCard asChild", () => {
|
|
7
|
+
it("renders the Root as the consumer element when asChild is set", () => {
|
|
8
|
+
// Arrange & Act
|
|
9
|
+
render(
|
|
10
|
+
<RadioCard.Root asChild aria-label="Plan">
|
|
11
|
+
<section>
|
|
12
|
+
<RadioCard.Item value="pro">Pro</RadioCard.Item>
|
|
13
|
+
</section>
|
|
14
|
+
</RadioCard.Root>,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Assert — section rendered, not div; role still present via merged prop
|
|
18
|
+
const group = screen.getByRole("radiogroup", { name: "Plan" });
|
|
19
|
+
expect(group.tagName).toBe("SECTION");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders the Item as the consumer element when asChild is set", () => {
|
|
23
|
+
// Arrange & Act
|
|
24
|
+
render(
|
|
25
|
+
<RadioCard.Root aria-label="Plan">
|
|
26
|
+
<RadioCard.Item value="pro" asChild>
|
|
27
|
+
<div>Pro card</div>
|
|
28
|
+
</RadioCard.Item>
|
|
29
|
+
</RadioCard.Root>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Assert — div rendered, not button; role and aria-checked merged
|
|
33
|
+
const item = screen.getByRole("radio", { name: "Pro card" });
|
|
34
|
+
expect(item.tagName).toBe("DIV");
|
|
35
|
+
expect(item).toHaveAttribute("aria-checked", "false");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("merges onClick onto the asChild Item element", async () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
const user = userEvent.setup();
|
|
41
|
+
const onClick = vi.fn();
|
|
42
|
+
render(
|
|
43
|
+
<RadioCard.Root aria-label="Plan">
|
|
44
|
+
<RadioCard.Item value="pro" asChild onClick={onClick}>
|
|
45
|
+
<div>Pro</div>
|
|
46
|
+
</RadioCard.Item>
|
|
47
|
+
</RadioCard.Root>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Act
|
|
51
|
+
await user.click(screen.getByRole("radio", { name: "Pro" }));
|
|
52
|
+
|
|
53
|
+
// Assert
|
|
54
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("renders the Indicator as the consumer element when asChild is set", () => {
|
|
58
|
+
// Arrange & Act
|
|
59
|
+
render(
|
|
60
|
+
<RadioCard.Root aria-label="Plan" defaultValue="pro">
|
|
61
|
+
<RadioCard.Item value="pro">
|
|
62
|
+
<RadioCard.Indicator asChild>
|
|
63
|
+
<svg data-testid="icon" viewBox="0 0 10 10" />
|
|
64
|
+
</RadioCard.Indicator>
|
|
65
|
+
Pro
|
|
66
|
+
</RadioCard.Item>
|
|
67
|
+
</RadioCard.Root>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Assert — svg rendered, not span; aria-hidden and data-state merged
|
|
71
|
+
const icon = screen.getByTestId("icon");
|
|
72
|
+
expect(icon.tagName).toBe("svg");
|
|
73
|
+
expect(icon).toHaveAttribute("aria-hidden", "true");
|
|
74
|
+
expect(icon).toHaveAttribute("data-state", "checked");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { RadioCard } from "../RadioCard";
|
|
4
|
+
|
|
5
|
+
describe("RadioCard basic rendering", () => {
|
|
6
|
+
it('renders a container with role="radiogroup"', () => {
|
|
7
|
+
// Arrange & Act
|
|
8
|
+
render(
|
|
9
|
+
<RadioCard.Root aria-label="Plan">
|
|
10
|
+
<RadioCard.Item value="starter">Starter</RadioCard.Item>
|
|
11
|
+
</RadioCard.Root>,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Assert
|
|
15
|
+
expect(
|
|
16
|
+
screen.getByRole("radiogroup", { name: "Plan" }),
|
|
17
|
+
).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders each item as a <button role="radio"> with aria-checked="false" by default', () => {
|
|
21
|
+
// Arrange & Act
|
|
22
|
+
render(
|
|
23
|
+
<RadioCard.Root aria-label="Plan">
|
|
24
|
+
<RadioCard.Item value="starter">Starter</RadioCard.Item>
|
|
25
|
+
<RadioCard.Item value="pro">Pro</RadioCard.Item>
|
|
26
|
+
</RadioCard.Root>,
|
|
27
|
+
);
|
|
28
|
+
const starter = screen.getByRole("radio", { name: "Starter" });
|
|
29
|
+
const pro = screen.getByRole("radio", { name: "Pro" });
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
32
|
+
expect(starter.tagName).toBe("BUTTON");
|
|
33
|
+
expect(starter).toHaveAttribute("aria-checked", "false");
|
|
34
|
+
expect(pro.tagName).toBe("BUTTON");
|
|
35
|
+
expect(pro).toHaveAttribute("aria-checked", "false");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('defaults type="button" on items so they never submit an enclosing form', () => {
|
|
39
|
+
// Arrange & Act
|
|
40
|
+
render(
|
|
41
|
+
<RadioCard.Root aria-label="Plan">
|
|
42
|
+
<RadioCard.Item value="starter">Starter</RadioCard.Item>
|
|
43
|
+
</RadioCard.Root>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect(screen.getByRole("radio", { name: "Starter" })).toHaveAttribute(
|
|
48
|
+
"type",
|
|
49
|
+
"button",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('sets data-state="unchecked" on each item when nothing is selected', () => {
|
|
54
|
+
// Arrange & Act
|
|
55
|
+
render(
|
|
56
|
+
<RadioCard.Root aria-label="Plan">
|
|
57
|
+
<RadioCard.Item value="starter">Starter</RadioCard.Item>
|
|
58
|
+
<RadioCard.Item value="pro">Pro</RadioCard.Item>
|
|
59
|
+
</RadioCard.Root>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(screen.getByRole("radio", { name: "Starter" })).toHaveAttribute(
|
|
64
|
+
"data-state",
|
|
65
|
+
"unchecked",
|
|
66
|
+
);
|
|
67
|
+
expect(screen.getByRole("radio", { name: "Pro" })).toHaveAttribute(
|
|
68
|
+
"data-state",
|
|
69
|
+
"unchecked",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("renders children inside the card item", () => {
|
|
74
|
+
// Arrange & Act
|
|
75
|
+
render(
|
|
76
|
+
<RadioCard.Root aria-label="Plan">
|
|
77
|
+
<RadioCard.Item value="pro">
|
|
78
|
+
<span data-testid="icon">icon</span>
|
|
79
|
+
Pro plan
|
|
80
|
+
</RadioCard.Item>
|
|
81
|
+
</RadioCard.Root>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
expect(screen.getByTestId("icon")).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
});
|