@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,67 @@
|
|
|
1
|
+
import { CSSProperties } from "react";
|
|
2
|
+
import { Slot } from "../Slot";
|
|
3
|
+
import { VisuallyHiddenProps } from "./types";
|
|
4
|
+
|
|
5
|
+
const visuallyHiddenStyle: CSSProperties = {
|
|
6
|
+
position: "absolute",
|
|
7
|
+
width: 1,
|
|
8
|
+
height: 1,
|
|
9
|
+
padding: 0,
|
|
10
|
+
margin: -1,
|
|
11
|
+
overflow: "hidden",
|
|
12
|
+
clip: "rect(0 0 0 0)",
|
|
13
|
+
clipPath: "inset(50%)",
|
|
14
|
+
whiteSpace: "nowrap",
|
|
15
|
+
borderWidth: 0,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Visually hides its children while keeping them in the accessibility tree.
|
|
20
|
+
*
|
|
21
|
+
* Renders a `<span>` carrying the canonical screen-reader-only clip styles:
|
|
22
|
+
* the content is removed from the visual layout but still announced by
|
|
23
|
+
* assistive technology. Use it for text that gives a control or region an
|
|
24
|
+
* accessible name without showing on screen.
|
|
25
|
+
*
|
|
26
|
+
* **Functional styles.** Unlike other `@primitiv-ui/react` components, the
|
|
27
|
+
* clip styles are applied inline because they *are* the component's
|
|
28
|
+
* behaviour, not decoration. A consumer `style` is merged on top, so any
|
|
29
|
+
* individual property can still be overridden.
|
|
30
|
+
*
|
|
31
|
+
* **`asChild` composition.** Renders the consumer's element instead of a
|
|
32
|
+
* `<span>`, merging the clip styles in via the {@link Slot} utility.
|
|
33
|
+
*
|
|
34
|
+
* @example Accessible name for an icon-only button
|
|
35
|
+
* ```tsx
|
|
36
|
+
* <button>
|
|
37
|
+
* <SearchIcon aria-hidden="true" />
|
|
38
|
+
* <VisuallyHidden>Search</VisuallyHidden>
|
|
39
|
+
* </button>
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example asChild — keep semantic markup hidden
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <VisuallyHidden asChild>
|
|
45
|
+
* <h2>Section heading</h2>
|
|
46
|
+
* </VisuallyHidden>
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function VisuallyHidden({
|
|
50
|
+
asChild = false,
|
|
51
|
+
children,
|
|
52
|
+
style,
|
|
53
|
+
...rest
|
|
54
|
+
}: VisuallyHiddenProps) {
|
|
55
|
+
const rootProps = {
|
|
56
|
+
...rest,
|
|
57
|
+
style: { ...visuallyHiddenStyle, ...style },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (asChild) {
|
|
61
|
+
return <Slot {...rootProps}>{children}</Slot>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return <span {...rootProps}>{children}</span>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
VisuallyHidden.displayName = "VisuallyHidden";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { VisuallyHidden } from "..";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
describe("VisuallyHidden component", () => {
|
|
5
|
+
it("should render a span containing its children", () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
render(<VisuallyHidden>Loading complete</VisuallyHidden>);
|
|
8
|
+
|
|
9
|
+
// Assert
|
|
10
|
+
const hidden = screen.getByText("Loading complete");
|
|
11
|
+
expect(hidden.tagName).toBe("SPAN");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should apply the screen-reader-only clip styles", () => {
|
|
15
|
+
// Arrange
|
|
16
|
+
render(<VisuallyHidden>Loading complete</VisuallyHidden>);
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
19
|
+
const hidden = screen.getByText("Loading complete");
|
|
20
|
+
expect(hidden).toHaveStyle({
|
|
21
|
+
position: "absolute",
|
|
22
|
+
width: "1px",
|
|
23
|
+
height: "1px",
|
|
24
|
+
overflow: "hidden",
|
|
25
|
+
whiteSpace: "nowrap",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should merge a consumer style over the clip styles", () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
render(
|
|
32
|
+
<VisuallyHidden style={{ position: "static", display: "block" }}>
|
|
33
|
+
Loading complete
|
|
34
|
+
</VisuallyHidden>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
const hidden = screen.getByText("Loading complete");
|
|
39
|
+
expect(hidden).toHaveStyle({
|
|
40
|
+
position: "static",
|
|
41
|
+
display: "block",
|
|
42
|
+
whiteSpace: "nowrap",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should render the consumer element with asChild, keeping the clip styles", () => {
|
|
47
|
+
// Arrange
|
|
48
|
+
render(
|
|
49
|
+
<VisuallyHidden asChild>
|
|
50
|
+
<label>Search</label>
|
|
51
|
+
</VisuallyHidden>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Assert
|
|
55
|
+
const hidden = screen.getByText("Search");
|
|
56
|
+
expect(hidden.tagName).toBe("LABEL");
|
|
57
|
+
expect(hidden).toHaveStyle({ position: "absolute", overflow: "hidden" });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useCallback, useRef, useState, type RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
type UseCollectionOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Whether to update the `keys` state when a registered item is
|
|
6
|
+
* unregistered (the registrar is called with a `null` value). Defaults to
|
|
7
|
+
* `true`.
|
|
8
|
+
*
|
|
9
|
+
* Set to `false` for collections that may unmount after a render-time
|
|
10
|
+
* throw (such as Accordion's trigger validation): in that flow, an effect
|
|
11
|
+
* cleanup running outside React's `act()` and updating state would surface
|
|
12
|
+
* as a "setState outside act" warning and mask the original error. With
|
|
13
|
+
* `false`, the ref is still updated — the validation effect that reads
|
|
14
|
+
* the ref still works — but the keys array stays stable until the next
|
|
15
|
+
* mount.
|
|
16
|
+
*/
|
|
17
|
+
updateKeysOnCleanup?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Manages a registry of mounted child elements (or any per-key value)
|
|
22
|
+
* inside a compound component. Used by Roots that need to know which
|
|
23
|
+
* Triggers / Items currently exist — for roving tabindex, for keyboard
|
|
24
|
+
* navigation, and for validating consumer-supplied `value` against
|
|
25
|
+
* registered keys.
|
|
26
|
+
*
|
|
27
|
+
* Returns a stable `register(key, value | null)` callback (call with
|
|
28
|
+
* `null` to unregister), a `RefObject` exposing the live `Map` (for
|
|
29
|
+
* imperative reads inside event handlers, where you want the latest
|
|
30
|
+
* value without rendering), and a `keys` array tracked as React state
|
|
31
|
+
* (so consumers re-render whenever items mount or unmount).
|
|
32
|
+
*
|
|
33
|
+
* @example Inside a Root hook
|
|
34
|
+
* ```ts
|
|
35
|
+
* const { register, itemsRef, keys } = useCollection<string, HTMLButtonElement>();
|
|
36
|
+
*
|
|
37
|
+
* // Inside a Trigger's effect:
|
|
38
|
+
* useEffect(() => {
|
|
39
|
+
* register(value, ref.current);
|
|
40
|
+
* return () => register(value, null);
|
|
41
|
+
* }, [value, register]);
|
|
42
|
+
*
|
|
43
|
+
* // Imperatively focus the first registered trigger:
|
|
44
|
+
* keys[0] && itemsRef.current.get(keys[0])?.focus();
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function useCollection<K, V>(
|
|
48
|
+
options: UseCollectionOptions = {},
|
|
49
|
+
): {
|
|
50
|
+
register: (key: K, value: V | null) => void;
|
|
51
|
+
itemsRef: RefObject<Map<K, V>>;
|
|
52
|
+
keys: K[];
|
|
53
|
+
} {
|
|
54
|
+
const { updateKeysOnCleanup = true } = options;
|
|
55
|
+
const itemsRef = useRef<Map<K, V>>(new Map());
|
|
56
|
+
const [keys, setKeys] = useState<K[]>([]);
|
|
57
|
+
|
|
58
|
+
const register = useCallback(
|
|
59
|
+
(key: K, value: V | null) => {
|
|
60
|
+
if (value !== null) {
|
|
61
|
+
itemsRef.current.set(key, value);
|
|
62
|
+
setKeys(Array.from(itemsRef.current.keys()));
|
|
63
|
+
} else {
|
|
64
|
+
itemsRef.current.delete(key);
|
|
65
|
+
if (updateKeysOnCleanup) {
|
|
66
|
+
setKeys(Array.from(itemsRef.current.keys()));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[updateKeysOnCleanup],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return { register, itemsRef, keys };
|
|
74
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages the controlled / uncontrolled state pair (`value` +
|
|
5
|
+
* `defaultValue` + `onChange`) that almost every compound component in
|
|
6
|
+
* this library exposes on its Root. Centralises the "is the consumer
|
|
7
|
+
* driving this externally?" branch and the matching `useState`
|
|
8
|
+
* fallback.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour:
|
|
11
|
+
*
|
|
12
|
+
* - When `controlled` is `undefined`, the hook is **uncontrolled**: it
|
|
13
|
+
* holds an internal `useState` initialised from `defaultValue`.
|
|
14
|
+
* Calling the returned setter updates that internal state **and**
|
|
15
|
+
* notifies `onChange`.
|
|
16
|
+
* - When `controlled` is anything other than `undefined`, the hook is
|
|
17
|
+
* **controlled**: the returned `value` is `controlled` directly,
|
|
18
|
+
* internal state is ignored, and the setter only notifies
|
|
19
|
+
* `onChange` (the consumer is expected to flip `controlled` on the
|
|
20
|
+
* next render).
|
|
21
|
+
*
|
|
22
|
+
* The setter does **not** dedupe — callers that need to skip
|
|
23
|
+
* `onChange` when the value hasn't changed should add that check at
|
|
24
|
+
* the call site (e.g. RadioGroup's `select`, Dropdown's `setOpen`).
|
|
25
|
+
*
|
|
26
|
+
* @example Boolean open/close state
|
|
27
|
+
* ```ts
|
|
28
|
+
* const [open, setOpen, isControlled] = useControllableState(
|
|
29
|
+
* controlledOpen, defaultOpen ?? false, onOpenChange,
|
|
30
|
+
* );
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example Optional string value with no default
|
|
34
|
+
* ```ts
|
|
35
|
+
* const [value, setValue] = useControllableState<string>(
|
|
36
|
+
* controlledValue, undefined, onValueChange,
|
|
37
|
+
* );
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @param controlled - Consumer-supplied value, or `undefined` when the
|
|
41
|
+
* consumer wants the component to manage its own state.
|
|
42
|
+
* @param defaultValue - Initial value when uncontrolled. Read once on
|
|
43
|
+
* mount; subsequent changes to this argument are ignored.
|
|
44
|
+
* @param onChange - Optional notification fired whenever the setter is
|
|
45
|
+
* called, in both controlled and uncontrolled modes.
|
|
46
|
+
*
|
|
47
|
+
* @returns `[value, setValue, isControlled]`. `value` is `T` when a
|
|
48
|
+
* non-undefined `controlled` or `defaultValue` is supplied,
|
|
49
|
+
* otherwise `T | undefined`.
|
|
50
|
+
*/
|
|
51
|
+
export function useControllableState<T>(
|
|
52
|
+
controlled: T | undefined,
|
|
53
|
+
defaultValue: T,
|
|
54
|
+
onChange?: (next: T) => void,
|
|
55
|
+
): readonly [T, (next: T) => void, boolean];
|
|
56
|
+
export function useControllableState<T>(
|
|
57
|
+
controlled: T | undefined,
|
|
58
|
+
defaultValue: T | undefined,
|
|
59
|
+
onChange?: (next: T) => void,
|
|
60
|
+
): readonly [T | undefined, (next: T) => void, boolean];
|
|
61
|
+
export function useControllableState<T>(
|
|
62
|
+
controlled: T | undefined,
|
|
63
|
+
defaultValue: T | undefined,
|
|
64
|
+
onChange?: (next: T) => void,
|
|
65
|
+
): readonly [T | undefined, (next: T) => void, boolean] {
|
|
66
|
+
const isControlled = controlled !== undefined;
|
|
67
|
+
const [uncontrolled, setUncontrolled] = useState<T | undefined>(defaultValue);
|
|
68
|
+
const value = isControlled ? controlled : uncontrolled;
|
|
69
|
+
|
|
70
|
+
const setValue = useCallback(
|
|
71
|
+
(next: T) => {
|
|
72
|
+
if (!isControlled) {
|
|
73
|
+
setUncontrolled(next);
|
|
74
|
+
}
|
|
75
|
+
onChange?.(next);
|
|
76
|
+
},
|
|
77
|
+
[isControlled, onChange],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return [value, setValue, isControlled] as const;
|
|
81
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useCallback, type KeyboardEvent } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getKeyToActionMap,
|
|
5
|
+
type RovingKeyAction,
|
|
6
|
+
} from "../utils/getKeyToActionMap";
|
|
7
|
+
|
|
8
|
+
type UseRovingTabindexOptions<K> = {
|
|
9
|
+
/**
|
|
10
|
+
* Which arrow-key axis is active. `"horizontal"` enables Arrow
|
|
11
|
+
* Left/Right, `"vertical"` enables Arrow Up/Down, `"both"` enables all
|
|
12
|
+
* four. {@link UseRovingTabindexOptions.dir} swaps the horizontal pair
|
|
13
|
+
* under RTL for both `"horizontal"` and `"both"` orientation.
|
|
14
|
+
*/
|
|
15
|
+
orientation: "horizontal" | "vertical" | "both";
|
|
16
|
+
/**
|
|
17
|
+
* Reading direction. When `"rtl"`, swaps the horizontal arrow pair —
|
|
18
|
+
* meaningful for `"horizontal"` and `"both"` orientation. Defaults to
|
|
19
|
+
* `"ltr"`.
|
|
20
|
+
*/
|
|
21
|
+
dir?: "ltr" | "rtl";
|
|
22
|
+
/**
|
|
23
|
+
* The ordered list of keys the navigation should walk through. The
|
|
24
|
+
* caller is responsible for filtering: e.g. RadioGroup and Accordion
|
|
25
|
+
* pre-filter out disabled values (arrow skips them); Tabs passes the
|
|
26
|
+
* unfiltered list (arrow lands on disabled tabs without activating
|
|
27
|
+
* them, which is Tabs' specific keyboard contract).
|
|
28
|
+
*/
|
|
29
|
+
navigable: K[];
|
|
30
|
+
/**
|
|
31
|
+
* The key of the trigger that owns the keydown event. Used as the
|
|
32
|
+
* pivot for "next"/"prev" navigation.
|
|
33
|
+
*/
|
|
34
|
+
currentKey: K;
|
|
35
|
+
/**
|
|
36
|
+
* Callback invoked with the chosen target and the abstract action
|
|
37
|
+
* that produced it. The consumer decides what "go to" means — focus
|
|
38
|
+
* only (Accordion), focus + select (RadioGroup), focus + maybe
|
|
39
|
+
* activate (Tabs).
|
|
40
|
+
*
|
|
41
|
+
* Not called when no key matches and not called when `navigable` is
|
|
42
|
+
* empty.
|
|
43
|
+
*/
|
|
44
|
+
onNavigate: (targetKey: K, action: RovingKeyAction) => void;
|
|
45
|
+
/**
|
|
46
|
+
* When `true`, Home maps to "first" and End to "last". Defaults to
|
|
47
|
+
* `false`.
|
|
48
|
+
*/
|
|
49
|
+
includeHomeEnd?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* When `true`, Enter and Space map to "activate" and trigger
|
|
52
|
+
* `onNavigate(currentKey, "activate")`. Defaults to `false`.
|
|
53
|
+
*/
|
|
54
|
+
includeActivate?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The keyboard half of the WAI-ARIA roving-tabindex pattern: maps
|
|
59
|
+
* arrow / Home / End / Enter-Space keys to navigation between siblings
|
|
60
|
+
* in a compound widget, with optional RTL inversion and Home/End/
|
|
61
|
+
* Enter-Space toggles. The state half (which key is the current Tab
|
|
62
|
+
* stop, focus management, the actual selection / activation) lives in
|
|
63
|
+
* the consumer — the hook just decides "given this key press, which
|
|
64
|
+
* sibling should we navigate to?" and delegates via `onNavigate`.
|
|
65
|
+
*
|
|
66
|
+
* The orientation/dir/Home-End/activate keymap is built by
|
|
67
|
+
* {@link getKeyToActionMap}, which the hook composes internally.
|
|
68
|
+
*
|
|
69
|
+
* The action passed to `onNavigate` is the abstract `RovingKeyAction`
|
|
70
|
+
* vocabulary: `"next" | "prev" | "first" | "last" | "activate"`.
|
|
71
|
+
* Consumers that only care about movement can ignore the action;
|
|
72
|
+
* consumers that distinguish navigation-vs-activation (Tabs in manual
|
|
73
|
+
* activation mode) branch on it.
|
|
74
|
+
*
|
|
75
|
+
* @example RadioGroup — select on every arrow, both orientations
|
|
76
|
+
* ```ts
|
|
77
|
+
* const enabledValues = itemValues.filter((v) => !disabledValues.has(v));
|
|
78
|
+
* const { handleKeyDown } = useRovingTabindex({
|
|
79
|
+
* orientation: "both",
|
|
80
|
+
* navigable: enabledValues,
|
|
81
|
+
* currentKey: value,
|
|
82
|
+
* onNavigate: (target) => {
|
|
83
|
+
* select(target);
|
|
84
|
+
* focusItem(target);
|
|
85
|
+
* },
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @example Tabs — land on disabled but only activate enabled, supports manual mode
|
|
90
|
+
* ```ts
|
|
91
|
+
* const { handleKeyDown } = useRovingTabindex({
|
|
92
|
+
* orientation, dir,
|
|
93
|
+
* navigable: triggerValues, // unfiltered: arrow lands on disabled
|
|
94
|
+
* currentKey: value,
|
|
95
|
+
* includeHomeEnd: true,
|
|
96
|
+
* includeActivate: true,
|
|
97
|
+
* onNavigate: (target, action) => {
|
|
98
|
+
* const isDisabled = disabledTriggerValues.has(target);
|
|
99
|
+
* const shouldActivate =
|
|
100
|
+
* !isDisabled &&
|
|
101
|
+
* (activationMode === "automatic" ||
|
|
102
|
+
* (activationMode === "manual" && action === "activate"));
|
|
103
|
+
* if (shouldActivate) activateTab(target);
|
|
104
|
+
* focusItem(target);
|
|
105
|
+
* },
|
|
106
|
+
* });
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function useRovingTabindex<K>({
|
|
110
|
+
orientation,
|
|
111
|
+
dir = "ltr",
|
|
112
|
+
navigable,
|
|
113
|
+
currentKey,
|
|
114
|
+
onNavigate,
|
|
115
|
+
includeHomeEnd = false,
|
|
116
|
+
includeActivate = false,
|
|
117
|
+
}: UseRovingTabindexOptions<K>): {
|
|
118
|
+
handleKeyDown: (e: KeyboardEvent<HTMLElement>) => void;
|
|
119
|
+
} {
|
|
120
|
+
const handleKeyDown = useCallback(
|
|
121
|
+
(e: KeyboardEvent<HTMLElement>) => {
|
|
122
|
+
const action = getKeyToActionMap({
|
|
123
|
+
orientation,
|
|
124
|
+
dir,
|
|
125
|
+
homeEnd: includeHomeEnd,
|
|
126
|
+
activate: includeActivate,
|
|
127
|
+
})[e.key];
|
|
128
|
+
|
|
129
|
+
if (!action) return;
|
|
130
|
+
if (navigable.length === 0) return;
|
|
131
|
+
|
|
132
|
+
if (action === "activate") {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
onNavigate(currentKey, action);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const total = navigable.length;
|
|
139
|
+
const currentIndex = navigable.indexOf(currentKey);
|
|
140
|
+
let targetIndex: number;
|
|
141
|
+
switch (action) {
|
|
142
|
+
case "first":
|
|
143
|
+
targetIndex = 0;
|
|
144
|
+
break;
|
|
145
|
+
case "last":
|
|
146
|
+
targetIndex = total - 1;
|
|
147
|
+
break;
|
|
148
|
+
case "next":
|
|
149
|
+
case "prev":
|
|
150
|
+
// Bail when the current key isn't in `navigable` — typically
|
|
151
|
+
// because the consumer filtered out a disabled current item
|
|
152
|
+
// (RadioGroup's contract: arrow keys do nothing while focus is
|
|
153
|
+
// on a disabled radio). Home/End above still work regardless,
|
|
154
|
+
// since they don't depend on a current position.
|
|
155
|
+
if (currentIndex === -1) return;
|
|
156
|
+
targetIndex =
|
|
157
|
+
action === "next"
|
|
158
|
+
? (currentIndex + 1) % total
|
|
159
|
+
: (currentIndex - 1 + total) % total;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
onNavigate(navigable[targetIndex], action);
|
|
165
|
+
},
|
|
166
|
+
[
|
|
167
|
+
orientation,
|
|
168
|
+
dir,
|
|
169
|
+
navigable,
|
|
170
|
+
currentKey,
|
|
171
|
+
onNavigate,
|
|
172
|
+
includeHomeEnd,
|
|
173
|
+
includeActivate,
|
|
174
|
+
],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return { handleKeyDown };
|
|
178
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export * from "./AccessibleIcon";
|
|
2
|
+
export * from "./Accordion";
|
|
3
|
+
export * from "./Alert";
|
|
4
|
+
export * from "./Avatar";
|
|
5
|
+
export * from "./Breadcrumb";
|
|
6
|
+
export * from "./Button";
|
|
7
|
+
export * from "./Carousel";
|
|
8
|
+
export * from "./Checkbox";
|
|
9
|
+
export * from "./CheckboxCard";
|
|
10
|
+
export * from "./Collapsible";
|
|
11
|
+
export * from "./ContextMenu";
|
|
12
|
+
export * from "./DirectionProvider";
|
|
13
|
+
export * from "./Divider";
|
|
14
|
+
export * from "./Dropdown";
|
|
15
|
+
export * from "./EmptyState";
|
|
16
|
+
export * from "./Field";
|
|
17
|
+
export * from "./Fieldset";
|
|
18
|
+
export * from "./Input";
|
|
19
|
+
export * from "./InputGroup";
|
|
20
|
+
export * from "./MillerColumns";
|
|
21
|
+
export * from "./Modal";
|
|
22
|
+
export * from "./Portal";
|
|
23
|
+
export * from "./Progress";
|
|
24
|
+
export * from "./RadioCard";
|
|
25
|
+
export * from "./RadioGroup";
|
|
26
|
+
export * from "./Select";
|
|
27
|
+
export * from "./SkipNav";
|
|
28
|
+
export * from "./Slider";
|
|
29
|
+
export * from "./Status";
|
|
30
|
+
export * from "./Switch";
|
|
31
|
+
export * from "./Table";
|
|
32
|
+
export * from "./Tabs";
|
|
33
|
+
export * from "./Textarea";
|
|
34
|
+
export * from "./Toggle";
|
|
35
|
+
export * from "./ToggleGroup";
|
|
36
|
+
export * from "./Tooltip";
|
|
37
|
+
export * from "./Tree";
|
|
38
|
+
export * from "./VisuallyHidden";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal jsdom polyfill for IntersectionObserver.
|
|
3
|
+
*
|
|
4
|
+
* jsdom does not implement IntersectionObserver. The component relies
|
|
5
|
+
* on it as the user-driven scroll → state fallback for browsers
|
|
6
|
+
* without `scrollsnapchange`, and as the source of truth for
|
|
7
|
+
* `isInView(slideIndex)` on the imperative API. Tests can grab the
|
|
8
|
+
* most-recently-constructed instance via `MockIntersectionObserver
|
|
9
|
+
* .latest` and call `.fire(entries)` to simulate visibility changes
|
|
10
|
+
* on observed elements.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type MockEntry = {
|
|
14
|
+
target: Element;
|
|
15
|
+
isIntersecting: boolean;
|
|
16
|
+
intersectionRatio: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class MockIntersectionObserver {
|
|
20
|
+
static instances: MockIntersectionObserver[] = [];
|
|
21
|
+
static get latest(): MockIntersectionObserver | undefined {
|
|
22
|
+
return MockIntersectionObserver.instances[
|
|
23
|
+
MockIntersectionObserver.instances.length - 1
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
static reset() {
|
|
27
|
+
MockIntersectionObserver.instances = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
callback: IntersectionObserverCallback;
|
|
31
|
+
options?: IntersectionObserverInit;
|
|
32
|
+
observed = new Set<Element>();
|
|
33
|
+
root: Element | Document | null = null;
|
|
34
|
+
rootMargin = "";
|
|
35
|
+
thresholds: ReadonlyArray<number> = [];
|
|
36
|
+
|
|
37
|
+
constructor(cb: IntersectionObserverCallback, options?: IntersectionObserverInit) {
|
|
38
|
+
this.callback = cb;
|
|
39
|
+
this.options = options;
|
|
40
|
+
MockIntersectionObserver.instances.push(this);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
observe(target: Element) {
|
|
44
|
+
this.observed.add(target);
|
|
45
|
+
}
|
|
46
|
+
unobserve(target: Element) {
|
|
47
|
+
this.observed.delete(target);
|
|
48
|
+
}
|
|
49
|
+
disconnect() {
|
|
50
|
+
this.observed.clear();
|
|
51
|
+
}
|
|
52
|
+
takeRecords(): IntersectionObserverEntry[] {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Test-only: invoke the callback with synthesised entries for any
|
|
57
|
+
* targets that are currently being observed. */
|
|
58
|
+
fire(entries: MockEntry[]) {
|
|
59
|
+
const matching = entries
|
|
60
|
+
.filter((e) => this.observed.has(e.target))
|
|
61
|
+
.map(
|
|
62
|
+
(e) =>
|
|
63
|
+
({
|
|
64
|
+
target: e.target,
|
|
65
|
+
isIntersecting: e.isIntersecting,
|
|
66
|
+
intersectionRatio: e.intersectionRatio,
|
|
67
|
+
boundingClientRect: e.target.getBoundingClientRect(),
|
|
68
|
+
intersectionRect: e.target.getBoundingClientRect(),
|
|
69
|
+
rootBounds: null,
|
|
70
|
+
time: 0,
|
|
71
|
+
}) as IntersectionObserverEntry,
|
|
72
|
+
);
|
|
73
|
+
if (matching.length > 0) {
|
|
74
|
+
this.callback(matching, this as unknown as IntersectionObserver);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function installIntersectionObserverPolyfill() {
|
|
80
|
+
if (typeof window === "undefined") return;
|
|
81
|
+
(window as unknown as { IntersectionObserver: typeof MockIntersectionObserver })
|
|
82
|
+
.IntersectionObserver = MockIntersectionObserver;
|
|
83
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal jsdom polyfill for the HTML Popover API.
|
|
3
|
+
*
|
|
4
|
+
* jsdom does not yet implement `showPopover()`, `hidePopover()`, or
|
|
5
|
+
* `togglePopover()`. This polyfill installs the three methods on the
|
|
6
|
+
* HTMLElement prototype so components under test can drive the popover
|
|
7
|
+
* API the same way they do in a real browser. Open state is mirrored to
|
|
8
|
+
* a `data-popover-open` attribute so jsdom assertions can observe it.
|
|
9
|
+
* `beforetoggle` / `toggle` events are dispatched to match the spec.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type PopoverState = "open" | "closed";
|
|
13
|
+
|
|
14
|
+
const stateMap = new WeakMap<HTMLElement, PopoverState>();
|
|
15
|
+
|
|
16
|
+
function getState(el: HTMLElement): PopoverState {
|
|
17
|
+
return stateMap.get(el) ?? "closed";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function setState(el: HTMLElement, next: PopoverState) {
|
|
21
|
+
stateMap.set(el, next);
|
|
22
|
+
if (next === "open") {
|
|
23
|
+
el.setAttribute("data-popover-open", "");
|
|
24
|
+
} else {
|
|
25
|
+
el.removeAttribute("data-popover-open");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fireToggleEvents(
|
|
30
|
+
el: HTMLElement,
|
|
31
|
+
oldState: PopoverState,
|
|
32
|
+
newState: PopoverState,
|
|
33
|
+
): boolean {
|
|
34
|
+
const before = new Event("beforetoggle", { cancelable: true, bubbles: false });
|
|
35
|
+
Object.defineProperty(before, "oldState", { value: oldState });
|
|
36
|
+
Object.defineProperty(before, "newState", { value: newState });
|
|
37
|
+
const allowed = el.dispatchEvent(before);
|
|
38
|
+
if (!allowed) return false;
|
|
39
|
+
|
|
40
|
+
const after = new Event("toggle", { cancelable: false, bubbles: false });
|
|
41
|
+
Object.defineProperty(after, "oldState", { value: oldState });
|
|
42
|
+
Object.defineProperty(after, "newState", { value: newState });
|
|
43
|
+
el.dispatchEvent(after);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function installPopoverPolyfill() {
|
|
48
|
+
if (typeof HTMLElement === "undefined") return;
|
|
49
|
+
const proto = HTMLElement.prototype as unknown as {
|
|
50
|
+
showPopover?: () => void;
|
|
51
|
+
hidePopover?: () => void;
|
|
52
|
+
togglePopover?: (force?: boolean) => boolean;
|
|
53
|
+
};
|
|
54
|
+
if (typeof proto.showPopover === "function") return;
|
|
55
|
+
|
|
56
|
+
proto.showPopover = function showPopover(this: HTMLElement) {
|
|
57
|
+
if (getState(this) === "open") return;
|
|
58
|
+
if (!fireToggleEvents(this, "closed", "open")) return;
|
|
59
|
+
setState(this, "open");
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
proto.hidePopover = function hidePopover(this: HTMLElement) {
|
|
63
|
+
if (getState(this) === "closed") return;
|
|
64
|
+
if (!fireToggleEvents(this, "open", "closed")) return;
|
|
65
|
+
setState(this, "closed");
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
proto.togglePopover = function togglePopover(
|
|
69
|
+
this: HTMLElement,
|
|
70
|
+
force?: boolean,
|
|
71
|
+
) {
|
|
72
|
+
const current = getState(this);
|
|
73
|
+
const target: PopoverState =
|
|
74
|
+
force === undefined
|
|
75
|
+
? current === "open"
|
|
76
|
+
? "closed"
|
|
77
|
+
: "open"
|
|
78
|
+
: force
|
|
79
|
+
? "open"
|
|
80
|
+
: "closed";
|
|
81
|
+
if (target === current) return current === "open";
|
|
82
|
+
if (target === "open") this.showPopover!();
|
|
83
|
+
else this.hidePopover!();
|
|
84
|
+
return getState(this) === "open";
|
|
85
|
+
};
|
|
86
|
+
}
|