@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,26 @@
|
|
|
1
|
+
import { EmptyState } from "..";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
describe("EmptyState.Title component", () => {
|
|
5
|
+
it("should render a paragraph containing its children", () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
render(<EmptyState.Title>No results found</EmptyState.Title>);
|
|
8
|
+
|
|
9
|
+
// Assert
|
|
10
|
+
const title = screen.getByText("No results found");
|
|
11
|
+
expect(title.tagName).toBe("P");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should render the consumer element with asChild", () => {
|
|
15
|
+
// Arrange
|
|
16
|
+
render(
|
|
17
|
+
<EmptyState.Title asChild>
|
|
18
|
+
<h2>No results found</h2>
|
|
19
|
+
</EmptyState.Title>,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Assert
|
|
23
|
+
const title = screen.getByRole("heading", { name: "No results found" });
|
|
24
|
+
expect(title.tagName).toBe("H2");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ComponentProps } from "react";
|
|
2
|
+
|
|
3
|
+
type WithAsChild = {
|
|
4
|
+
/** Render the consumer's own element instead of the default, via `Slot`. */
|
|
5
|
+
asChild?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/** Props for {@link EmptyState.Root} — all `<div>` props plus `asChild`. */
|
|
9
|
+
export type EmptyStateRootProps = ComponentProps<"div"> & WithAsChild;
|
|
10
|
+
|
|
11
|
+
/** Props for {@link EmptyState.Media} — all `<div>` props plus `asChild`. */
|
|
12
|
+
export type EmptyStateMediaProps = ComponentProps<"div"> & WithAsChild;
|
|
13
|
+
|
|
14
|
+
/** Props for {@link EmptyState.Title} — all `<p>` props plus `asChild`. */
|
|
15
|
+
export type EmptyStateTitleProps = ComponentProps<"p"> & WithAsChild;
|
|
16
|
+
|
|
17
|
+
/** Props for {@link EmptyState.Description} — all `<p>` props plus `asChild`. */
|
|
18
|
+
export type EmptyStateDescriptionProps = ComponentProps<"p"> & WithAsChild;
|
|
19
|
+
|
|
20
|
+
/** Props for {@link EmptyState.Actions} — all `<div>` props plus `asChild`. */
|
|
21
|
+
export type EmptyStateActionsProps = ComponentProps<"div"> & WithAsChild;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useId, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import { Slot } from "../Slot";
|
|
4
|
+
import { FieldContext } from "./FieldContext";
|
|
5
|
+
import { useFieldContext } from "./hooks";
|
|
6
|
+
import {
|
|
7
|
+
FieldDescriptionProps,
|
|
8
|
+
FieldErrorTextProps,
|
|
9
|
+
FieldLabelProps,
|
|
10
|
+
FieldRootProps,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The root of a Field — provides {@link FieldContext} (stable id plus
|
|
15
|
+
* derived `descriptionId` / `errorId`, and the cascaded `invalid` /
|
|
16
|
+
* `disabled` / `required` flags) and renders a `<div data-field>`
|
|
17
|
+
* wrapper.
|
|
18
|
+
*
|
|
19
|
+
* **ID propagation.** If `id` is passed, it's used directly; otherwise
|
|
20
|
+
* a stable id is auto-generated via React's `useId`. The
|
|
21
|
+
* `descriptionId` and `errorId` are derived from the field id
|
|
22
|
+
* (`<id>-description` / `<id>-error`) and are exposed via context to
|
|
23
|
+
* any sub-component or context-aware control (e.g. `Input`).
|
|
24
|
+
*
|
|
25
|
+
* **State cascade.** `invalid`, `disabled`, and `required` cascade via
|
|
26
|
+
* context to any control reading {@link FieldContext} — `Input`
|
|
27
|
+
* inherits them automatically when nested inside a `<Field.Root>` and
|
|
28
|
+
* the consumer hasn't passed an explicit override. The wrapper also
|
|
29
|
+
* carries `data-field-invalid` / `data-field-disabled` /
|
|
30
|
+
* `data-field-required` attributes when the corresponding flag is
|
|
31
|
+
* truthy, so CSS can style the whole field group on a single selector.
|
|
32
|
+
*
|
|
33
|
+
* **`asChild` composition.** Pass `asChild` to render the consumer's
|
|
34
|
+
* element instead of `<div>` — e.g. a semantic `<fieldset>` or
|
|
35
|
+
* `<section>`. The `data-field-*` hooks and context provider stay
|
|
36
|
+
* intact.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Field.Root id="email" invalid={!!errors.email}>
|
|
41
|
+
* <Field.Label>Email</Field.Label>
|
|
42
|
+
* <Input type="email" {...register("email")} />
|
|
43
|
+
* <Field.Description>We won't share it.</Field.Description>
|
|
44
|
+
* <Field.ErrorText>{errors.email?.message}</Field.ErrorText>
|
|
45
|
+
* </Field.Root>
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
function FieldRoot({
|
|
49
|
+
id: idProp,
|
|
50
|
+
invalid = false,
|
|
51
|
+
disabled = false,
|
|
52
|
+
required = false,
|
|
53
|
+
asChild = false,
|
|
54
|
+
children,
|
|
55
|
+
...rest
|
|
56
|
+
}: FieldRootProps) {
|
|
57
|
+
const autoId = useId();
|
|
58
|
+
const id = idProp ?? autoId;
|
|
59
|
+
const descriptionId = `${id}-description`;
|
|
60
|
+
const errorId = `${id}-error`;
|
|
61
|
+
|
|
62
|
+
const value = useMemo(
|
|
63
|
+
() => ({ id, descriptionId, errorId, invalid, disabled, required }),
|
|
64
|
+
[id, descriptionId, errorId, invalid, disabled, required],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const rootProps = {
|
|
68
|
+
...rest,
|
|
69
|
+
"data-field": "",
|
|
70
|
+
"data-field-invalid": invalid ? "" : undefined,
|
|
71
|
+
"data-field-disabled": disabled ? "" : undefined,
|
|
72
|
+
"data-field-required": required ? "" : undefined,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<FieldContext.Provider value={value}>
|
|
77
|
+
{asChild ? (
|
|
78
|
+
<Slot {...rootProps}>{children}</Slot>
|
|
79
|
+
) : (
|
|
80
|
+
<div {...rootProps}>{children}</div>
|
|
81
|
+
)}
|
|
82
|
+
</FieldContext.Provider>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
FieldRoot.displayName = "FieldRoot";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Renders a `<label>` wired to the field's id via `htmlFor` — clicking
|
|
90
|
+
* the label focuses the associated control.
|
|
91
|
+
*
|
|
92
|
+
* **`asChild` composition.** Pass `asChild` to render any consumer
|
|
93
|
+
* element with the `htmlFor` attribute merged on.
|
|
94
|
+
*
|
|
95
|
+
* @throws If rendered outside a `<Field.Root>`.
|
|
96
|
+
*/
|
|
97
|
+
function FieldLabel({ asChild = false, children, ...rest }: FieldLabelProps) {
|
|
98
|
+
const { id } = useFieldContext();
|
|
99
|
+
const labelProps = { ...rest, htmlFor: id };
|
|
100
|
+
|
|
101
|
+
if (asChild) {
|
|
102
|
+
return <Slot {...labelProps}>{children}</Slot>;
|
|
103
|
+
}
|
|
104
|
+
return <label {...labelProps}>{children}</label>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
FieldLabel.displayName = "FieldLabel";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Renders a `<div>` carrying the field's `descriptionId`. Reference it
|
|
111
|
+
* from your control via `aria-describedby` for assistive technology to
|
|
112
|
+
* announce it alongside the label — `Input` does this automatically
|
|
113
|
+
* when nested inside a `<Field.Root>`.
|
|
114
|
+
*
|
|
115
|
+
* **`asChild` composition.** Pass `asChild` to render a `<p>`,
|
|
116
|
+
* `<span>`, or any other element with the `id` merged on.
|
|
117
|
+
*
|
|
118
|
+
* @throws If rendered outside a `<Field.Root>`.
|
|
119
|
+
*/
|
|
120
|
+
function FieldDescription({
|
|
121
|
+
asChild = false,
|
|
122
|
+
children,
|
|
123
|
+
...rest
|
|
124
|
+
}: FieldDescriptionProps) {
|
|
125
|
+
const { descriptionId } = useFieldContext();
|
|
126
|
+
const descriptionProps = { ...rest, id: descriptionId };
|
|
127
|
+
|
|
128
|
+
if (asChild) {
|
|
129
|
+
return <Slot {...descriptionProps}>{children}</Slot>;
|
|
130
|
+
}
|
|
131
|
+
return <div {...descriptionProps}>{children}</div>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
FieldDescription.displayName = "FieldDescription";
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Renders a `<div role="alert">` carrying the field's `errorId` — only
|
|
138
|
+
* when the field is in an invalid state. Returns `null` otherwise, so
|
|
139
|
+
* consumers can render it unconditionally and rely on the field to gate
|
|
140
|
+
* visibility.
|
|
141
|
+
*
|
|
142
|
+
* **`asChild` composition.** Pass `asChild` to render the consumer's
|
|
143
|
+
* element with the `id` and `role="alert"` merged on.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```tsx
|
|
147
|
+
* <Field.Root invalid={!!errors.email}>
|
|
148
|
+
* <Field.ErrorText>{errors.email?.message ?? "Required"}</Field.ErrorText>
|
|
149
|
+
* </Field.Root>
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @throws If rendered outside a `<Field.Root>`.
|
|
153
|
+
*/
|
|
154
|
+
function FieldErrorText({
|
|
155
|
+
asChild = false,
|
|
156
|
+
children,
|
|
157
|
+
...rest
|
|
158
|
+
}: FieldErrorTextProps) {
|
|
159
|
+
const { errorId, invalid } = useFieldContext();
|
|
160
|
+
if (!invalid) return null;
|
|
161
|
+
const errorProps = { ...rest, id: errorId, role: "alert" as const };
|
|
162
|
+
|
|
163
|
+
if (asChild) {
|
|
164
|
+
return <Slot {...errorProps}>{children}</Slot>;
|
|
165
|
+
}
|
|
166
|
+
return <div {...errorProps}>{children}</div>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
FieldErrorText.displayName = "FieldErrorText";
|
|
170
|
+
|
|
171
|
+
type TFieldCompound = typeof FieldRoot & {
|
|
172
|
+
Root: typeof FieldRoot;
|
|
173
|
+
Label: typeof FieldLabel;
|
|
174
|
+
Description: typeof FieldDescription;
|
|
175
|
+
ErrorText: typeof FieldErrorText;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Headless, accessible **Field** — a coordinator compound that owns the
|
|
180
|
+
* `id`, `aria-describedby`, and `invalid` / `disabled` / `required`
|
|
181
|
+
* wiring for a single form control plus its label, description, and
|
|
182
|
+
* error message. Zero styles ship.
|
|
183
|
+
*
|
|
184
|
+
* `Field` does not render the control itself — it sits *around* an
|
|
185
|
+
* existing control (`Input`, `InputGroup` wrapping an `Input`, a future
|
|
186
|
+
* `Textarea`, etc.). The control opts into {@link FieldContext} to
|
|
187
|
+
* inherit the wiring; the sub-components below render the
|
|
188
|
+
* label / description / error UI.
|
|
189
|
+
*
|
|
190
|
+
* `Field` is callable (alias of `Field.Root`) and carries its
|
|
191
|
+
* sub-components as static properties:
|
|
192
|
+
*
|
|
193
|
+
* - {@link FieldRoot | `Field.Root`} — provides context, renders the
|
|
194
|
+
* `<div data-field>` wrapper (or any element via `asChild`).
|
|
195
|
+
* - {@link FieldLabel | `Field.Label`} — `<label htmlFor>` wired to the
|
|
196
|
+
* field id.
|
|
197
|
+
* - {@link FieldDescription | `Field.Description`} — `<div id>` for
|
|
198
|
+
* help text, referenced via `aria-describedby`.
|
|
199
|
+
* - {@link FieldErrorText | `Field.ErrorText`} — `<div role="alert">`
|
|
200
|
+
* rendered only when invalid.
|
|
201
|
+
*
|
|
202
|
+
* Every part supports `asChild` for the consumer to swap in their own
|
|
203
|
+
* element while keeping the wiring.
|
|
204
|
+
*
|
|
205
|
+
* @example Basic
|
|
206
|
+
* ```tsx
|
|
207
|
+
* import { Field, Input } from "@primitiv-ui/react";
|
|
208
|
+
*
|
|
209
|
+
* <Field.Root>
|
|
210
|
+
* <Field.Label>Email</Field.Label>
|
|
211
|
+
* <Input type="email" required />
|
|
212
|
+
* <Field.Description>We won't share it.</Field.Description>
|
|
213
|
+
* </Field.Root>
|
|
214
|
+
* ```
|
|
215
|
+
*
|
|
216
|
+
* @example With error and InputGroup adornments
|
|
217
|
+
* ```tsx
|
|
218
|
+
* <Field.Root id="email" invalid={!!errors.email}>
|
|
219
|
+
* <Field.Label>Email</Field.Label>
|
|
220
|
+
* <InputGroup>
|
|
221
|
+
* <InputGroup.LeadingAdornment><MailIcon aria-hidden="true" /></InputGroup.LeadingAdornment>
|
|
222
|
+
* <Input type="email" {...register("email")} />
|
|
223
|
+
* </InputGroup>
|
|
224
|
+
* <Field.ErrorText>{errors.email?.message}</Field.ErrorText>
|
|
225
|
+
* </Field.Root>
|
|
226
|
+
* ```
|
|
227
|
+
*
|
|
228
|
+
* @see {@link FieldContext} — the context shape that controls can opt into.
|
|
229
|
+
*/
|
|
230
|
+
const FieldCompound: TFieldCompound = Object.assign(FieldRoot, {
|
|
231
|
+
Root: FieldRoot,
|
|
232
|
+
Label: FieldLabel,
|
|
233
|
+
Description: FieldDescription,
|
|
234
|
+
ErrorText: FieldErrorText,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
FieldCompound.displayName = "Field";
|
|
238
|
+
|
|
239
|
+
export { FieldCompound as Field };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createStrictContext } from "../utils";
|
|
2
|
+
|
|
3
|
+
export type FieldContextValue = {
|
|
4
|
+
/** Stable id for the control wired to this field. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Id of the {@link Field.Description} element, when rendered. */
|
|
7
|
+
descriptionId: string;
|
|
8
|
+
/** Id of the {@link Field.ErrorText} element, when rendered. */
|
|
9
|
+
errorId: string;
|
|
10
|
+
/** Whether the field is in an invalid state. */
|
|
11
|
+
invalid: boolean;
|
|
12
|
+
/** Whether the field is disabled. */
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
/** Whether the field is required. */
|
|
15
|
+
required: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const [FieldContext, useFieldContext] =
|
|
19
|
+
createStrictContext<FieldContextValue>(
|
|
20
|
+
"Field sub-components must be rendered inside a <Field.Root>.",
|
|
21
|
+
"FieldContext",
|
|
22
|
+
);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Field
|
|
2
|
+
|
|
3
|
+
A headless, accessible coordinator that owns the `id`,
|
|
4
|
+
`aria-describedby`, and `invalid` / `disabled` / `required` wiring for
|
|
5
|
+
a single form control plus its label, description, and error message.
|
|
6
|
+
Zero styles ship.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
import { Field, Input } from "@primitiv-ui/react";
|
|
10
|
+
|
|
11
|
+
<Field.Root>
|
|
12
|
+
<Field.Label>Email</Field.Label>
|
|
13
|
+
<Input type="email" required />
|
|
14
|
+
<Field.Description>We won't share it.</Field.Description>
|
|
15
|
+
</Field.Root>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Anatomy
|
|
19
|
+
|
|
20
|
+
| Part | Element | Styling hook |
|
|
21
|
+
| --------------------------- | ------------------------ | ------------------------------------- |
|
|
22
|
+
| `Field` / `Field.Root` | `<div>` | `data-field=""` |
|
|
23
|
+
| `Field.Label` | `<label>` (`htmlFor=id`) | n/a |
|
|
24
|
+
| `Field.Description` | `<div id=…-description>` | n/a |
|
|
25
|
+
| `Field.ErrorText` | `<div role="alert">` | n/a (only renders when invalid) |
|
|
26
|
+
|
|
27
|
+
Plus three state-driven hooks on the root: `data-field-invalid`,
|
|
28
|
+
`data-field-disabled`, `data-field-required`.
|
|
29
|
+
|
|
30
|
+
`Field` does **not** render the control itself — it sits *around* an
|
|
31
|
+
existing control. `Input` reads `FieldContext` automatically and
|
|
32
|
+
inherits the wiring; outside a `<Field.Root>`, `Input` behaves exactly
|
|
33
|
+
as before.
|
|
34
|
+
|
|
35
|
+
## State cascade
|
|
36
|
+
|
|
37
|
+
Three props on `Field.Root` propagate via context to the control:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<Field.Root invalid disabled required>
|
|
41
|
+
<Field.Label>Email</Field.Label>
|
|
42
|
+
<Input type="email" />
|
|
43
|
+
{/* ^ inherits aria-invalid="true", disabled, required, plus
|
|
44
|
+
aria-describedby pointing at the error / description ids */}
|
|
45
|
+
</Field.Root>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Consumer-supplied props on the control always win — pass
|
|
49
|
+
`disabled={false}` on `Input` and it stays enabled regardless of
|
|
50
|
+
`Field.Root`'s `disabled`.
|
|
51
|
+
|
|
52
|
+
## ID and aria-describedby wiring
|
|
53
|
+
|
|
54
|
+
`Field.Root` either accepts an explicit `id` or auto-generates one via
|
|
55
|
+
`useId`. The derived ids:
|
|
56
|
+
|
|
57
|
+
| Reference | Form |
|
|
58
|
+
| -------------------- | ------------------- |
|
|
59
|
+
| `field.id` | `<id>` |
|
|
60
|
+
| `field.descriptionId`| `<id>-description` |
|
|
61
|
+
| `field.errorId` | `<id>-error` |
|
|
62
|
+
|
|
63
|
+
When `Input` is rendered inside `Field.Root`:
|
|
64
|
+
|
|
65
|
+
- `id` defaults to `field.id`
|
|
66
|
+
- `aria-describedby` composes consumer-supplied ids first, then the
|
|
67
|
+
field's `descriptionId`, then the `errorId` (only when invalid)
|
|
68
|
+
- `aria-invalid` is set to `"true"` when `field.invalid` is true
|
|
69
|
+
|
|
70
|
+
If you don't render `Field.Description` or `Field.ErrorText`, the
|
|
71
|
+
referenced ids point at nothing — screen readers handle missing ids
|
|
72
|
+
gracefully, but it's worth knowing.
|
|
73
|
+
|
|
74
|
+
## Validation flow
|
|
75
|
+
|
|
76
|
+
`Field.ErrorText` returns `null` unless `Field.Root` is `invalid`, so
|
|
77
|
+
you can render it unconditionally:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<Field.Root invalid={!!errors.email}>
|
|
81
|
+
<Field.Label>Email</Field.Label>
|
|
82
|
+
<Input type="email" required {...register("email")} />
|
|
83
|
+
<Field.ErrorText>{errors.email?.message ?? "Required"}</Field.ErrorText>
|
|
84
|
+
</Field.Root>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## With react-hook-form
|
|
88
|
+
|
|
89
|
+
`Field` doesn't know about RHF — it just provides the wiring; you tell
|
|
90
|
+
it whether the field is invalid by passing `invalid` to `Field.Root`,
|
|
91
|
+
and spread `register` onto the `Input`:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { useForm } from "react-hook-form";
|
|
95
|
+
|
|
96
|
+
const { register, formState: { errors } } = useForm();
|
|
97
|
+
|
|
98
|
+
<Field.Root invalid={!!errors.email}>
|
|
99
|
+
<Field.Label>Email</Field.Label>
|
|
100
|
+
<Input type="email" required {...register("email")} />
|
|
101
|
+
<Field.Description>We won't spam you.</Field.Description>
|
|
102
|
+
<Field.ErrorText>{errors.email?.message}</Field.ErrorText>
|
|
103
|
+
</Field.Root>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## With InputGroup
|
|
107
|
+
|
|
108
|
+
`Field` and `InputGroup` compose freely — `Field` wraps the entire
|
|
109
|
+
field group; `InputGroup` wraps just the control plus its adornments.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<Field.Root invalid={!!errors.email}>
|
|
113
|
+
<Field.Label>Email</Field.Label>
|
|
114
|
+
<InputGroup>
|
|
115
|
+
<InputGroup.LeadingAdornment>
|
|
116
|
+
<MailIcon aria-hidden="true" />
|
|
117
|
+
</InputGroup.LeadingAdornment>
|
|
118
|
+
<Input type="email" {...register("email")} />
|
|
119
|
+
</InputGroup>
|
|
120
|
+
<Field.ErrorText>{errors.email?.message}</Field.ErrorText>
|
|
121
|
+
</Field.Root>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`Input` still reads `FieldContext` even when wrapped in `InputGroup` —
|
|
125
|
+
the context flows through any DOM nesting.
|
|
126
|
+
|
|
127
|
+
## Sub-components throw when used outside a Root
|
|
128
|
+
|
|
129
|
+
`Field.Label`, `Field.Description`, and `Field.ErrorText` throw a
|
|
130
|
+
clear error if rendered outside a `<Field.Root>`. The strict-context
|
|
131
|
+
hook ensures the typo / misuse fails loudly rather than rendering
|
|
132
|
+
silently broken markup.
|
|
133
|
+
|
|
134
|
+
## `asChild` composition
|
|
135
|
+
|
|
136
|
+
Every part supports `asChild` for swapping the default element while
|
|
137
|
+
preserving the wiring:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// Render the field as a semantic <fieldset>
|
|
141
|
+
<Field.Root asChild>
|
|
142
|
+
<fieldset>
|
|
143
|
+
<Field.Label>Email</Field.Label>
|
|
144
|
+
<Input type="email" />
|
|
145
|
+
</fieldset>
|
|
146
|
+
</Field.Root>
|
|
147
|
+
|
|
148
|
+
// Render the description as a <p> instead of a <div>
|
|
149
|
+
<Field.Description asChild>
|
|
150
|
+
<p className="hint">We won't share it.</p>
|
|
151
|
+
</Field.Description>
|
|
152
|
+
|
|
153
|
+
// Render the error as a <p>
|
|
154
|
+
<Field.ErrorText asChild>
|
|
155
|
+
<p className="error">{errors.email?.message}</p>
|
|
156
|
+
</Field.ErrorText>
|
|
157
|
+
|
|
158
|
+
// Render the label on a custom heading slot
|
|
159
|
+
<Field.Label asChild>
|
|
160
|
+
<span className="label-text">Email</span>
|
|
161
|
+
</Field.Label>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Merge rules follow the standard Slot contract: event handlers compose
|
|
165
|
+
(child runs first), `style` is shallow-merged, `className` is
|
|
166
|
+
concatenated, and the wired attributes (`htmlFor`, `id`, `role`,
|
|
167
|
+
`data-field*`) are always merged onto the consumer element.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Field } from "../Field";
|
|
4
|
+
|
|
5
|
+
describe("Field asChild composition", () => {
|
|
6
|
+
it("Root asChild renders the consumer element with data-field merged on", () => {
|
|
7
|
+
// Arrange & Act
|
|
8
|
+
render(
|
|
9
|
+
<Field.Root asChild data-testid="field">
|
|
10
|
+
<fieldset />
|
|
11
|
+
</Field.Root>,
|
|
12
|
+
);
|
|
13
|
+
const root = screen.getByTestId("field");
|
|
14
|
+
|
|
15
|
+
// Assert
|
|
16
|
+
expect(root.tagName).toBe("FIELDSET");
|
|
17
|
+
expect(root).toHaveAttribute("data-field", "");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("Root asChild keeps data-field-invalid when invalid", () => {
|
|
21
|
+
// Arrange & Act
|
|
22
|
+
render(
|
|
23
|
+
<Field.Root asChild data-testid="field" invalid>
|
|
24
|
+
<section />
|
|
25
|
+
</Field.Root>,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(screen.getByTestId("field")).toHaveAttribute(
|
|
30
|
+
"data-field-invalid",
|
|
31
|
+
"",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("Label asChild renders the consumer element with htmlFor merged on", () => {
|
|
36
|
+
// Arrange & Act
|
|
37
|
+
render(
|
|
38
|
+
<Field.Root id="email">
|
|
39
|
+
<Field.Label asChild>
|
|
40
|
+
<span data-testid="label">Email</span>
|
|
41
|
+
</Field.Label>
|
|
42
|
+
</Field.Root>,
|
|
43
|
+
);
|
|
44
|
+
const label = screen.getByTestId("label");
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect(label.tagName).toBe("SPAN");
|
|
48
|
+
expect(label).toHaveAttribute("for", "email");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Description asChild renders the consumer element with id merged on", () => {
|
|
52
|
+
// Arrange & Act
|
|
53
|
+
render(
|
|
54
|
+
<Field.Root id="email">
|
|
55
|
+
<Field.Description asChild>
|
|
56
|
+
<p data-testid="hint">We won't share it.</p>
|
|
57
|
+
</Field.Description>
|
|
58
|
+
</Field.Root>,
|
|
59
|
+
);
|
|
60
|
+
const description = screen.getByTestId("hint");
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(description.tagName).toBe("P");
|
|
64
|
+
expect(description).toHaveAttribute("id", "email-description");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("ErrorText asChild renders the consumer element with id and role merged on", () => {
|
|
68
|
+
// Arrange & Act
|
|
69
|
+
render(
|
|
70
|
+
<Field.Root id="email" invalid>
|
|
71
|
+
<Field.ErrorText asChild>
|
|
72
|
+
<p data-testid="error">Required.</p>
|
|
73
|
+
</Field.ErrorText>
|
|
74
|
+
</Field.Root>,
|
|
75
|
+
);
|
|
76
|
+
const error = screen.getByTestId("error");
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect(error.tagName).toBe("P");
|
|
80
|
+
expect(error).toHaveAttribute("id", "email-error");
|
|
81
|
+
expect(error).toHaveAttribute("role", "alert");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Field } from "../Field";
|
|
4
|
+
|
|
5
|
+
describe("Field basic rendering", () => {
|
|
6
|
+
it("renders a wrapper element with data-field on the root", () => {
|
|
7
|
+
// Arrange & Act
|
|
8
|
+
render(<Field.Root data-testid="field" />);
|
|
9
|
+
|
|
10
|
+
// Assert
|
|
11
|
+
const root = screen.getByTestId("field");
|
|
12
|
+
expect(root.tagName).toBe("DIV");
|
|
13
|
+
expect(root).toHaveAttribute("data-field", "");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("Field is callable as an alias of Field.Root", () => {
|
|
17
|
+
// Arrange & Act
|
|
18
|
+
render(<Field data-testid="field" />);
|
|
19
|
+
|
|
20
|
+
// Assert
|
|
21
|
+
expect(screen.getByTestId("field").tagName).toBe("DIV");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("Field.Label renders a <label> wired to an auto-generated id", () => {
|
|
25
|
+
// Arrange & Act
|
|
26
|
+
render(
|
|
27
|
+
<Field.Root>
|
|
28
|
+
<Field.Label>Email</Field.Label>
|
|
29
|
+
</Field.Root>,
|
|
30
|
+
);
|
|
31
|
+
const label = screen.getByText("Email");
|
|
32
|
+
|
|
33
|
+
// Assert
|
|
34
|
+
expect(label.tagName).toBe("LABEL");
|
|
35
|
+
expect(label).toHaveAttribute("for");
|
|
36
|
+
expect(label.getAttribute("for")).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("Field.Label uses a consumer-supplied id on Root", () => {
|
|
40
|
+
// Arrange & Act
|
|
41
|
+
render(
|
|
42
|
+
<Field.Root id="email">
|
|
43
|
+
<Field.Label>Email</Field.Label>
|
|
44
|
+
</Field.Root>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(screen.getByText("Email")).toHaveAttribute("for", "email");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Field.Description renders a <div> with id derived from the field id", () => {
|
|
52
|
+
// Arrange & Act
|
|
53
|
+
render(
|
|
54
|
+
<Field.Root id="email">
|
|
55
|
+
<Field.Description>We won't share it.</Field.Description>
|
|
56
|
+
</Field.Root>,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
expect(screen.getByText("We won't share it.")).toHaveAttribute(
|
|
61
|
+
"id",
|
|
62
|
+
"email-description",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("Field.ErrorText renders a <div role='alert'> with derived id when invalid", () => {
|
|
67
|
+
// Arrange & Act
|
|
68
|
+
render(
|
|
69
|
+
<Field.Root id="email" invalid>
|
|
70
|
+
<Field.ErrorText>Required.</Field.ErrorText>
|
|
71
|
+
</Field.Root>,
|
|
72
|
+
);
|
|
73
|
+
const error = screen.getByText("Required.");
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
expect(error.tagName).toBe("DIV");
|
|
77
|
+
expect(error).toHaveAttribute("role", "alert");
|
|
78
|
+
expect(error).toHaveAttribute("id", "email-error");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("renders children inside the wrapper in source order", () => {
|
|
82
|
+
// Arrange & Act
|
|
83
|
+
render(
|
|
84
|
+
<Field.Root id="email">
|
|
85
|
+
<Field.Label>Email</Field.Label>
|
|
86
|
+
<input data-testid="input" />
|
|
87
|
+
<Field.Description>Hint.</Field.Description>
|
|
88
|
+
</Field.Root>,
|
|
89
|
+
);
|
|
90
|
+
const wrapper = screen.getByText("Email").parentElement;
|
|
91
|
+
|
|
92
|
+
// Assert
|
|
93
|
+
expect(wrapper).toContainElement(screen.getByTestId("input"));
|
|
94
|
+
expect(wrapper).toContainElement(screen.getByText("Hint."));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("passes className and data-* through on the Root", () => {
|
|
98
|
+
// Arrange & Act
|
|
99
|
+
render(<Field.Root className="form-field" data-testid="field" />);
|
|
100
|
+
|
|
101
|
+
// Assert
|
|
102
|
+
expect(screen.getByTestId("field")).toHaveClass("form-field");
|
|
103
|
+
});
|
|
104
|
+
});
|