Skip to content

test: add regression for global aria-posinset across sections - #10554

Open
gonzoblasco wants to merge 10 commits into
adobe:mainfrom
gonzoblasco:test/posinset-sections-regression
Open

test: add regression for global aria-posinset across sections#10554
gonzoblasco wants to merge 10 commits into
adobe:mainfrom
gonzoblasco:test/posinset-sections-regression

Conversation

@gonzoblasco

Copy link
Copy Markdown

What

Test-only PR: adds a regression test that locks in global aria-posinset across sections in a virtualized menu.

Why

From the discussion in #9556: aria-posinset must be consistent with aria-setsize (the global item count) for sectioned collections. The current code already produces the correct global position (1..4 across two sections, matching aria-setsize=4), but nothing guards it. This test pins the behavior so a future refactor of the index computation cannot silently reintroduce per-section restart.

Notes

  • Passes against current main (verified locally, 4/4 in useMenu.test.tsx).
  • No production code changes.

Locks in that aria-posinset is global (1..4) across sections in a
virtualized menu and matches aria-setsize, instead of restarting per
group. Passes against current main; guards the behavior from the
discussion in adobe#9556.
@yihuiliao

Copy link
Copy Markdown
Member

Thanks for the PR! You'll need to sign the CLA and then close/re-open the PR to rerun the check

yihuiliao
yihuiliao previously approved these changes Sep 2, 2026
@yihuiliao yihuiliao added the no testing Does not require manual testing during testing session label Sep 2, 2026

@snowystinger snowystinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test isn't rendering the sections

Image

while that shouldn't affect the posinset, it's incomplete

The previous helper flattened sections into plain items, so the DOM never
contained the sectioned menu structure the test claims to cover. Render
each section as a labelled group (role=presentation wrapper + role=group
with aria-labelledby heading), mirroring the real sectioned menu, and
assert the groups are present before checking the global posinset values.
@gonzoblasco

Copy link
Copy Markdown
Author

Fixed in 1af6cf2 - the helper now renders the real sectioned structure instead of flattening.

Each section renders as a role=presentation wrapper with a labelled group inside (role=group + aria-labelledby pointing at the section heading), mirroring how the sectioned menu is built. The test now asserts the two groups are present and each is labelled by its heading before checking the global posinset values, so the DOM shape is locked in as part of the regression.

Verified: 4/4 in useMenu.test.tsx, full lint (format, types, oxlint, constraints) clean.

Comment thread packages/react-aria/test/menu/useMenu.test.tsx Outdated
gonzoblasco and others added 2 commits September 2, 2026 20:17
Render each section through the real useMenuSection hook instead of
hand-rolling the group structure, so any future logic added to the hook
(per-section count restart, references) is exercised by this test.
// sectioned menu structure, so this test locks the real DOM shape.
let groups = getAllByRole2('group');
expect(groups).toHaveLength(2);
// Each group is labelled by its section heading, which is present in the DOM.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Each group is labelled by its section heading, which is present in the DOM.

Comment on lines +199 to +200
// The sections must actually render as groups, mirroring the real
// sectioned menu structure, so this test locks the real DOM shape.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// The sections must actually render as groups, mirroring the real
// sectioned menu structure, so this test locks the real DOM shape.

Comment on lines +212 to +213
// aria-posinset should be global (1..4) and match aria-setsize, not restart
// per section (which would report 1..2 for both groups).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// aria-posinset should be global (1..4) and match aria-setsize, not restart
// per section (which would report 1..2 for both groups).

This comment is the name of the test

Comment on lines +93 to +96
let {itemProps, headingProps, groupProps} = useMenuSection({
heading: node.rendered,
'aria-label': node['aria-label']
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you meant this?

Suggested change
let {itemProps, headingProps, groupProps} = useMenuSection({
heading: node.rendered,
'aria-label': node['aria-label']
});
let {itemProps, headingProps, groupProps} = useMenuSection({
heading: node.props.title,
'aria-label': node['aria-label']
});

though not sure what the aria-label there is for? it could just be omitted i think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 6b65027 - the heading now uses node.props.title (matching how Section renders it) and the redundant aria-label is dropped, since with a heading present useMenuSection labels the group via aria-labelledby.

Verified: 4/4 in useMenu.test.tsx, full lint clean.

@gonzoblasco gonzoblasco Sep 3, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aria-label is the fallback accessible name for sections without a heading. useMenuSection labels the group via aria-labelledby when a heading is present, and falls back to aria-label when the section has no heading (the heading is optional in the ARIA menu pattern). Without it, an unheaded section group would have no accessible name at all.

I kept it and added a test that locks that branch: a Section with aria-label and no title renders a group with aria-label and no aria-labelledby. So the accessible-name contract of sectioned menus is covered on both paths - labelled-by-heading and labelled-by-aria-label.

Verified: 5/5 in useMenu.test.tsx, full lint clean.

Use node.props.title for the section heading (matching how Section renders
it) and drop the redundant aria-label - with a heading present, useMenuSection
labels the group via aria-labelledby.
Comment thread packages/react-aria/test/menu/useMenu.test.tsx Outdated
useMenuSection labels a section group via aria-labelledby when a heading is
present, and falls back to aria-label when the section has no heading. The
new test locks that fallback branch so the accessible name contract of
sectioned menus stays covered.

@snowystinger snowystinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might find this an easier test to write at the RAC level instead of the hook level

return (
<div {...itemProps}>
{node.props.title && <span {...headingProps}>{node.props.title}</span>}
<div {...groupProps}>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know that in our RAC implementation, headings + menu items are siblings. as in, items are not grouped in a wrapper separate from the heading. and as @snowystinger said, probably easier to do this as the RAC level

snowystinger senalo en 3919829010 que el span del heading debe seguir
renderizando node.rendered, no node.props.title - matching how the real
MenuSection component in @adobe/react-spectrum does it. The hook already
uses node.rendered for the heading prop, this restores the same source
for the visible span.

Also explored migrating the test to react-aria-components level with a
real Virtualizer (per snowystinger's suggestion in 5096476622) - found
that the Virtualizer path exposes different posinset behavior (resets
per section, presumably a separate issue from the original adobe#10487 hook
contract this PR locks in). Keeping the test at the hook level where
the original contract was verified.
@gonzoblasco

Copy link
Copy Markdown
Author

Both points addressed in a0bde03:

  1. Span rendering (3919829010): the heading span now renders node.rendered, matching the real MenuSection component. The hook also uses node.rendered, so both the ARIA labelling and the visible content come from the same source - matching the production code path in @adobe/react-spectrum.

  2. RAC-level migration (5096476622): I explored moving the test to react-aria-components with Virtualizer, as you suggested. Found something worth flagging separately: with a real wrapper, aria-posinset resets per section in the rendered output (item 1 of section 2 reports posinset=2, not the global continuation). That looks like a separate contract issue in the Virtualizer path - the hook-level contract from fix(react-aria): make aria-posinset global across sections in listbox and menu #10487 (item.index is global, posinset matches setsize) holds at the hook level but does not appear to propagate through the Virtualizer layout.

Since that is a different contract and a different fix, I kept the test at the hook level where the original #10487 contract was verified, and left the Virtualizer-level finding as a separate observation rather than mixing it into this PR. Happy to open a follow-up issue for the Virtualizer behavior if it is in scope.

Verified: 5/5 in useMenu.test.tsx, full lint clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no testing Does not require manual testing during testing session

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants