From c67ecf2954dda1f520fe3c4a68f1c6bc47a597d8 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 21 Apr 2026 13:57:04 -0500 Subject: [PATCH 1/3] fix(S2): adjust Menu/Picker transition to prevent background color transition (#9953) Co-authored-by: Robert Snow --- packages/@react-spectrum/s2/src/ComboBox.tsx | 2 +- packages/@react-spectrum/s2/src/Menu.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index 1c8eac6c5ec..d58621974b1 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -268,7 +268,7 @@ export let listboxItem = style({ default: 'default', isLink: 'pointer' }, - transition: 'default' + transition: 'transform' }, getAllowedOverrides()); export let listboxHeader = style<{size?: 'S' | 'M' | 'L' | 'XL'}>({ diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index 26478028846..998c3a7c104 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -200,7 +200,7 @@ export let menuitem = style & default: 'default', isLink: 'pointer' }, - transition: 'default', + transition: 'transform', forcedColorAdjust: 'none' }, getAllowedOverrides()); @@ -296,8 +296,7 @@ export let description = style<{size: 'S' | 'M' | 'L' | 'XL', isFocused: boolean forcedColors: { default: 'inherit' } - }, - transition: 'default' + } }); let value = style({ From abb9082219b46e7f6c80c09b8a71644e4e052a83 Mon Sep 17 00:00:00 2001 From: Theo <114101907+SupaSeeka@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:10:37 +0100 Subject: [PATCH 2/3] fix: support CSS logical properties for Tabs animations (#9951) * Fix TabPanels height animation not working with CSS logical properties * fix/adding-documentation --- .../dev/s2-docs/pages/react-aria/Tabs.mdx | 4 +- packages/react-aria-components/src/Tabs.tsx | 2 +- .../react-aria-components/test/Tabs.test.js | 48 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/Tabs.mdx b/packages/dev/s2-docs/pages/react-aria/Tabs.mdx index d74e9be4fdc..8783fae31bc 100644 --- a/packages/dev/s2-docs/pages/react-aria/Tabs.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Tabs.mdx @@ -290,8 +290,8 @@ function Example() { links={docs.links} showDescription cssVariables={{ - '--tab-panel-width': 'The width of the active tab panel in pixels. Useful for animations.', - '--tab-panel-height': 'The height of the active tab panel in pixels. Useful for animations.' + '--tab-panel-width': 'The width of the active tab panel in pixels. Useful for animations. Set when width, inline-size, or all is used in the CSS transition.', + '--tab-panel-height': 'The height of the active tab panel in pixels. Useful for animations. Set when height, block-size, or all is used in the CSS transition.' }} /> ### TabPanel diff --git a/packages/react-aria-components/src/Tabs.tsx b/packages/react-aria-components/src/Tabs.tsx index b6428283bc5..33dc60b72a9 100644 --- a/packages/react-aria-components/src/Tabs.tsx +++ b/packages/react-aria-components/src/Tabs.tsx @@ -364,7 +364,7 @@ export const TabPanels = /*#__PURE__*/ createHideableComponent(function TabPanel } if (hasTransition.current == null) { - hasTransition.current = /width|height|all/.test(window.getComputedStyle(el).transition); + hasTransition.current = /width|height|block-size|inline-size|all/.test(window.getComputedStyle(el).transition); } if (hasTransition.current && selectedKeyRef.current != null && selectedKeyRef.current !== state.selectedKey) { diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 20f255979ca..7982487d543 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -717,6 +717,54 @@ describe('Tabs', () => { expect(tabPanels).toHaveStyle({width: '100px'}); }); + it('should detect block-size in transition for TabPanels', async () => { + let originalGetComputedStyle = window.getComputedStyle; + window.getComputedStyle = (el) => ({...originalGetComputedStyle(el), transition: 'block-size 400ms ease'}); + + let {getByTestId} = render( + + + A + B + + + A + B + + + ); + + let tabs = document.querySelectorAll('[role="tab"]'); + await user.click(tabs[1]); + + expect(getByTestId('tabpanels').style.getPropertyValue('--tab-panel-height')).not.toBe(''); + window.getComputedStyle = originalGetComputedStyle; + }); + + it('should detect inline-size in transition for TabPanels', async () => { + let originalGetComputedStyle = window.getComputedStyle; + window.getComputedStyle = (el) => ({...originalGetComputedStyle(el), transition: 'inline-size 400ms ease'}); + + let {getByTestId} = render( + + + A + B + + + A + B + + + ); + + let tabs = document.querySelectorAll('[role="tab"]'); + await user.click(tabs[1]); + + expect(getByTestId('tabpanels').style.getPropertyValue('--tab-panel-width')).not.toBe(''); + window.getComputedStyle = originalGetComputedStyle; + }); + it('supports tooltips', async function () { let {getByRole, getAllByRole} = render( From 670b571bb77913614d777cb802c87869c24b744b Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Tue, 21 Apr 2026 16:14:40 -0700 Subject: [PATCH 3/3] fix: include stepStateText in StepListItem accessible name (#9888) Add the VisuallyHidden stepStateText element to the aria-labelledby of each StepListItem link so screen readers announce the step state (Current/Completed/Not completed) as part of the accessible name. This ensures conformance with WCAG SC 2.5.3 Label in Name (Level A) by including the visually hidden but rendered step state text in the computed accessible name for each step. Closes #9881 --- .../src/steplist/StepListItem.tsx | 5 +-- .../test/steplist/StepList.test.tsx | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/@adobe/react-spectrum/src/steplist/StepListItem.tsx b/packages/@adobe/react-spectrum/src/steplist/StepListItem.tsx index 057b6044b65..e10c14c888d 100644 --- a/packages/@adobe/react-spectrum/src/steplist/StepListItem.tsx +++ b/packages/@adobe/react-spectrum/src/steplist/StepListItem.tsx @@ -65,6 +65,7 @@ export function StepListItem(props: SpectrumStepListItemProps): ReactNode } let markerId = useId(); + let stateId = useId(); let labelId = useId(); return ( @@ -78,7 +79,7 @@ export function StepListItem(props: SpectrumStepListItemProps): ReactNode (props: SpectrumStepListItemProps): ReactNode 'is-selectable': state.isSelectable(key) && !isSelected } )}> - {stepStateText} + {stepStateText} diff --git a/packages/@adobe/react-spectrum/test/steplist/StepList.test.tsx b/packages/@adobe/react-spectrum/test/steplist/StepList.test.tsx index c32ab7aabb7..0a7657c5283 100644 --- a/packages/@adobe/react-spectrum/test/steplist/StepList.test.tsx +++ b/packages/@adobe/react-spectrum/test/steplist/StepList.test.tsx @@ -75,6 +75,38 @@ describe('StepList', function () { expect(stepList).toHaveAttribute('id', 'steplist-id'); }); + it('includes step state text in the accessible name via aria-labelledby', function () { + const tree = renderComponent({defaultLastCompletedStep: 'step-two', defaultSelectedKey: 'step-three', onSelectionChange}); + const stepListItems = tree.getAllByRole('link'); + + // Each link should have an aria-labelledby referencing marker, state, and label IDs + for (let link of stepListItems) { + let labelledby = link.getAttribute('aria-labelledby'); + expect(labelledby).toBeTruthy(); + let ids = labelledby!.split(' '); + expect(ids).toHaveLength(3); + for (let id of ids) { + expect(document.getElementById(id)).toBeTruthy(); + } + } + + // Verify the step state text is included in referenced elements + let currentStep = stepListItems[2]; + let currentIds = currentStep.getAttribute('aria-labelledby')!.split(' '); + let stateEl = document.getElementById(currentIds[1]); + expect(stateEl!.textContent).toContain('Current'); + + let completedStep = stepListItems[0]; + let completedIds = completedStep.getAttribute('aria-labelledby')!.split(' '); + let completedStateEl = document.getElementById(completedIds[1]); + expect(completedStateEl!.textContent).toContain('Completed'); + + let notCompletedStep = stepListItems[3]; + let notCompletedIds = notCompletedStep.getAttribute('aria-labelledby')!.split(' '); + let notCompletedStateEl = document.getElementById(notCompletedIds[1]); + expect(notCompletedStateEl!.textContent).toContain('Not'); + }); + it('attaches a user provided ref', function () { const ref = React.createRef>(); const container = renderComponent({ref});