From 33746a0a9594e770210ea470cc2f45ba02d08d35 Mon Sep 17 00:00:00 2001
From: chirokas <157580465+chirokas@users.noreply.github.com>
Date: Thu, 7 May 2026 04:13:27 +0800
Subject: [PATCH 1/5] fix(Tree): Dont set focusedKey to a section node key
(#10020)
---
.../react-aria-components/test/Tree.test.tsx | 282 +++++++++---------
.../src/gridlist/useGridListItem.ts | 3 +-
2 files changed, 151 insertions(+), 134 deletions(-)
diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx
index 1da7313e8f9..80c346bbe33 100644
--- a/packages/react-aria-components/test/Tree.test.tsx
+++ b/packages/react-aria-components/test/Tree.test.tsx
@@ -915,144 +915,160 @@ describe('Tree', () => {
});
describe('keyboard interactions', () => {
- it('left and right arrows should navigate between interactive elements in the row', async () => {
- let {getAllByRole} = render();
- let expandableRow = getAllByRole('row')[0];
- let buttons = within(expandableRow).getAllByRole('button');
- let checkbox = within(expandableRow).getByRole('checkbox');
-
- await user.tab();
- expect(expandableRow).toHaveAttribute('aria-expanded', 'true');
- expect(document.activeElement).toBe(expandableRow);
- await user.keyboard('{ArrowRight}');
- expect(document.activeElement).toBe(checkbox);
- await user.keyboard('{ArrowRight}');
- expect(document.activeElement).toBe(buttons[1]);
- await user.keyboard('{ArrowRight}');
- expect(document.activeElement).toBe(buttons[2]);
- await user.keyboard('{ArrowRight}');
- expect(document.activeElement).toBe(expandableRow);
-
- // Test that if focus is on the row that right/left will expand/collapse if it isn't already
- await user.keyboard('{ArrowLeft}');
- expect(document.activeElement).toBe(expandableRow);
- expect(expandableRow).toHaveAttribute('aria-expanded', 'false');
-
- await user.keyboard('{ArrowRight}');
- expect(document.activeElement).toBe(expandableRow);
- expect(expandableRow).toHaveAttribute('aria-expanded', 'true');
-
- // Resume testing navigation to interacive elements
- await user.keyboard('{ArrowLeft}');
- await user.keyboard('{ArrowLeft}');
- expect(document.activeElement).toBe(buttons[2]);
- await user.keyboard('{ArrowLeft}');
- expect(document.activeElement).toBe(buttons[1]);
- await user.keyboard('{ArrowLeft}');
- expect(document.activeElement).toBe(checkbox);
- });
-
- it('should support type ahead', async () => {
- let {getAllByRole, queryByText} = render();
- await user.tab();
- let rows = getAllByRole('row');
- expect(document.activeElement).toBe(rows[0]);
- await user.keyboard('Reports 1ABC');
- expect(document.activeElement).toBe(rows[16]);
-
- act(() => {jest.runAllTimers();});
- await user.keyboard('Pro');
- expect(document.activeElement).toBe(rows[0]);
-
- // Test typeahead doesn't match against hidden rows
- await user.click(rows[12]);
- expect(queryByText('Reports 1ABC')).toBeFalsy();
- await user.keyboard('Reports 1ABC');
- expect(document.activeElement).toBe(rows[12]);
- expect(rows[12]).toHaveAttribute('aria-label', 'Reports');
- });
-
- it('should support collapse key to navigate to parent', async () => {
- let {getAllByRole} = render();
- await user.tab();
- let rows = getAllByRole('row');
- expect(rows).toHaveLength(20);
- expect(document.activeElement).toBe(rows[0]);
- expect(document.activeElement).toHaveAttribute('data-expanded', 'true');
-
- // Navigate down to Project 2B
- await user.keyboard('{ArrowDown}');
- await user.keyboard('{ArrowDown}');
- await user.keyboard('{ArrowRight}');
- await user.keyboard('{ArrowDown}');
- await user.keyboard('{ArrowDown}');
- expect(document.activeElement).toBe(rows[4]);
- expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2B');
-
- // Collapse key on leaf node should move focus to parent (Projects)
- await user.keyboard('{ArrowLeft}');
- expect(document.activeElement).toBe(rows[2]);
- expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2');
- expect(document.activeElement).toHaveAttribute('data-expanded', 'true');
-
- // Collapse key on expanded parent should collapse it
- await user.keyboard('{ArrowLeft}');
- // Projects should now be collapsed, so fewer rows visible
- rows = getAllByRole('row');
- expect(rows.length).toBeLessThan(20);
- expect(document.activeElement).toBe(rows[2]);
- expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2');
- expect(document.activeElement).not.toHaveAttribute('data-expanded');
+ describe.each(['DynamicTree', 'DynamicSectionTree'])('%s', (comp) => {
+ const TreeExample = comp === 'DynamicSectionTree' ? DynamicSectionTree : DynamicTree;
+
+ function getAllRows(tree: HTMLElement) {
+ const rowGroups = within(tree).queryAllByRole('rowgroup');
+ return rowGroups.length > 0
+ ? rowGroups.flatMap((rowGroup) => within(rowGroup).queryAllByRole('row').slice(1))
+ : within(tree).queryAllByRole('row');
+ }
- // Collapse key again on now-collapsed parent should move to its parent
- await user.keyboard('{ArrowLeft}');
- expect(document.activeElement).toBe(rows[0]);
- expect(document.activeElement).toHaveAttribute('aria-label', 'Projects');
- });
+ it('left and right arrows should navigate between interactive elements in the row', async () => {
+ let {getByRole} = render();
+ let tree = getByRole('treegrid');
+ let expandableRow = getAllRows(tree)[0];
+ let buttons = within(expandableRow).getAllByRole('button');
+ let checkbox = within(expandableRow).getByRole('checkbox');
+
+ await user.tab();
+ expect(expandableRow).toHaveAttribute('aria-expanded', 'true');
+ expect(document.activeElement).toBe(expandableRow);
+ await user.keyboard('{ArrowRight}');
+ expect(document.activeElement).toBe(checkbox);
+ await user.keyboard('{ArrowRight}');
+ expect(document.activeElement).toBe(buttons[1]);
+ await user.keyboard('{ArrowRight}');
+ expect(document.activeElement).toBe(buttons[2]);
+ await user.keyboard('{ArrowRight}');
+ expect(document.activeElement).toBe(expandableRow);
+
+ // Test that if focus is on the row that right/left will expand/collapse if it isn't already
+ await user.keyboard('{ArrowLeft}');
+ expect(document.activeElement).toBe(expandableRow);
+ expect(expandableRow).toHaveAttribute('aria-expanded', 'false');
+
+ await user.keyboard('{ArrowRight}');
+ expect(document.activeElement).toBe(expandableRow);
+ expect(expandableRow).toHaveAttribute('aria-expanded', 'true');
+
+ // Resume testing navigation to interacive elements
+ await user.keyboard('{ArrowLeft}');
+ await user.keyboard('{ArrowLeft}');
+ expect(document.activeElement).toBe(buttons[2]);
+ await user.keyboard('{ArrowLeft}');
+ expect(document.activeElement).toBe(buttons[1]);
+ await user.keyboard('{ArrowLeft}');
+ expect(document.activeElement).toBe(checkbox);
+ });
- it('should navigate between visible rows when using Arrow Up/Down', async () => {
- let {getAllByRole} = render();
- await user.tab();
- let rows = getAllByRole('row');
- expect(rows).toHaveLength(20);
- expect(document.activeElement).toBe(rows[0]);
- await user.keyboard('{ArrowDown}');
- expect(document.activeElement).toBe(rows[1]);
- expect(rows[1]).toHaveAttribute('aria-label', 'Project 1');
- await user.keyboard('{ArrowUp}');
+ it('should support type ahead', async () => {
+ let {getByRole, queryByText} = render();
+ await user.tab();
+ let tree = getByRole('treegrid');
+ let rows = getAllRows(tree);
+ expect(document.activeElement).toBe(rows[0]);
+ await user.keyboard('Reports 1ABC');
+ expect(document.activeElement).toBe(rows[16]);
+
+ act(() => {jest.runAllTimers();});
+ await user.keyboard('Pro');
+ expect(document.activeElement).toBe(rows[0]);
+
+ // Test typeahead doesn't match against hidden rows
+ await user.click(rows[12]);
+ expect(queryByText('Reports 1ABC')).toBeFalsy();
+ await user.keyboard('Reports 1ABC');
+ expect(document.activeElement).toBe(rows[12]);
+ expect(rows[12]).toHaveAttribute('aria-label', 'Reports');
+ });
- // Collapse parent row and try arrow navigation again
- await user.keyboard('{ArrowLeft}');
- rows = getAllByRole('row');
- expect(rows).toHaveLength(9);
- await user.keyboard('{ArrowDown}');
- expect(document.activeElement).toBe(rows[1]);
- expect(rows[1]).toHaveAttribute('aria-label', 'Reports');
- await user.keyboard('{ArrowUp}');
- expect(document.activeElement).toBe(rows[0]);
- expect(rows[0]).toHaveAttribute('aria-label', 'Projects');
- });
+ it('should support collapse key to navigate to parent', async () => {
+ let {getByRole} = render();
+ await user.tab();
+ let tree = getByRole('treegrid');
+ let rows = getAllRows(tree);
+ expect(rows).toHaveLength(20);
+ expect(document.activeElement).toBe(rows[0]);
+ expect(document.activeElement).toHaveAttribute('data-expanded', 'true');
+
+ // Navigate down to Project 2B
+ await user.keyboard('{ArrowDown}');
+ await user.keyboard('{ArrowDown}');
+ await user.keyboard('{ArrowRight}');
+ await user.keyboard('{ArrowDown}');
+ await user.keyboard('{ArrowDown}');
+ expect(document.activeElement).toBe(rows[4]);
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2B');
+
+ // Collapse key on leaf node should move focus to parent (Projects)
+ await user.keyboard('{ArrowLeft}');
+ expect(document.activeElement).toBe(rows[2]);
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2');
+ expect(document.activeElement).toHaveAttribute('data-expanded', 'true');
+
+ // Collapse key on expanded parent should collapse it
+ await user.keyboard('{ArrowLeft}');
+ // Projects should now be collapsed, so fewer rows visible
+ rows = getAllRows(tree);
+ expect(rows.length).toBeLessThan(20);
+ expect(document.activeElement).toBe(rows[2]);
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2');
+ expect(document.activeElement).not.toHaveAttribute('data-expanded');
+
+ // Collapse key again on now-collapsed parent should move to its parent
+ await user.keyboard('{ArrowLeft}');
+ expect(document.activeElement).toBe(rows[0]);
+ expect(document.activeElement).toHaveAttribute('aria-label', 'Projects');
+ });
- it('should navigate between visible rows when using Home/End', async () => {
- let {getAllByRole} = render();
- await user.tab();
- let rows = getAllByRole('row');
- expect(rows).toHaveLength(20);
- expect(document.activeElement).toBe(rows[0]);
- await user.keyboard('{End}');
- expect(document.activeElement).toBe(rows[19]);
- expect(rows[19]).toHaveAttribute('aria-label', 'Reports 2');
- await user.keyboard('{Home}');
- expect(document.activeElement).toBe(rows[0]);
+ it('should navigate between visible rows when using Arrow Up/Down', async () => {
+ let {getByRole} = render();
+ await user.tab();
+ let tree = getByRole('treegrid');
+ let rows = getAllRows(tree);
+ expect(rows).toHaveLength(20);
+ expect(document.activeElement).toBe(rows[0]);
+ await user.keyboard('{ArrowDown}');
+ expect(document.activeElement).toBe(rows[1]);
+ expect(rows[1]).toHaveAttribute('aria-label', 'Project 1');
+ await user.keyboard('{ArrowUp}');
+
+ // Collapse parent row and try arrow navigation again
+ await user.keyboard('{ArrowLeft}');
+ rows = getAllRows(tree);
+ expect(rows).toHaveLength(9);
+ await user.keyboard('{ArrowDown}');
+ expect(document.activeElement).toBe(rows[1]);
+ expect(rows[1]).toHaveAttribute('aria-label', 'Reports');
+ await user.keyboard('{ArrowUp}');
+ expect(document.activeElement).toBe(rows[0]);
+ expect(rows[0]).toHaveAttribute('aria-label', 'Projects');
+ });
- // Collapse the 2nd top level row and try End/Home again
- await user.click(rows[12]);
- rows = getAllByRole('row');
- expect(rows).toHaveLength(13);
- await user.keyboard('{Home}');
- await user.keyboard('{End}');
- expect(document.activeElement).toBe(rows[12]);
- expect(rows[12]).toHaveAttribute('aria-label', 'Reports');
+ it('should navigate between visible rows when using Home/End', async () => {
+ let {getByRole} = render();
+ await user.tab();
+ let tree = getByRole('treegrid');
+ let rows = getAllRows(tree);
+ expect(rows).toHaveLength(20);
+ expect(document.activeElement).toBe(rows[0]);
+ await user.keyboard('{End}');
+ expect(document.activeElement).toBe(rows[19]);
+ expect(rows[19]).toHaveAttribute('aria-label', 'Reports 2');
+ await user.keyboard('{Home}');
+ expect(document.activeElement).toBe(rows[0]);
+
+ // Collapse the 2nd top level row and try End/Home again
+ await user.click(rows[12]);
+ rows = getAllRows(tree);
+ expect(rows).toHaveLength(13);
+ await user.keyboard('{Home}');
+ await user.keyboard('{End}');
+ expect(document.activeElement).toBe(rows[12]);
+ expect(rows[12]).toHaveAttribute('aria-label', 'Reports');
+ });
});
});
});
diff --git a/packages/react-aria/src/gridlist/useGridListItem.ts b/packages/react-aria/src/gridlist/useGridListItem.ts
index c2ab564852c..ba06535d305 100644
--- a/packages/react-aria/src/gridlist/useGridListItem.ts
+++ b/packages/react-aria/src/gridlist/useGridListItem.ts
@@ -164,7 +164,8 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt
return;
} else if (
!state.expandedKeys.has(node.key) &&
- node.parentKey
+ node.parentKey &&
+ state.collection.getItem(node.parentKey)?.type === 'item'
) {
// Item is a leaf or already collapsed, move focus to parent
state.selectionManager.setFocusedKey(node.parentKey);
From 9ddf84f187c42bac44717e7609070305800f55d5 Mon Sep 17 00:00:00 2001
From: Devon Govett
Date: Tue, 5 May 2026 10:56:48 -0700
Subject: [PATCH 2/5] Publish
---
packages/@react-spectrum/s2/package.json | 2 +-
.../s2/style/__tests__/style-macro.test.js | 142 +++++++++---------
packages/dev/codemods/package.json | 2 +-
yarn.lock | 4 +-
4 files changed, 75 insertions(+), 75 deletions(-)
diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json
index 40b130b4a7a..c4a1d63ea8b 100644
--- a/packages/@react-spectrum/s2/package.json
+++ b/packages/@react-spectrum/s2/package.json
@@ -1,6 +1,6 @@
{
"name": "@react-spectrum/s2",
- "version": "1.3.0",
+ "version": "1.3.1",
"description": "Spectrum 2 UI components in React",
"license": "Apache-2.0",
"repository": {
diff --git a/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js b/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js
index 71923011af6..70b9d26ef04 100644
--- a/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js
+++ b/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js
@@ -40,26 +40,26 @@ describe('style-macro', () => {
"@layer _.a, _.b, _.c;
@layer _.b {
- .Jbs13:first-child {
+ .Jbs131:first-child {
margin-top: 0.25rem;
}
}
@layer _.c.p {
@media (min-width: 64rem) {
- .Jbpv13:first-child {
+ .Jbpv131:first-child {
margin-top: 0.5rem;
}
}
}
-.-macro-static-YFh7Dd {
- --macro-data-YFh7Dd: {"style":{"marginTop":{":first-child":{"default":4,"lg":8}}},"loc":"undefined:undefined:undefined"};
+.-macro-static-MypNfe {
+ --macro-data-MypNfe: {"style":{"marginTop":{":first-child":{"default":4,"lg":8}}},"loc":"undefined:undefined:undefined"};
}
"
`);
- expect(js).toMatchInlineSnapshot('" Jbs13 Jbpv13 -macro-static-YFh7Dd"');
+ expect(js).toMatchInlineSnapshot('" Jbs131 Jbpv131 -macro-static-MypNfe"');
});
it('should support self references', () => {
@@ -73,59 +73,59 @@ describe('style-macro', () => {
"@layer _.a;
@layer _.a {
- ._kc13 {
+ ._kc131 {
border-top-width: 2px;
}
- .hc13 {
+ .hc131 {
border-bottom-width: 2px;
}
- .mCPFGYc13 {
+ .mCPFGYc131 {
border-inline-start-width: var(--m);
}
- .lc13 {
+ .lc131 {
border-inline-end-width: 2px;
}
- .SMBFGYc13 {
+ .SMBFGYc131 {
padding-inline-start: var(--S);
}
- .Rv13 {
+ .Rv131 {
padding-inline-end: calc(var(--F, var(--M)) * 3 / 8);
}
- .ZjUQgKd13 {
+ .ZjUQgKd131 {
width: calc(200px - var(--m) - var(--S));
}
- .-m_-mc13 {
+ .-m_-mc131 {
--m: 2px;
}
- .-S_-Sv13 {
+ .-S_-Sv131 {
--S: calc(var(--F, var(--M)) * 3 / 8);
}
}
-.-macro-static-1Cddbe {
- --macro-data-1Cddbe: {"style":{"borderWidth":2,"paddingX":"edge-to-text","width":"calc(200px - self(borderStartWidth) - self(paddingStart))"},"loc":"undefined:undefined:undefined"};
+.-macro-static-uhFF5b {
+ --macro-data-uhFF5b: {"style":{"borderWidth":2,"paddingX":"edge-to-text","width":"calc(200px - self(borderStartWidth) - self(paddingStart))"},"loc":"undefined:undefined:undefined"};
}
"
`);
- expect(js).toMatchInlineSnapshot('" _kc13 hc13 mCPFGYc13 lc13 SMBFGYc13 Rv13 ZjUQgKd13 -m_-mc13 -S_-Sv13 -macro-static-1Cddbe"');
+ expect(js).toMatchInlineSnapshot('" _kc131 hc131 mCPFGYc131 lc131 SMBFGYc131 Rv131 ZjUQgKd131 -m_-mc131 -S_-Sv131 -macro-static-uhFF5b"');
});
it('should support allowed overrides', () => {
@@ -142,9 +142,9 @@ describe('style-macro', () => {
color: 'green-400'
});
- expect(js()).toMatchInlineSnapshot('" gw13 pg13 -macro-dynamic-1sq4ojw"');
- expect(overrides).toMatchInlineSnapshot('" g8tmWqb13 pHJ3AUd13 -macro-static-y8GgU"');
- expect(js({}, overrides)).toMatchInlineSnapshot('" g8tmWqb13 pg13 -macro-dynamic-ip5paw"');
+ expect(js()).toMatchInlineSnapshot('" gw131 pg131 -macro-dynamic-mu4yby"');
+ expect(overrides).toMatchInlineSnapshot('" g8tmWqb131 pHJ3AUd131 -macro-static-CvspF"');
+ expect(js({}, overrides)).toMatchInlineSnapshot('" g8tmWqb131 pg131 -macro-dynamic-1b9jdne"');
});
it('should support allowed overrides for properties that expand into multiple', () => {
@@ -159,9 +159,9 @@ describe('style-macro', () => {
translateX: 40
});
- expect(js()).toMatchInlineSnapshot('" -_7PloMd-B13 __Ya13 -macro-dynamic-x9xvvh"');
- expect(overrides).toMatchInlineSnapshot('" -_7PloMd-D13 __Ya13 -macro-static-RwkXic"');
- expect(js({}, overrides)).toMatchInlineSnapshot('" -_7PloMd-D13 __Ya13 -macro-dynamic-1xvle73"');
+ expect(js()).toMatchInlineSnapshot('" -_7PloMd-B131 __Ya131 -macro-dynamic-4f51mn"');
+ expect(overrides).toMatchInlineSnapshot('" -_7PloMd-D131 __Ya131 -macro-static-nMoh6b"');
+ expect(js({}, overrides)).toMatchInlineSnapshot('" -_7PloMd-D131 __Ya131 -macro-dynamic-f33ipt"');
});
it('should support allowed overrides for shorthands', () => {
@@ -176,9 +176,9 @@ describe('style-macro', () => {
padding: 40
});
- expect(js()).toMatchInlineSnapshot('" Tk13 Qk13 Sk13 Rk13 -macro-dynamic-1y1msxh"');
- expect(overrides).toMatchInlineSnapshot('" Tm13 Qm13 Sm13 Rm13 -macro-static-9aCkg"');
- expect(js({}, overrides)).toMatchInlineSnapshot('" Tm13 Qm13 Sm13 Rm13 -macro-dynamic-12xmef1"');
+ expect(js()).toMatchInlineSnapshot('" Tk131 Qk131 Sk131 Rk131 -macro-dynamic-efgw2h"');
+ expect(overrides).toMatchInlineSnapshot('" Tm131 Qm131 Sm131 Rm131 -macro-static-9GxvHb"');
+ expect(js({}, overrides)).toMatchInlineSnapshot('" Tm131 Qm131 Sm131 Rm131 -macro-dynamic-15z7wbl"');
});
it('should support allowed overrides for fontSize', () => {
@@ -193,9 +193,9 @@ describe('style-macro', () => {
fontSize: 'ui-xs'
});
- expect(js()).toMatchInlineSnapshot('" -_6BNtrc-woabcc13 vx13 -macro-dynamic-n1gji4"');
- expect(overrides).toMatchInlineSnapshot('" -_6BNtrc-a13 vx13 -macro-static-Qivc3b"');
- expect(js({}, overrides)).toMatchInlineSnapshot('" -_6BNtrc-a13 vx13 -macro-dynamic-amscby"');
+ expect(js()).toMatchInlineSnapshot('" -_6BNtrc-woabcc131 vx131 -macro-dynamic-zj2kbi"');
+ expect(overrides).toMatchInlineSnapshot('" -_6BNtrc-a131 vx131 -macro-static-EPvKFd"');
+ expect(js({}, overrides)).toMatchInlineSnapshot('" -_6BNtrc-a131 vx131 -macro-dynamic-jkg234"');
});
it("should support allowed overrides for values that aren't defined", () => {
@@ -210,9 +210,9 @@ describe('style-macro', () => {
minWidth: 32
});
- expect(js()).toMatchInlineSnapshot('" gE13 -macro-dynamic-n9ew9r"');
- expect(overrides).toMatchInlineSnapshot('" Nk13 -macro-static-kp86Ie"');
- expect(js({}, overrides)).toMatchInlineSnapshot('" Nk13 gE13 -macro-dynamic-1r3wdy4"');
+ expect(js()).toMatchInlineSnapshot('" gE131 -macro-dynamic-1osl3dc"');
+ expect(overrides).toMatchInlineSnapshot('" Nk131 -macro-static-J7ItY"');
+ expect(js({}, overrides)).toMatchInlineSnapshot('" Nk131 gE131 -macro-dynamic-1soyf0e"');
});
it('should support runtime conditions', () => {
@@ -233,32 +233,32 @@ describe('style-macro', () => {
"@layer _.a;
@layer _.a {
- .gH13 {
+ .gH131 {
background-color: light-dark(rgb(233, 233, 233), rgb(44, 44, 44));
}
- .gF13 {
+ .gF131 {
background-color: light-dark(rgb(225, 225, 225), rgb(50, 50, 50));
}
- .gE13 {
+ .gE131 {
background-color: light-dark(rgb(218, 218, 218), rgb(57, 57, 57));
}
- .pt13 {
+ .pt131 {
color: light-dark(rgb(41, 41, 41), rgb(219, 219, 219));
}
- .po13 {
+ .po131 {
color: light-dark(rgb(19, 19, 19), rgb(242, 242, 242));
}
- .pm13 {
+ .pm131 {
color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));
}
}
@@ -266,9 +266,9 @@ describe('style-macro', () => {
"
`);
- expect(js({})).toMatchInlineSnapshot('" gH13 pt13 -macro-dynamic-1mxpwju"');
- expect(js({isHovered: true})).toMatchInlineSnapshot('" gF13 po13 -macro-dynamic-xzamxv"');
- expect(js({isPressed: true})).toMatchInlineSnapshot('" gE13 pm13 -macro-dynamic-1t1eo"');
+ expect(js({})).toMatchInlineSnapshot('" gH131 pt131 -macro-dynamic-15nvpf0"');
+ expect(js({isHovered: true})).toMatchInlineSnapshot('" gF131 po131 -macro-dynamic-1g7f3s5"');
+ expect(js({isPressed: true})).toMatchInlineSnapshot('" gE131 pm131 -macro-dynamic-1nv6ioy"');
});
it('should support nested runtime conditions', () => {
@@ -287,32 +287,32 @@ describe('style-macro', () => {
"@layer _.a;
@layer _.a {
- .gH13 {
+ .gH131 {
background-color: light-dark(rgb(233, 233, 233), rgb(44, 44, 44));
}
- .gF13 {
+ .gF131 {
background-color: light-dark(rgb(225, 225, 225), rgb(50, 50, 50));
}
- .g_h13 {
+ .g_h131 {
background-color: light-dark(rgb(75, 117, 255), rgb(64, 105, 253));
}
- .g313 {
+ .g3131 {
background-color: light-dark(rgb(59, 99, 251), rgb(86, 129, 255));
}
}
"
`);
- expect(js({})).toMatchInlineSnapshot('" gH13 -macro-dynamic-10lz7uq"');
- expect(js({isHovered: true})).toMatchInlineSnapshot('" gF13 -macro-dynamic-1fdz0sg"');
- expect(js({isSelected: true})).toMatchInlineSnapshot('" g_h13 -macro-dynamic-1xv38n5"');
- expect(js({isSelected: true, isHovered: true})).toMatchInlineSnapshot('" g313 -macro-dynamic-e750r1"');
+ expect(js({})).toMatchInlineSnapshot('" gH131 -macro-dynamic-43fbrn"');
+ expect(js({isHovered: true})).toMatchInlineSnapshot('" gF131 -macro-dynamic-1tkkitt"');
+ expect(js({isSelected: true})).toMatchInlineSnapshot('" g_h131 -macro-dynamic-zzf86q"');
+ expect(js({isSelected: true, isHovered: true})).toMatchInlineSnapshot('" g3131 -macro-dynamic-19tzf3y"');
});
it('should support variant runtime conditions', () => {
@@ -326,9 +326,9 @@ describe('style-macro', () => {
}
});
- expect(js({variant: 'accent'})).toMatchInlineSnapshot('" gY13 -macro-dynamic-hjoyur"');
- expect(js({variant: 'primary'})).toMatchInlineSnapshot('" gjQquMe13 -macro-dynamic-h5ftf1"');
- expect(js({variant: 'secondary'})).toMatchInlineSnapshot('" gw13 -macro-dynamic-8z42q9"');
+ expect(js({variant: 'accent'})).toMatchInlineSnapshot('" gY131 -macro-dynamic-ea1kkk"');
+ expect(js({variant: 'primary'})).toMatchInlineSnapshot('" gjQquMe131 -macro-dynamic-17nl5q"');
+ expect(js({variant: 'secondary'})).toMatchInlineSnapshot('" gw131 -macro-dynamic-fjccci"');
});
it('supports runtime conditions nested inside css conditions', () => {
@@ -346,14 +346,14 @@ describe('style-macro', () => {
@layer _.b.l {
@media (forced-colors: active) {
- .plb13 {
+ .plb131 {
color: ButtonText;
}
}
@media (forced-colors: active) {
- .ple13 {
+ .ple131 {
color: HighlightText;
}
}
@@ -362,8 +362,8 @@ describe('style-macro', () => {
"
`);
- expect(js({})).toMatchInlineSnapshot('" plb13 -macro-dynamic-whoju9"');
- expect(js({isSelected: true})).toMatchInlineSnapshot('" ple13 -macro-dynamic-19u8vf8"');
+ expect(js({})).toMatchInlineSnapshot('" plb131 -macro-dynamic-a7vfaa"');
+ expect(js({isSelected: true})).toMatchInlineSnapshot('" ple131 -macro-dynamic-ojtpnp"');
});
it('inherits parent default when nested branch has no default key', () => {
@@ -379,8 +379,8 @@ describe('style-macro', () => {
});
// forcedColors.default should apply when variant=highlight but !isSelected
expect(css).toContain('ButtonText');
- expect(js({variant: 'highlight'})).toMatchInlineSnapshot('" plb13 -macro-dynamic-whoju9"');
- expect(js({variant: 'highlight', isSelected: true})).toMatchInlineSnapshot('" ple13 -macro-dynamic-19u8vf8"');
+ expect(js({variant: 'highlight'})).toMatchInlineSnapshot('" plb131 -macro-dynamic-a7vfaa"');
+ expect(js({variant: 'highlight', isSelected: true})).toMatchInlineSnapshot('" ple131 -macro-dynamic-ojtpnp"');
});
it('should expand shorthand properties to longhands', () => {
@@ -388,33 +388,33 @@ describe('style-macro', () => {
padding: 24
});
- expect(js).toMatchInlineSnapshot('" Th13 Qh13 Sh13 Rh13 -macro-static-jobh3c"');
+ expect(js).toMatchInlineSnapshot('" Th131 Qh131 Sh131 Rh131 -macro-static-D80Fbe"');
expect(css).toMatchInlineSnapshot(`
"@layer _.a;
@layer _.a {
- .Th13 {
+ .Th131 {
padding-top: 24px;
}
- .Qh13 {
+ .Qh131 {
padding-bottom: 24px;
}
- .Sh13 {
+ .Sh131 {
padding-inline-start: 24px;
}
- .Rh13 {
+ .Rh131 {
padding-inline-end: 24px;
}
}
-.-macro-static-jobh3c {
- --macro-data-jobh3c: {"style":{"padding":24},"loc":"undefined:undefined:undefined"};
+.-macro-static-D80Fbe {
+ --macro-data-D80Fbe: {"style":{"padding":24},"loc":"undefined:undefined:undefined"};
}
"
@@ -430,13 +430,13 @@ describe('style-macro', () => {
"@layer _.a;
@layer _.a {
- .gpQzfVb13 {
+ .gpQzfVb131 {
background-color: rgb(from light-dark(rgb(39, 77, 234), rgb(105, 149, 254)) r g b / 50%);
}
}
-.-macro-static-FwWE9d {
- --macro-data-FwWE9d: {"style":{"backgroundColor":"blue-1000/50"},"loc":"undefined:undefined:undefined"};
+.-macro-static-aOn8Q {
+ --macro-data-aOn8Q: {"style":{"backgroundColor":"blue-1000/50"},"loc":"undefined:undefined:undefined"};
}
"
@@ -455,13 +455,13 @@ describe('style-macro', () => {
"@layer _.a;
@layer _.a {
- .-FUeYm-gE13 {
+ .-FUeYm-gE131 {
--foo: light-dark(rgb(218, 218, 218), rgb(57, 57, 57));
}
}
-.-macro-static-7WNpCd {
- --macro-data-7WNpCd: {"style":{"--foo":{"type":"backgroundColor","value":"gray-300"}},"loc":"undefined:undefined:undefined"};
+.-macro-static-kFNJUb {
+ --macro-data-kFNJUb: {"style":{"--foo":{"type":"backgroundColor","value":"gray-300"}},"loc":"undefined:undefined:undefined"};
}
"
diff --git a/packages/dev/codemods/package.json b/packages/dev/codemods/package.json
index 60e997c84bd..f24cc75f27b 100644
--- a/packages/dev/codemods/package.json
+++ b/packages/dev/codemods/package.json
@@ -28,7 +28,7 @@
"@babel/parser": "^7.24.5",
"@babel/traverse": "^7.24.5",
"@babel/types": "^7.24.5",
- "@react-spectrum/s2": "1.3.0",
+ "@react-spectrum/s2": "1.3.1",
"@react-types/shared": "^3.34.0",
"@types/node": "^24",
"boxen": "^5.1.2",
diff --git a/yarn.lock b/yarn.lock
index 0663c15e716..a5919135cc4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7253,7 +7253,7 @@ __metadata:
"@babel/parser": "npm:^7.24.5"
"@babel/traverse": "npm:^7.24.5"
"@babel/types": "npm:^7.24.5"
- "@react-spectrum/s2": "npm:1.3.0"
+ "@react-spectrum/s2": "npm:1.3.1"
"@react-types/shared": "npm:^3.34.0"
"@types/jscodeshift": "npm:^0.11.11"
"@types/node": "npm:^24"
@@ -7829,7 +7829,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@react-spectrum/s2@npm:1.3.0, @react-spectrum/s2@npm:^1.0.0, @react-spectrum/s2@workspace:packages/@react-spectrum/s2":
+"@react-spectrum/s2@npm:1.3.1, @react-spectrum/s2@npm:^1.0.0, @react-spectrum/s2@workspace:packages/@react-spectrum/s2":
version: 0.0.0-use.local
resolution: "@react-spectrum/s2@workspace:packages/@react-spectrum/s2"
dependencies:
From 77ab796bea458f9f7a62058c6c24fab85d8a4b8d Mon Sep 17 00:00:00 2001
From: Devon Govett
Date: Wed, 6 May 2026 12:36:28 -1000
Subject: [PATCH 3/5] fix: Menu, Select, and ComboBox inside Tabs (#10019)
---
.../react-aria-components/src/ComboBox.tsx | 6 +-
packages/react-aria-components/src/Menu.tsx | 10 +-
packages/react-aria-components/src/Select.tsx | 6 +-
.../react-aria-components/test/Tabs.test.js | 123 ++++++++++++++++++
4 files changed, 138 insertions(+), 7 deletions(-)
diff --git a/packages/react-aria-components/src/ComboBox.tsx b/packages/react-aria-components/src/ComboBox.tsx
index f75cd8d5642..e3fc0c00a1e 100644
--- a/packages/react-aria-components/src/ComboBox.tsx
+++ b/packages/react-aria-components/src/ComboBox.tsx
@@ -33,14 +33,14 @@ import {createHideableComponent} from 'react-aria/private/collections/Hidden';
import {FieldErrorContext} from './FieldError';
import {filterDOMProps} from 'react-aria/filterDOMProps';
import {FormContext} from './Form';
-import {forwardRefType, GlobalDOMAttributes, Key, RefObject} from '@react-types/shared';
+import {GlobalDOMAttributes, Key, RefObject} from '@react-types/shared';
import {GroupContext} from './Group';
import {InputContext} from './Input';
import {LabelContext} from './Label';
import {ListBoxContext, ListStateContext} from './ListBox';
import {OverlayTriggerStateContext} from './Dialog';
import {PopoverContext} from './Popover';
-import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, ReactElement, ReactNode, useCallback, useContext, useMemo, useRef, useState} from 'react';
+import React, {createContext, ForwardedRef, HTMLAttributes, ReactElement, ReactNode, useCallback, useContext, useMemo, useRef, useState} from 'react';
import {TextContext} from './Text';
import {useFilter} from 'react-aria/useFilter';
import {useListFormatter} from 'react-aria/useListFormatter';
@@ -100,7 +100,7 @@ export const ComboBoxStateContext = createContext(props: ComboBoxProps, ref: ForwardedRef) {
+export const ComboBox = /*#__PURE__*/ createHideableComponent(function ComboBox(props: ComboBoxProps, ref: ForwardedRef) {
[props, ref] = useContextProps(props, ref, ComboBoxContext);
let {children, isDisabled = false, isInvalid = false, isRequired = false, isReadOnly = false} = props;
let content = useMemo(() => (
diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx
index 8d474a61192..24d63d3511f 100644
--- a/packages/react-aria-components/src/Menu.tsx
+++ b/packages/react-aria-components/src/Menu.tsx
@@ -75,6 +75,7 @@ import {SharedElementTransition} from './SharedElementTransition';
import {TextContext} from './Text';
import {TreeState, useTreeState} from 'react-stately/useTreeState';
import {useHover} from 'react-aria/useHover';
+import {useIsHidden} from 'react-aria/private/collections/Hidden';
import {useMultipleSelectionState} from 'react-stately/useMultipleSelectionState';
import {useObjectRef} from 'react-aria/useObjectRef';
@@ -87,7 +88,7 @@ export interface MenuTriggerProps extends BaseMenuTriggerProps {
children: ReactNode
}
-export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
+export function MenuTrigger(props: MenuTriggerProps): JSX.Element | null {
let state = useMenuTriggerState(props);
let ref = useRef(null);
let {menuTriggerProps, menuProps} = useMenuTrigger({
@@ -96,6 +97,13 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
}, state, ref);
let scrollRef = useRef(null);
+ // If within a collection (e.g. Tabs), render nothing.
+ // Not using createHideableComponent for this because that also creates a forwardRef.
+ let isHidden = useIsHidden();
+ if (isHidden) {
+ return null;
+ }
+
return (
(props: SelectProps, ref: ForwardedRef) {
+export const Select = /*#__PURE__*/ createHideableComponent(function Select(props: SelectProps, ref: ForwardedRef) {
[props, ref] = useContextProps(props, ref, SelectContext);
let {children, isDisabled = false, isInvalid = false, isRequired = false} = props;
let content = useMemo(() => (
diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js
index 7982487d543..f78eddf1d9c 100644
--- a/packages/react-aria-components/test/Tabs.test.js
+++ b/packages/react-aria-components/test/Tabs.test.js
@@ -12,8 +12,15 @@
import {act, fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal';
import {Button} from '../src/Button';
+import {ComboBox} from '../src/ComboBox';
+import {Input} from '../src/Input';
+import {Label} from '../src/Label';
+import {ListBox, ListBoxItem} from '../src/ListBox';
+import {Menu, MenuItem, MenuTrigger} from '../src/Menu';
+import {Popover} from '../src/Popover';
import React, {useState} from 'react';
import {RouterProvider} from 'react-aria/private/utils/openLink';
+import {Select, SelectValue} from '../src/Select';
import {Tab, TabList, TabPanel, TabPanels, Tabs} from '../src/Tabs';
import {TabsExample} from '../stories/Tabs.stories';
import {Tooltip, TooltipTrigger} from '../src/Tooltip';
@@ -847,4 +854,120 @@ describe('Tabs', () => {
expect(getAllByRole('tab')).toHaveLength(3);
});
}
+
+ it('supports Menu inside Tabs', async () => {
+ let tree = render(
+
+
+
+ First Tab
+ Second Tab
+
+
+
+
+
+
+
+
+
+ First Tab content
+
+
+ Second Tab content
+
+
+ );
+
+ let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')});
+ expect(tester.tabs.length).toBe(2);
+
+ let trigger = tree.getByRole('button');
+ let menu = testUtilUser.createTester('Menu', {root: trigger});
+ await menu.open();
+ expect(menu.options()).toHaveLength(2);
+ await menu.close();
+ });
+
+ it('supports Select inside Tabs', async () => {
+ let tree = render(
+
+
+
+ First Tab
+ Second Tab
+
+
+
+
+ First Tab content
+
+
+ Second Tab content
+
+
+ );
+
+ let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')});
+ expect(tester.tabs.length).toBe(2);
+
+ let trigger = tree.getByRole('button');
+ let menu = testUtilUser.createTester('Select', {root: trigger});
+ await menu.open();
+ expect(menu.options()).toHaveLength(3);
+ await menu.close();
+ });
+
+ it('supports ComboBox inside Tabs', async () => {
+ let tree = render(
+
+
+
+ First Tab
+ Second Tab
+
+
+
+
+
+
+ Cat
+ Dog
+ Kangaroo
+
+
+
+
+
+ First Tab content
+
+
+ Second Tab content
+
+
+ );
+
+ let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')});
+ expect(tester.tabs.length).toBe(2);
+
+ let menu = testUtilUser.createTester('ComboBox', {interactionType: 'keyboard', root: tree.container.querySelector('.react-aria-ComboBox')});
+ await menu.open();
+ expect(menu.options()).toHaveLength(3);
+ await menu.close();
+ });
});
From 12bd517a62f7916bedd5b6ba5a82e72d9285213c Mon Sep 17 00:00:00 2001
From: Devon Govett
Date: Wed, 6 May 2026 12:47:34 -1000
Subject: [PATCH 4/5] fix: MenuItem render function should not receive an href
when not a link (#10018)
---
.../react-aria-components/test/Menu.test.tsx | 12 +++++++++---
packages/react-aria/src/utils/openLink.tsx | 17 +++++++++--------
2 files changed, 18 insertions(+), 11 deletions(-)
diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx
index 798ca2d6eed..38b43a8fa44 100644
--- a/packages/react-aria-components/test/Menu.test.tsx
+++ b/packages/react-aria-components/test/Menu.test.tsx
@@ -137,7 +137,10 @@ describe('Menu', () => {
it('should support custom render function', () => {
let {getAllByRole, getByRole} = renderMenu(
{render: props => },
- {render: props => }
+ {render: props => {
+ expect('href' in props).toBe(false);
+ return ;
+ }}
);
let menu = getByRole('menu');
expect(menu).toHaveAttribute('data-custom', 'true');
@@ -150,8 +153,11 @@ describe('Menu', () => {
it('should support custom render function as a link', () => {
let {getAllByRole, getByRole} = renderMenu(
{render: props => },
- // eslint-disable-next-line jsx-a11y/anchor-has-content
- {href: '#foo', render: props => }
+ {href: '#foo', render: props => {
+ expect(props.href).toBe('#foo');
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
+ return ;
+ }}
);
let menu = getByRole('menu');
expect(menu).toHaveAttribute('data-custom', 'true');
diff --git a/packages/react-aria/src/utils/openLink.tsx b/packages/react-aria/src/utils/openLink.tsx
index d38980d2396..a22fae74ef2 100644
--- a/packages/react-aria/src/utils/openLink.tsx
+++ b/packages/react-aria/src/utils/openLink.tsx
@@ -174,14 +174,15 @@ export function getSyntheticLinkProps(props: LinkDOMProps): DOMAttributes
Date: Thu, 7 May 2026 10:35:53 +1000
Subject: [PATCH 5/5] fix: calendar header spacing (#10017)
---
packages/@react-spectrum/s2/src/Calendar.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx
index 68e61293b3a..6b5360c818f 100644
--- a/packages/@react-spectrum/s2/src/Calendar.tsx
+++ b/packages/@react-spectrum/s2/src/Calendar.tsx
@@ -126,7 +126,8 @@ const titleStyles = style({
font: 'title-lg',
textAlign: 'center',
flexGrow: 1,
- flexShrink: 0
+ flexShrink: 0,
+ marginY: 0
});
const headerCellStyles = style({