diff --git a/docs/app/docs/components/scroll-area/content.mdx b/docs/app/docs/components/scroll-area/content.mdx index 8146928be..7d1de84aa 100644 --- a/docs/app/docs/components/scroll-area/content.mdx +++ b/docs/app/docs/components/scroll-area/content.mdx @@ -1,6 +1,6 @@ import Documentation from '@/components/layout/Documentation/Documentation'; import ScrollAreaExample from "./docs/example_1" -import { code, anatomy, api_documentation, ariaReferences, fullPageRestoration, keyboardShortcuts } from "./docs/codeUsage" +import { code, anatomy, api_documentation, ariaReferences, keyboardShortcuts } from "./docs/codeUsage" - - Use `scrollRestoration="manual"` when ScrollArea is the page-level scroller and the viewport should reset after navigation. - Keep the default `auto` behavior for nested scroll regions, code blocks, chat panes, virtualized lists, and other areas that should preserve their current scroll offset across rerenders. - Pass a stable `restoreKey`, such as the current pathname, to choose when the reset happens. - - - {fullPageRestoration.javascript.code} - - - - - {children} - - - - - - ); -}` - } -}; - export const api_documentation = { root: root_api, scrollbar: scrollbar_api diff --git a/docs/app/docs/components/scroll-area/docs/component_api/root.tsx b/docs/app/docs/components/scroll-area/docs/component_api/root.tsx index ac6498af7..df620847b 100644 --- a/docs/app/docs/components/scroll-area/docs/component_api/root.tsx +++ b/docs/app/docs/components/scroll-area/docs/component_api/root.tsx @@ -8,8 +8,6 @@ const data = { ], data: [ { prop: { name: "type", info_tooltips: "Controls when the scrollbar is visible." }, type: "enum", enum_values: ["auto", "always", "scroll", "hover"], default: "hover" }, - { prop: { name: "scrollRestoration", info_tooltips: "Use manual for full-page scroll containers that should reset their viewport on route changes." }, type: "enum", enum_values: ["auto", "manual"], default: "auto" }, - { prop: { name: "restoreKey", info_tooltips: "Key that triggers a manual scroll reset when it changes, such as a pathname." }, type: "React.Key", default: "undefined" }, { prop: { name: "customRootClass", info_tooltips: "Override the root CSS class." }, type: "string", default: '""' } ] } diff --git a/docs/app/playground/components/BadgePlayground.js b/docs/app/playground/components/BadgePlayground.js index 77437f27e..4f0cfc4bb 100644 --- a/docs/app/playground/components/BadgePlayground.js +++ b/docs/app/playground/components/BadgePlayground.js @@ -3,6 +3,10 @@ import ColorLooper from "../helpers/ColorLooper" import Badge from "@radui/ui/Badge" +const variants = ["solid", "soft", "surface", "outline", "ghost"] +const colors = ["gray", "blue", "green", "red", "plum", "gold"] +const sizes = ["small", "medium", "large", "x-large"] + const Playground = () => (
( docsLink="/docs/components/badge" description="Variants, sizes, and semantic labels with current badge props." > -
- Stable - Beta - Preview - Docs - Ghost -
-
- Small - Medium - Large - XL +
+ {variants.map((variant) => ( +
+ {variant} +
+ {colors.map((color) => {color})} +
+
+ ))} +
+ {sizes.map((size) => {size})} +
diff --git a/docs/app/playground/components/ButtonPlayground.js b/docs/app/playground/components/ButtonPlayground.js index 25ffba3a6..aa8c892e7 100644 --- a/docs/app/playground/components/ButtonPlayground.js +++ b/docs/app/playground/components/ButtonPlayground.js @@ -1,39 +1,30 @@ 'use client' -import { ArrowRight } from "lucide-react" +import { ArrowRight, LoaderCircle } from "lucide-react" import ColorLooper from "../helpers/ColorLooper" import Button from "@radui/ui/Button" +const variants = ["solid", "soft", "outline", "ghost", "destructive"] +const sizes = ["small", "medium", "large", "x-large"] + const Playground = () => (
-
- - - - -
-
- - - - +
+ {variants.map((variant) => ( +
+ {variant} +
+ {sizes.map((size) => )} + + +
+
+ ))}
diff --git a/docs/app/playground/components/CompleteCoveragePlayground.js b/docs/app/playground/components/CompleteCoveragePlayground.js new file mode 100644 index 000000000..0285903db --- /dev/null +++ b/docs/app/playground/components/CompleteCoveragePlayground.js @@ -0,0 +1,150 @@ +'use client' + +import React from "react" +import { ChevronRight, Home, Inbox, Menu, Search, Settings, SlidersHorizontal, X } from "lucide-react" +import Breadcrumb from "@radui/ui/Breadcrumb" +import Button from "@radui/ui/Button" +import Checkbox from "@radui/ui/Checkbox" +import CheckboxCards from "@radui/ui/CheckboxCards" +import CheckboxGroup from "@radui/ui/CheckboxGroup" +import Collapsible from "@radui/ui/Collapsible" +import Combobox from "@radui/ui/Combobox" +import Command from "@radui/ui/Command" +import ContextMenu from "@radui/ui/ContextMenu" +import DataList from "@radui/ui/DataList" +import Disclosure from "@radui/ui/Disclosure" +import Drawer from "@radui/ui/Drawer" +import DropdownMenu from "@radui/ui/DropdownMenu" +import Fieldset from "@radui/ui/Fieldset" +import Heading from "@radui/ui/Heading" +import HoverCard from "@radui/ui/HoverCard" +import Link from "@radui/ui/Link" +import LiveRegion from "@radui/ui/LiveRegion" +import Menubar from "@radui/ui/Menubar" +import Minimap from "@radui/ui/Minimap" +import NavigationMenu from "@radui/ui/NavigationMenu" +import NumberField from "@radui/ui/NumberField" +import Popover from "@radui/ui/Popover" +import Radio from "@radui/ui/Radio" +import RadioCards from "@radui/ui/RadioCards" +import RadioGroup from "@radui/ui/RadioGroup" +import ScrollArea from "@radui/ui/ScrollArea" +import Select from "@radui/ui/Select" +import Skeleton from "@radui/ui/Skeleton" +import Slider from "@radui/ui/Slider" +import Spinner from "@radui/ui/Spinner" +import Splitter from "@radui/ui/Splitter" +import Steps from "@radui/ui/Steps" +import TabNav from "@radui/ui/TabNav" +import Text from "@radui/ui/Text" +import TextArea from "@radui/ui/TextArea" +import TextField from "@radui/ui/TextField" +import Theme from "@radui/ui/Theme" +import Toast, { createToastManager } from "@radui/ui/Toast" +import Toolbar from "@radui/ui/Toolbar" +import Tree from "@radui/ui/Tree" +import ColorLooper from "../helpers/ColorLooper" + +const sizes = ["small", "medium", "large"] +const colors = ["gray", "blue", "green", "red", "plum", "gold"] +const Section = ({ name, description, children }) => {children} +const Grid = ({ children, two = false }) =>
{children}
+const Row = ({ children }) =>
{children}
+const Sample = ({ label, children }) =>
{label}{children}
+const CommandRow = ({ icon, children }) => {icon}{children} +const playgroundToastManager = createToastManager({ timeout: 8000 }) + +const treeItems = [ + { label: "Components", expanded: true, items: [{ label: "Inputs" }, { label: "Overlays" }] }, + { label: "Tokens", expanded: false, items: [{ label: "Colors" }, { label: "Spacing" }] } +] + +const ToastShelf = () => { + React.useEffect(() => () => playgroundToastManager.close(), []) + + return <> +} + +const CompleteCoveragePlayground = () => { + const [cards, setCards] = React.useState(["security"]) + const [checks, setChecks] = React.useState(["email"]) + const [range, setRange] = React.useState([24, 72]) + const [number, setNumber] = React.useState(3) + const [message, setMessage] = React.useState("Ready") + const [contextAction, setContextAction] = React.useState("No action selected") + + return
+
{[["Light / blue", { appearance: "light", accentColor: "blue", radius: "sm", scaling: "90%" }], ["Dark / green", { appearance: "dark", accentColor: "green", radius: "lg", scaling: "100%" }], ["System / red", { appearance: "system", accentColor: "red", radius: "md", scaling: "110%" }]].map(([label, props]) => {label})}
+ +
{sizes.map((size) => )}{colors.map((color) => )}
+ +
{[["security", "Security alerts"], ["product", "Product updates"], ["billing", "Billing notices"]].map(([value, label]) =>
{label}
Notification preference
)}
+ +
{["email", "sms", "push"].map((value) => {value})}
+ +
{["outline", "solid"].map((variant) => {sizes.map((size) => )})}
+ +
{["compact", "comfortable", "spacious"].map((value) => {value})}
+ +
{[["starter", "Starter"], ["pro", "Pro"], ["enterprise", "Enterprise"]].map(([value, label]) =>
{label}
Choose this plan
)}
+ +
{sizes.map((size) => )}
+ +
{["soft", "outline", "solid", "ghost"].map((variant) => + ); + + const root = container.firstElementChild as HTMLDivElement; + const input = screen.getByRole('textbox', { name: 'Message' }); + expect(input).toHaveAttribute('id', 'message'); + expect(input).toHaveAttribute('readonly'); + expect(root).not.toHaveAttribute('aria-label'); + expect(root).not.toHaveAttribute('id'); + expect(root).not.toHaveTextContent('DraftDraft'); + }); + it('renders without console warnings', () => { const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); const error = jest.spyOn(console, 'error').mockImplementation(() => {}); diff --git a/src/components/ui/TextArea/TextArea.tsx b/src/components/ui/TextArea/TextArea.tsx index 3f0969e5e..adf5d5a3a 100644 --- a/src/components/ui/TextArea/TextArea.tsx +++ b/src/components/ui/TextArea/TextArea.tsx @@ -4,10 +4,15 @@ import clsx from 'clsx'; import TextAreaRoot, { TextAreaRootProps } from './fragments/TextAreaRoot'; import TextAreaInput, { TextAreaInputProps } from './fragments/TextAreaInput'; -export type TextAreaProps = React.ComponentPropsWithoutRef<'div'> & TextAreaRootProps & TextAreaInputProps & { - customRootClass?: string; - readonly ?: boolean; - disabled ?: boolean; +export type TextAreaProps = TextAreaInputProps & Pick & { + /** Classes are applied to the styled root for backwards compatibility. */ + className?: string; + /** Styles are applied to the styled root for backwards compatibility. */ + style?: React.CSSProperties; + readonly?: boolean; + disabled?: boolean; }; type TextAreaComponent = React.ForwardRefExoticComponent>> & { @@ -15,13 +20,42 @@ type TextAreaComponent = React.ForwardRefExoticComponent, TextAreaProps>(({ customRootClass = '', placeholder = '', className = '', disabled = false, readonly = false, children, ...props }, ref) => { +const TextArea = React.forwardRef, TextAreaProps>(({ + customRootClass = '', + placeholder = '', + className = '', + style, + disabled = false, + readonly, + readOnly, + children, + variant, + size, + resize, + color, + radius, + ...inputProps +}, ref) => { return ( - - + + {children} - {children} ); }) as TextAreaComponent; diff --git a/src/components/ui/TextArea/__snapshots__/TextArea.test.tsx.snap b/src/components/ui/TextArea/__snapshots__/TextArea.test.tsx.snap index 66ce9dfad..22e197247 100644 --- a/src/components/ui/TextArea/__snapshots__/TextArea.test.tsx.snap +++ b/src/components/ui/TextArea/__snapshots__/TextArea.test.tsx.snap @@ -10,6 +10,5 @@ exports[`TextArea matches snapshot 1`] = ` > content - content
`; diff --git a/src/components/ui/TextArea/tests/TextArea.a11y.test.tsx b/src/components/ui/TextArea/tests/TextArea.a11y.test.tsx index 830331007..d3687abe0 100644 --- a/src/components/ui/TextArea/tests/TextArea.a11y.test.tsx +++ b/src/components/ui/TextArea/tests/TextArea.a11y.test.tsx @@ -6,7 +6,7 @@ import TextArea from '../TextArea'; describe('TextArea accessibility', () => { test('axe: no violations', async() => { - const { container } = render(); + const { container } = render(); const results = await axe.run(container, { runOnly: { type: 'tag', values: ACCESSIBILITY_TEST_TAGS } }); expect(results.violations).toHaveLength(0); }); diff --git a/src/components/ui/Toast/fragments/ToastViewport.tsx b/src/components/ui/Toast/fragments/ToastViewport.tsx index c8e039aba..5c6cfe3b1 100644 --- a/src/components/ui/Toast/fragments/ToastViewport.tsx +++ b/src/components/ui/Toast/fragments/ToastViewport.tsx @@ -52,7 +52,6 @@ const ToastViewport: React.FC = ({ children, className, styl return (
    { expect(results.violations).toHaveLength(0); }); - // The viewport is the landmark that lets a screen reader user skip to - // notifications; the individual toasts are the live regions. - test('viewport is a named region', async() => { + // The viewport is a named list; the individual toasts are live regions. + test('viewport is a named list', async() => { renderToasts(); emit({ title: 'Saved' }); - await waitFor(() => expect(screen.getByRole('region', { name: 'Notifications' })).toBeInTheDocument()); + await waitFor(() => expect(screen.getByRole('list', { name: 'Notifications' })).toBeInTheDocument()); }); test('each toast is a polite status region', async() => { diff --git a/src/components/ui/Toast/tests/Toast.toaster.test.tsx b/src/components/ui/Toast/tests/Toast.toaster.test.tsx index 426d2dcb8..602cfd4f4 100644 --- a/src/components/ui/Toast/tests/Toast.toaster.test.tsx +++ b/src/components/ui/Toast/tests/Toast.toaster.test.tsx @@ -38,7 +38,7 @@ describe('Toaster convenience wrapper', () => { /> ); - const viewport = screen.getByRole('region', { name: 'Alerts' }); + const viewport = screen.getByRole('list', { name: 'Alerts' }); expect(viewport).toHaveClass('viewport-class'); expect(viewport).toHaveStyle({ outlineOffset: '4px' diff --git a/src/core/primitives/Checkbox/fragments/CheckboxPrimitiveRoot.tsx b/src/core/primitives/Checkbox/fragments/CheckboxPrimitiveRoot.tsx index 3ea849b53..691f203ab 100644 --- a/src/core/primitives/Checkbox/fragments/CheckboxPrimitiveRoot.tsx +++ b/src/core/primitives/Checkbox/fragments/CheckboxPrimitiveRoot.tsx @@ -53,7 +53,16 @@ const CheckboxPrimitiveRoot = forwardRef + }} + name={name} + value={value} + checked={isChecked === true} + disabled={disabled} + required={required} + aria-hidden="true" + tabIndex={-1} + readOnly + /> ; } ); diff --git a/src/core/primitives/Checkbox/tests/CheckboxPrimitive.test.tsx b/src/core/primitives/Checkbox/tests/CheckboxPrimitive.test.tsx index 9637edb30..216568cdc 100644 --- a/src/core/primitives/Checkbox/tests/CheckboxPrimitive.test.tsx +++ b/src/core/primitives/Checkbox/tests/CheckboxPrimitive.test.tsx @@ -9,6 +9,19 @@ const TickIcon = () => ( ); describe('CheckboxPrimitive', () => { + it('keeps the form input out of the accessibility and tab order', () => { + const { container } = render( + + + + ); + + expect(container.querySelectorAll('[role="checkbox"]')).toHaveLength(1); + const input = container.querySelector('input[type="checkbox"]'); + expect(input).toHaveAttribute('aria-hidden', 'true'); + expect(input).toHaveAttribute('tabindex', '-1'); + }); + it('renders and toggles (uncontrolled)', () => { const { container, queryByText } = render( diff --git a/src/core/primitives/CheckboxGroup/fragments/CheckboxGroupPrimitiveRoot.tsx b/src/core/primitives/CheckboxGroup/fragments/CheckboxGroupPrimitiveRoot.tsx index 499e02fb3..e2774d4a6 100644 --- a/src/core/primitives/CheckboxGroup/fragments/CheckboxGroupPrimitiveRoot.tsx +++ b/src/core/primitives/CheckboxGroup/fragments/CheckboxGroupPrimitiveRoot.tsx @@ -28,7 +28,7 @@ const CheckboxGroupPrimitiveRoot = forwardRef - {children} +
    {children}
    diff --git a/src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx index d25e5b3e0..ee9dadd60 100644 --- a/src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx +++ b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveItem.tsx @@ -29,6 +29,7 @@ const ComboboxPrimitiveItem = React.forwardRef< const { handleSelect, + isOpen, isTypingRef, getItemProps, activeIndex, @@ -41,6 +42,8 @@ const ComboboxPrimitiveItem = React.forwardRef< hiddenIndices, disabledIndices, setDisabledIndices, + setActiveIndex, + elementsRef, labelsRef, displayLabelsRef, valuesRef, @@ -117,6 +120,48 @@ const ComboboxPrimitiveItem = React.forwardRef< } }, [hasSearch, isSelected, selectedItemRef]); + React.useLayoutEffect(() => { + if (!isOpen || !isSelected || hasSearch) return; + setActiveIndex(index); + itemRef.current?.focus(); + }, [hasSearch, index, isOpen, isSelected, setActiveIndex]); + + const handleKeyDownCapture = (event: React.KeyboardEvent) => { + props.onKeyDownCapture?.(event); + if (event.defaultPrevented || hasSearch) return; + + const itemCount = elementsRef.current.length; + let nextIndex = index; + + if (event.key === KEYBOARD_KEYS.ARROW_DOWN || event.key === KEYBOARD_KEYS.ARROW_UP) { + const step = event.key === KEYBOARD_KEYS.ARROW_DOWN ? 1 : -1; + for (let offset = 1; offset <= itemCount; offset += 1) { + const candidate = (index + (offset * step) + itemCount) % itemCount; + if (!disabledIndices.includes(candidate) && elementsRef.current[candidate]) { + nextIndex = candidate; + break; + } + } + } else if (event.key === KEYBOARD_KEYS.HOME || event.key === KEYBOARD_KEYS.END) { + const step = event.key === KEYBOARD_KEYS.HOME ? 1 : -1; + const start = event.key === KEYBOARD_KEYS.HOME ? 0 : itemCount - 1; + for (let offset = 0; offset < itemCount; offset += 1) { + const candidate = start + (offset * step); + if (!disabledIndices.includes(candidate) && elementsRef.current[candidate]) { + nextIndex = candidate; + break; + } + } + } else { + return; + } + + event.preventDefault(); + event.stopPropagation(); + setActiveIndex(nextIndex); + elementsRef.current[nextIndex]?.focus(); + }; + return ( {children} diff --git a/src/core/primitives/Combobox/fragments/ComboboxPrimitivePortal.tsx b/src/core/primitives/Combobox/fragments/ComboboxPrimitivePortal.tsx index 5d29e63d8..e068bd7e0 100644 --- a/src/core/primitives/Combobox/fragments/ComboboxPrimitivePortal.tsx +++ b/src/core/primitives/Combobox/fragments/ComboboxPrimitivePortal.tsx @@ -15,7 +15,7 @@ const ComboboxPrimitivePortal = React.forwardRef< container ?? themeContext?.portalRootRef.current ?? themeContext?.containerRef.current - ?? document.body + ?? (typeof document !== 'undefined' ? document.body : null) ) as HTMLElement | null; useEffect(() => { diff --git a/src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx index 8aba49933..ddddb2e70 100644 --- a/src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx +++ b/src/core/primitives/Combobox/fragments/ComboboxPrimitiveRoot.tsx @@ -184,6 +184,11 @@ const ComboboxPrimitiveRoot = React.forwardRef< sticky ]); + const handleOpenChange = React.useCallback((nextOpen: boolean) => { + setIsOpen(nextOpen); + setActiveIndex(nextOpen ? selectedIndex : null); + }, [selectedIndex]); + const { refs, floatingStyles, @@ -195,7 +200,7 @@ const ComboboxPrimitiveRoot = React.forwardRef< } = Floater.useFloating({ middleware, open: isOpen, - onOpenChange: setIsOpen, + onOpenChange: handleOpenChange, placement: resolvedPlacement, strategy: positioningStrategy, whileElementsMounted: (reference, floating, updatePosition) => Floater.autoUpdate( @@ -266,6 +271,15 @@ const ComboboxPrimitiveRoot = React.forwardRef< setSelectedLabel(label); }, [labelsVersion, selectedValue]); + useLayoutEffect(() => { + if (!isOpen) { + setActiveIndex(null); + return; + } + + setActiveIndex(selectedIndex); + }, [isOpen, selectedIndex]); + const listNav = Floater.useListNavigation(floatingContext, { listRef: elementsRef, activeIndex, diff --git a/src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx b/src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx index 685c3a9a6..74c2a7ef8 100644 --- a/src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx +++ b/src/core/primitives/Dialog/fragments/DialogPrimitiveContent.tsx @@ -29,6 +29,7 @@ const DialogPrimitiveContent = forwardRef { const { isOpen, getFloatingProps, refs, floaterContext } = useContext(DialogPrimitiveContext); const previousFocusedElementRef = React.useRef(null); + const wasOpenRef = React.useRef(isOpen); const referenceElement = refs.reference?.current; const mergedRef = Floater.useMergeRefs([refs.setFloating, ref]); @@ -40,9 +41,16 @@ const DialogPrimitiveContent = forwardRef { if (finalFocus === false) { return null; diff --git a/src/core/primitives/Radio/index.tsx b/src/core/primitives/Radio/index.tsx index ca1baffb8..134a8aebb 100644 --- a/src/core/primitives/Radio/index.tsx +++ b/src/core/primitives/Radio/index.tsx @@ -20,7 +20,6 @@ const RadioPrimitive = React.forwardRef { expect(radio).toHaveAttribute('name', 'test-radio'); expect(radio).toHaveAttribute('value', 'option1'); expect(radio).toHaveAttribute('id', 'radio1'); + expect((radio as HTMLInputElement).tabIndex).toBe(0); }); it('applies checked, required, and disabled props', () => { diff --git a/src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx b/src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx index 981312d87..d8dc37de3 100644 --- a/src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx +++ b/src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx @@ -33,48 +33,46 @@ const RadioGroupPrimitiveRoot = React.forwardRef + - + - - {children} - +
    {children}
    -
    -
    - {name && ( - <> - - {required && selectedValue !== '' && ( - {}} - disabled={groupDisabled} - required - aria-hidden='true' - tabIndex={-1} - style={{ - position: 'absolute', - width: 0, - height: 0, - opacity: 0, - pointerEvents: 'none', - margin: 0 - }} - /> + {name && ( + <> + + {required && selectedValue !== '' && ( + {}} + disabled={groupDisabled} + required + aria-hidden='true' + tabIndex={-1} + style={{ + position: 'absolute', + width: 0, + height: 0, + opacity: 0, + pointerEvents: 'none', + margin: 0 + }} + /> + )} + )} - - )} -
    + + + ) ; } diff --git a/src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx b/src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx index d158f11fa..2685e27de 100644 --- a/src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx +++ b/src/core/utils/RovingFocusGroup/fragments/RovingFocusItem.tsx @@ -64,7 +64,7 @@ const RovingFocusItem = forwardRef(({ const isDisabled = child?.props?.disabled === true; const childElementType = typeof child?.type === 'string' ? child.type.toLowerCase() : ''; const isLinkLikeChild = childElementType === 'a' || child?.props?.href != null; - const resolvedRole = role ?? (isLinkLikeChild ? undefined : 'button'); + const resolvedRole = role ?? (isLinkLikeChild ? 'link' : 'button'); const resolvedType = type ?? (isLinkLikeChild ? undefined : 'button'); // Is this item currently selected @@ -278,7 +278,6 @@ const RovingFocusItem = forwardRef(({ data-child-disabled={isDisabled} role={resolvedRole} type={resolvedType} - aria-selected={isSelected} aria-disabled={isDisabled} aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} diff --git a/src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx b/src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx index 92cd20b30..2733ad117 100644 --- a/src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx +++ b/src/core/utils/RovingFocusGroup/fragments/RovingFocusRoot.tsx @@ -60,7 +60,6 @@ const RovingFocusRoot = React.forwardRef, RovingFocusRoo return { await user.tab(); expect(item1).toHaveFocus(); + expect(item1).toHaveAttribute('tabindex', '0'); + expect(item1).not.toHaveAttribute('aria-selected'); await user.keyboard('{ArrowRight}'); expect(item2).toHaveFocus(); + expect(item1).toHaveAttribute('tabindex', '-1'); + expect(item2).toHaveAttribute('tabindex', '0'); + expect(item2).not.toHaveAttribute('aria-selected'); await user.tab(); expect(outside).toHaveFocus(); diff --git a/src/setupTests.ts b/src/setupTests.ts index a9ce594eb..cb91e41c9 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -4,6 +4,7 @@ import { DOCUMENT_OVERLAY_OPEN_ATTRIBUTE, resetDocumentOverlayOpenForTests } fro afterEach(() => { resetDocumentOverlayOpenForTests(); + if (typeof document === 'undefined') return; document.documentElement.removeAttribute(DOCUMENT_OVERLAY_OPEN_ATTRIBUTE); document.body.style.overflow = ''; document.body.style.paddingRight = '';