Skip to content

Preserve unset accessibilityState.selected in C++ props (#58599) - #58599

Open
crazeface wants to merge 1 commit into
react:mainfrom
crazeface:export-D120049025
Open

crazeface wants to merge 1 commit into
react:mainfrom
crazeface:export-D120049025

Conversation

@crazeface

@crazeface crazeface commented Sep 18, 2026

Copy link
Copy Markdown

Summary:

AccessibilityState::selected was a plain bool defaulting to false, so the
native side could not tell a component that is selectable but currently
unselected (accessibilityState={{selected: false}}) from one that is not
selectable at all (accessibilityState={{}}). Both arrived as false. The JS
type is already selected?: ?boolean, so this is the bridge discarding a value
the public API accepts.

Make selected a std::optional<bool> defaulting to std::nullopt.

JS accessibilityState native selected (before -> after)
{} false -> undefined
{selected: false} false -> false
{selected: true} true -> true

This aligns the representation with ARIA, which accessibilityState mirrors:
the bool / tri-state split now tracks which ARIA attributes admit an undefined
value.

field ARIA value type admits undefined representation
disabled boolean no bool
busy boolean no bool
selected boolean yes std::optional (changed)
expanded boolean yes std::optional
checked tristate yes CheckedState (None = unset)

That is also why disabled and busy stay plain bool: ARIA gives them no
undefined value, so there is no unset state to preserve. expanded was made
optional for this same reason in
#40881 and checked has always
carried a None; selected was the outlier.

Nor is "unset" merely "absent" for this attribute. testing-library/dom
computes it as boolean | undefined, documented "false/true if (not)selected,
undefined if not selectable" -- the same shape, with the same meaning, that
this change introduces.

Host platforms need the distinction: on Windows a selectable component must
implement ISelectionItemProvider so UIA can report selection state, and with
the old representation every component carrying an accessibilityState looked
selectable.

iOS and Android rendering is unchanged. Trait derivation coalesces the optional
with value_or(false), and the Android serializer omits the key when the value
is unset, which BaseViewManager#setViewState already handles by falling back
to setSelected(false).

Reviewer note: std::optional<bool> is contextually convertible to bool, so a
bare if (state.selected) still compiles but tests engagement rather than
value, silently marking an explicitly unselected component as selected. There is
a regression test for that specific hazard.

Fixes #46988
Supersedes #47296, which went stale.

Changelog:
[General][Breaking] - AccessibilityState::selected is now std::optional<bool> in C++ props, preserving an unset selected instead of coercing it to false

Reviewed By: javache

Differential Revision: D120049025

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 18, 2026
@meta-codesync

meta-codesync Bot commented Sep 18, 2026

Copy link
Copy Markdown

@crazeface has exported this pull request. If you are a Meta employee, you can view the originating Diff in D120049025.

Summary:
Pull Request resolved: react#58599

`AccessibilityState::selected` was a plain `bool` defaulting to `false`, so the
native side could not tell a component that is selectable but currently
unselected (`accessibilityState={{selected: false}}`) from one that is not
selectable at all (`accessibilityState={{}}`). Both arrived as `false`. The JS
type is already `selected?: ?boolean`, so this is the bridge discarding a value
the public API accepts.

Make `selected` a `std::optional<bool>` defaulting to `std::nullopt`.

  JS accessibilityState      native selected  (before -> after)
  {}                         false -> undefined
  {selected: false}          false -> false
  {selected: true}           true  -> true

This aligns the representation with ARIA, which `accessibilityState` mirrors:
the bool / tri-state split now tracks which ARIA attributes admit an undefined
value.

  field      ARIA value type  admits undefined  representation
  disabled   boolean          no                bool
  busy       boolean          no                bool
  selected   boolean          yes               std::optional<bool>  (changed)
  expanded   boolean          yes               std::optional<bool>
  checked    tristate         yes               CheckedState (None = unset)

That is also why `disabled` and `busy` stay plain `bool`: ARIA gives them no
undefined value, so there is no unset state to preserve. `expanded` was made
optional for this same reason in
react#40881 and `checked` has always
carried a `None`; `selected` was the outlier.

Nor is "unset" merely "absent" for this attribute. `testing-library/dom`
computes it as `boolean | undefined`, documented "false/true if (not)selected,
undefined if not selectable" -- the same shape, with the same meaning, that
this change introduces.

Host platforms need the distinction: on Windows a selectable component must
implement ISelectionItemProvider so UIA can report selection state, and with
the old representation every component carrying an accessibilityState looked
selectable.

iOS and Android rendering is unchanged. Trait derivation coalesces the optional
with `value_or(false)`, and the Android serializer omits the key when the value
is unset, which `BaseViewManager#setViewState` already handles by falling back
to `setSelected(false)`.

Reviewer note: `std::optional<bool>` is contextually convertible to `bool`, so a
bare `if (state.selected)` still compiles but tests engagement rather than
value, silently marking an explicitly unselected component as selected. There is
a regression test for that specific hazard.

Fixes react#46988
Supersedes react#47296, which went stale.

Changelog:
[General][Breaking] - `AccessibilityState::selected` is now `std::optional<bool>` in C++ props, preserving an unset `selected` instead of coercing it to `false`

Reviewed By: javache

Differential Revision: D120049025
@meta-codesync meta-codesync Bot changed the title Preserve unset accessibilityState.selected in C++ props Preserve unset accessibilityState.selected in C++ props (#58599) Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

In native code, there is no way to determine unset selected vs selected: false

1 participant