Skip to content

fix(aria): prevent getOwnedVirtual from returning duplicate nodes#4987

Merged
WilcoFiers merged 3 commits intodequelabs:developfrom
camiha:fix/4840-duplicate-aria-owns
Feb 18, 2026
Merged

fix(aria): prevent getOwnedVirtual from returning duplicate nodes#4987
WilcoFiers merged 3 commits intodequelabs:developfrom
camiha:fix/4840-duplicate-aria-owns

Conversation

@camiha
Copy link
Copy Markdown
Contributor

@camiha camiha commented Jan 11, 2026

When a child element is also referenced via aria-owns, getOwnedVirtual returned the same node twice. Filter aria-owns references that already exist in children using Set.

Closes: #4840

When a child element is also referenced via aria-owns, getOwnedVirtual returned the same node twice. Filter aria-owns references that already exist in children using Set.

Closes: dequelabs#4840
@camiha camiha requested a review from a team as a code owner January 11, 2026 07:52
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Jan 11, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

@WilcoFiers WilcoFiers left a comment

Choose a reason for hiding this comment

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

@camiha Thank you for this pull request. Really appreciate it. The difficulty with this, as is the case for most axe-core issues is that there are subtle edge cases to consider. Do you have access to screen readers to be able to test for the cases I mentioned? If not I can see if we can do the testing.

const childNodeSet = new Set(children.map(child => child.actualNode));
const uniqueOwns = owns.filter(own => !childNodeSet.has(own.actualNode));

return [...children, ...uniqueOwns];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure the order is correct this way. This needs to be tested, but I'd wager that if you have child A and B, and you then use aria-owns on A, it'll change the owned elements order to B, A.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've verified with Safari + VoiceOver and updated accordingly. I also added a test case for this behavior (with aria-owns="c a" and children a, b, c, the order becomes b → c → a).

Comment on lines +21 to +22
const childNodeSet = new Set(children.map(child => child.actualNode));
const uniqueOwns = owns.filter(own => !childNodeSet.has(own.actualNode));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We don't need actualNode or Set here. This can be done with children.includes(owned)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Simplified to use includes() directly.

if (virtualNode.hasAttr('aria-owns')) {
const owns = idrefs(actualNode, 'aria-owns')
.filter(element => !!element)
.map(element => axe.utils.getNodeFromTree(element));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this fully solves the problem of ensuring this function doesn't return the same node twice. You've solved it for returning a child that's also aria-owns, but not for the same node getting picked by aria-owns twice. I think to solve that you need a different solution.

What I don't know is if I do aria-owns="A B A" whether the order I get in the accessibility tree is "A B" or "B A". This should be tested before implementing this, and we'll probably want to leave a comment about it in the code to have a record of that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated to deduplicate by first occurrence and added a comment documenting this behavior.

- Deduplicate aria-owns references by first occurrence
- Move owned children to end to match browser accessibility tree behavior
@camiha camiha requested a review from WilcoFiers January 18, 2026 11:16
@camiha
Copy link
Copy Markdown
Contributor Author

camiha commented Jan 18, 2026

@WilcoFiers
Thank you for the review. This is my first time submitting a code change to an open source project, so I really appreciate the thorough feedback.

I tested the edge cases you mentioned with Safari (26.2) + VoiceOver.

  • Child also in aria-owns
    • with aria-owns="a" and children a, b, I confirmed that VoiceOver reads them in the order b → a.
  • Duplicate IDs in aria-owns
    • with aria-owns="a b a", element A is only announced once, with the first occurrence taking precedence. (a → b)

I've updated the implementation accordingly. I don't have access to JAWS or NVDA testing environments, so if the behavior differs, please let me know and I'll make adjustments.

Copy link
Copy Markdown
Contributor

@WilcoFiers WilcoFiers left a comment

Choose a reason for hiding this comment

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

Excellent work. Thank you @camiha!

For the books; reviewed for security.

@WilcoFiers WilcoFiers merged commit 48ca955 into dequelabs:develop Feb 18, 2026
22 of 23 checks passed
WilcoFiers added a commit that referenced this pull request Mar 30, 2026
)

When a child element is also referenced via aria-owns, getOwnedVirtual
returned the same node twice. Filter aria-owns references that already
exist in children using Set.

Closes: #4840

---------

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
straker pushed a commit that referenced this pull request Mar 30, 2026
)

When a child element is also referenced via aria-owns, getOwnedVirtual
returned the same node twice. Filter aria-owns references that already
exist in children using Set.

Closes: #4840

---------

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
@straker straker mentioned this pull request Mar 30, 2026
WilcoFiers added a commit that referenced this pull request Mar 31, 2026
### [4.11.2](v4.11.1...v4.11.2)
(2026-03-30)

### Bug Fixes

- **aria-valid-attr-value:** handle multiple aria-errormessage IDs
([#4973](#4973))
([9322148](9322148))
- **aria:** prevent getOwnedVirtual from returning duplicate nodes
([#4987](#4987))
([99d1e77](99d1e77)),
closes [#4840](#4840)
- **DqElement:** avoid calling constructors with cloneNode
([#5013](#5013))
([88bc57f](88bc57f))
- **existing-rule:** aria-busy now shows an error message for a use with
unallowed children
([#5017](#5017))
([dded75a](dded75a))
- **scrollable-region-focusable:** clarify the issue is in safari
([#4995](#4995))
([2567afd](2567afd)),
closes
[WebKit#190870](https://github.com/dequelabs/WebKit/issues/190870)
[WebKit#277290](https://github.com/dequelabs/WebKit/issues/277290)
- **scrollable-region-focusable:** do not fail scroll areas when all
content is visible without scrolling
([#4993](#4993))
([240f8b5](240f8b5))
- **target-size:** determine offset using clientRects if target is
display:inline
([#5012](#5012))
([69d81c1](69d81c1))
- **target-size:** ignore widgets that are inline with other inline
elements ([#5000](#5000))
([cf8a3c0](cf8a3c0))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

aria-required-children can double-report related nodes through aria-owns

3 participants