Support focusable aria-disabled-only buttons (do not force native disabled)#2846
Support focusable aria-disabled-only buttons (do not force native disabled)#2846KAMRONBEK wants to merge 1 commit into
Conversation
createDOMProps coupled 'aria-disabled' to the native 'disabled'
attribute: any element rendered as button/form/input/select/textarea
with 'aria-disabled' also received 'disabled', which removes it from
the tab order and blocks screen-reader interaction. This made the
ARIA-recommended focusable aria-disabled-only pattern impossible.
Pressable also rendered 'aria-disabled={disabled}' after the prop
spread, clobbering a user-supplied 'aria-disabled' with undefined
when 'disabled' was not set, so the attribute was stripped entirely.
Gate the native 'disabled' attribute on 'accessibilityDisabled' and
pass Pressable's 'disabled' through as 'accessibilityDisabled' (as
the Touchables already do). 'disabled'/'accessibilityDisabled' keep
producing both attributes exactly as before; only 'aria-disabled'
alone now emits just the ARIA attribute.
Fix necolas#2736
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 8308187:
|
|
@necolas TL;DR for #2736: |
Problem
It is impossible to render the ARIA-recommended focusable disabled control, e.g.
<button aria-disabled="true">. With<Pressable aria-disabled role="button">the attribute is stripped from the DOM entirely, and passingaria-disabledto any element rendered asbutton/form/input/select/textareaalso forces the nativedisabledattribute, which removes the element from the tab order and prevents screen-reader users from discovering it and receiving feedback.Root cause
Two coupled defects:
createDOMPropsmerged the props (const disabled = ariaDisabled || accessibilityDisabled) and, when true, set botharia-disabled="true"and the nativedisabledattribute for button/form/input/select/textarea element types. There was no way to get the ARIA attribute without the native one.Pressablerenderedaria-disabled={disabled}after{...rest}, so a user-suppliedaria-disabledwas clobbered withundefinedwhenever thedisabledprop was not set — which is why the attribute disappears from the DOM completely in the issue's repro.Fix
createDOMProps: the nativedisabledattribute is now only set whenaccessibilityDisabled === true;aria-disabledalone emits justaria-disabled="true", keeping the element focusable and perceivable by assistive technology. WhenaccessibilityDisabledis set (with or withoutaria-disabled) the output is unchanged: both attributes, exactly as before.Pressable: passes itsdisabledprop through asaccessibilityDisabled(mirroring whatTouchableOpacity/TouchableHighlight/TouchableWithoutFeedbackalready do), so<Pressable disabled role="button">keeps producing byte-identical DOM (aria-disabled="true" disabled tabindex="-1", pointer-events/press/hover gating untouched), while a user-suppliedaria-disablednow flows through untouched:<Pressable aria-disabled role="button">renders<button aria-disabled="true" role="button" tabindex="0" type="button">withonPressstill firing — matching React Native's behavior on iOS/Android described in the issue.Tests
createDOMProps:accessibilityDisabledon button →aria-disabled+disabled(as today);aria-disabledonly →aria-disabledwithoutdisabled; both props → both attributes (as today).Pressable: snapshot + attribute assertions fordisabledwithrole="button"(locks current behavior) andaria-disabledwithrole="button"(new pattern).disabled/accessibilityDisabledusers). The new aria-disabled-only tests fail without the fix.flow,prettier, andeslintpass.Note for maintainers
The
aria-disabled→ nativedisabledcoupling increateDOMPropslooks deliberate ("Enhance with native semantics"), so this PR is a semantic decision as much as a bug fix: it implements the ARIA authoring-practices pattern wherearia-disabledcommunicates the disabled state while keeping the control focusable, and reserves the native attribute fordisabled/accessibilityDisabled. Happy to adjust if you prefer a different split (e.g. keying the native attribute off a different prop).Fixes #2736