fix(react-aria): detect screen reader pointer events as virtual modality#10316
fix(react-aria): detect screen reader pointer events as virtual modality#10316AKnassa wants to merge 3 commits into
Conversation
JAWS and NVDA on Chromium synthesize a pointerdown whose pointerType is 'mouse' rather than the empty string other screen readers report, so handlePointerEvent classified screen reader users as 'pointer'. The event is only distinguishable from a real mouse by its zero contact geometry, which is the signal usePress has used since adobe#3524; useFocusVisible never adopted it. Per the Pointer Events spec a real mouse must report a width and height of 1, so a zero-size pointerdown cannot originate from one. Also broadcast the resolved modality to change handlers instead of the literal 'pointer'. Closes adobe#10310
…al modality Asserts the user-visible outcome of the screen reader fix (isFocusVisible stays true after a zero-geometry pointerdown), guards pen input against misclassification, and pins that a real mouse move returns modality to pointer after a virtual interaction.
| act(() => { | ||
| fireEvent( |
There was a problem hiding this comment.
fireEvent already wraps everything in an act, why the extra one?
There was a problem hiding this comment.
Thanks, you're right, RTL's fireEvent is already act-wrapped. Removed in 61301e8; I'd copied the pattern from the older tests in this file, and left those untouched to keep the diff scoped. Happy to clean them up here too if you'd prefer.
|
@AKnassa |
I clicked without any screen reader in the same button and the event comes with the same width and height |



Closes #10310
✅ Pull Request Checklist:
🐛 Bug
useInteractionModality()reportspointerwhen a JAWS or NVDA user activates a control in Chromium. Screen reader users are treated as mouse users, so focus rings are suppressed and modality-dependent behavior misfires.🔍 Root cause
handlePointerEventinuseFocusVisible.tssetcurrentModality = 'pointer'for everypointerdownwithout consultingisVirtualPointerEvent. Chromium's screen-reader-synthesizedpointerdownreportspointerType: 'mouse'— not the empty string JAWS/Firefox and VoiceOver use — soisVirtualClickon the trailingclickcan't catch it either. Firefox and VoiceOver only work by accident: they emit nopointerdownat all.This makes
useFocusVisiblethe only hook that never adopted the virtual-pointer heuristic the rest of the library has used since #3524 (usePress,useDrag,DragManager,useRangeCalendar).✅ Fix
Reuse the existing
isVirtualPointerEventhelper. Per the Pointer Events spec, a UA MUST reportwidth/heightof1for inputs without contact geometry, so a zero-sizepointerdowncannot originate from a real mouse. Also broadcasts the resolved modality rather than the literal'pointer'.📝 Test Instructions:
Automated:
yarn jest packages/react-aria/test/interactions/useFocusVisible.test.js— twelve tests in theuseInteractionModality with virtual (screen reader) pointer eventsblock. Three prove virtual detection (including subscriber broadcast and that the pairedpointerupdoesn't reset modality), plusisFocusVisible()staying true after a screen-reader pointerdown; the rest guard real mouse, touch, pen, Android TalkBack, Firefox/VoiceOver empty-pointerTypeclick, keyboard, and recovery topointeron a real mouse move. Fullreact-aria(1163) andreact-aria-components(1522) suites pass.Manual (Windows + JAWS or NVDA + Chrome): start the screen reader, activate any React Aria button with Enter, and observe
useInteractionModality()/getInteractionModality()now reportsvirtualinstead ofpointer.I don't have access to JAWS or NVDA. The fix assumes Chromium's synthesized
pointerdownreportswidth: 0, height: 0. This is inferred from (a) the spec forbidding0for real pointers, and (b)usePresshaving relied on this exact signal for JAWS/NVDA browse mode since #3524 — if the geometry were different, that shipped code would already be broken. A maintainer or @jesusgalhub confirming the geometry on real hardware would close this gap.📋 Out of scope (follow-ups)
handleClickEventsetscurrentModality = 'virtual'but never callstriggerChangeHandlers, so the Firefox/VoiceOver virtual click never re-broadcasts to subscribers. Latent, separate.🧢 Your Project:
Individual contributor