Skip to content

Commit ae4d91d

Browse files
Calvin Liumeta-codesync[bot]
authored andcommitted
Preserve unset accessibilityState.selected in C++ props
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<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 #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
1 parent 3718f62 commit ae4d91d

34 files changed

Lines changed: 368 additions & 95 deletions

packages/react-native/Libraries/Components/Pressable/__tests__/Pressable-itest.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('<Pressable>', () => {
4646
expect(root.getRenderedOutput().toJSX()).toEqual(
4747
<rn-view
4848
accessible="true"
49-
accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:null}"
49+
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
5050
backgroundColor="rgba(0, 0, 255, 1)"
5151
borderWidth="3"
5252
height="50"
@@ -70,7 +70,7 @@ describe('<Pressable>', () => {
7070
expect(root.getRenderedOutput().toJSX()).toEqual(
7171
<rn-view
7272
accessible="true"
73-
accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:null}"
73+
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
7474
backgroundColor="rgba(128, 128, 128, 1)"
7575
/>,
7676
);
@@ -134,7 +134,7 @@ describe('<Pressable>', () => {
134134
expect(
135135
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
136136
).toEqual(
137-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
137+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
138138
);
139139
});
140140

@@ -148,7 +148,7 @@ describe('<Pressable>', () => {
148148
expect(
149149
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
150150
).toEqual(
151-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
151+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
152152
);
153153
});
154154

@@ -164,7 +164,7 @@ describe('<Pressable>', () => {
164164
expect(
165165
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
166166
).toEqual(
167-
<rn-view accessibilityState="{disabled:true,selected:false,checked:Checked,busy:false,expanded:null}" />,
167+
<rn-view accessibilityState="{disabled:true,selected:null,checked:Checked,busy:false,expanded:null}" />,
168168
);
169169
});
170170

@@ -183,7 +183,7 @@ describe('<Pressable>', () => {
183183
expect(
184184
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
185185
).toEqual(
186-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
186+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
187187
);
188188
});
189189
});
@@ -201,7 +201,7 @@ describe('<Pressable>', () => {
201201
expect(root.getRenderedOutput().toJSX()).toEqual(
202202
<rn-view
203203
accessible="true"
204-
accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:null}"
204+
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
205205
/>,
206206
);
207207
});
@@ -223,7 +223,7 @@ describe('<Pressable>', () => {
223223
expect(root.getRenderedOutput().toJSX()).toEqual(
224224
<rn-view
225225
accessible="true"
226-
accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:null}"
226+
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
227227
/>,
228228
);
229229
});
@@ -245,7 +245,7 @@ describe('<Pressable>', () => {
245245
expect(root.getRenderedOutput().toJSX()).toEqual(
246246
<rn-view
247247
accessible="true"
248-
accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:null}"
248+
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
249249
/>,
250250
);
251251
});
@@ -270,7 +270,7 @@ describe('<Pressable>', () => {
270270
expect(root.getRenderedOutput().toJSX()).toEqual(
271271
<rn-view
272272
accessible="true"
273-
accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:null}">
273+
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}">
274274
<rn-paragraph
275275
allowFontScaling="true"
276276
ellipsizeMode="tail"

packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('Switch', () => {
4242
expect(
4343
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
4444
).toEqual(
45-
<rn-androidSwitch accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
45+
<rn-androidSwitch accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
4646
);
4747
});
4848

packages/react-native/Libraries/Components/Touchable/__tests__/TouchableHighlight-itest.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ describe('<TouchableHighlight>', () => {
285285
expect(
286286
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
287287
).toEqual(
288-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
288+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
289289
);
290290
});
291291

@@ -303,7 +303,7 @@ describe('<TouchableHighlight>', () => {
303303
expect(
304304
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
305305
).toEqual(
306-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
306+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
307307
);
308308
});
309309

@@ -323,7 +323,7 @@ describe('<TouchableHighlight>', () => {
323323
expect(
324324
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
325325
).toEqual(
326-
<rn-view accessibilityState="{disabled:true,selected:false,checked:Checked,busy:false,expanded:null}" />,
326+
<rn-view accessibilityState="{disabled:true,selected:null,checked:Checked,busy:false,expanded:null}" />,
327327
);
328328
});
329329

@@ -343,7 +343,7 @@ describe('<TouchableHighlight>', () => {
343343
expect(
344344
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
345345
).toEqual(
346-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
346+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
347347
);
348348
});
349349

@@ -367,7 +367,7 @@ describe('<TouchableHighlight>', () => {
367367
expect(
368368
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
369369
).toEqual(
370-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
370+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
371371
);
372372

373373
const element = nullthrows(elementRef.current);

packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-itest.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe('<TouchableNativeFeedback>', () => {
5454
expect(root.getRenderedOutput().toJSX()).toEqual(
5555
<rn-view
5656
accessible="true"
57-
accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:null}"
57+
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
5858
/>,
5959
);
6060
});
@@ -75,7 +75,7 @@ describe('<TouchableNativeFeedback>', () => {
7575
expect(
7676
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
7777
).toEqual(
78-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
78+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
7979
);
8080
});
8181

@@ -93,7 +93,7 @@ describe('<TouchableNativeFeedback>', () => {
9393
expect(
9494
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
9595
).toEqual(
96-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
96+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
9797
);
9898
});
9999

@@ -113,7 +113,7 @@ describe('<TouchableNativeFeedback>', () => {
113113
expect(
114114
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
115115
).toEqual(
116-
<rn-view accessibilityState="{disabled:true,selected:false,checked:Checked,busy:false,expanded:null}" />,
116+
<rn-view accessibilityState="{disabled:true,selected:null,checked:Checked,busy:false,expanded:null}" />,
117117
);
118118
});
119119

@@ -133,7 +133,7 @@ describe('<TouchableNativeFeedback>', () => {
133133
expect(
134134
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
135135
).toEqual(
136-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
136+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
137137
);
138138
});
139139

@@ -153,7 +153,7 @@ describe('<TouchableNativeFeedback>', () => {
153153
expect(
154154
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
155155
).toEqual(
156-
<rn-view accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:null}" />,
156+
<rn-view accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}" />,
157157
);
158158
});
159159
});

packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-itest.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ describe('<TouchableOpacity>', () => {
155155
expect(
156156
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
157157
).toEqual(
158-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
158+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
159159
);
160160
});
161161

@@ -171,7 +171,7 @@ describe('<TouchableOpacity>', () => {
171171
expect(
172172
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
173173
).toEqual(
174-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
174+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
175175
);
176176
});
177177
});

packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-itest.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe('<TouchableWithoutFeedback>', () => {
6363
expect(
6464
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
6565
).toEqual(
66-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
66+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
6767
);
6868
});
6969

@@ -81,7 +81,7 @@ describe('<TouchableWithoutFeedback>', () => {
8181
expect(
8282
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
8383
).toEqual(
84-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
84+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
8585
);
8686
});
8787

@@ -101,7 +101,7 @@ describe('<TouchableWithoutFeedback>', () => {
101101
expect(
102102
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
103103
).toEqual(
104-
<rn-view accessibilityState="{disabled:true,selected:false,checked:Checked,busy:false,expanded:null}" />,
104+
<rn-view accessibilityState="{disabled:true,selected:null,checked:Checked,busy:false,expanded:null}" />,
105105
);
106106
});
107107

@@ -121,7 +121,7 @@ describe('<TouchableWithoutFeedback>', () => {
121121
expect(
122122
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
123123
).toEqual(
124-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
124+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
125125
);
126126
});
127127
});

packages/react-native/Libraries/Components/View/__tests__/View-itest.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ describe('<View>', () => {
854854
expect(
855855
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
856856
).toEqual(
857-
<rn-view accessibilityState="{disabled:false,selected:false,checked:None,busy:true,expanded:null}" />,
857+
<rn-view accessibilityState="{disabled:false,selected:null,checked:None,busy:true,expanded:null}" />,
858858
);
859859
});
860860

@@ -866,7 +866,7 @@ describe('<View>', () => {
866866
expect(
867867
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
868868
).toEqual(
869-
<rn-view accessibilityState="{disabled:false,selected:false,checked:None,busy:true,expanded:null}" />,
869+
<rn-view accessibilityState="{disabled:false,selected:null,checked:None,busy:true,expanded:null}" />,
870870
);
871871
Fantom.runTask(() => {
872872
root.render(<View accessible={true} />);
@@ -888,7 +888,7 @@ describe('<View>', () => {
888888
expect(
889889
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
890890
).toEqual(
891-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
891+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
892892
);
893893
});
894894

@@ -900,7 +900,7 @@ describe('<View>', () => {
900900
expect(
901901
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
902902
).toEqual(
903-
<rn-view accessibilityState="{disabled:true,selected:false,checked:None,busy:false,expanded:null}" />,
903+
<rn-view accessibilityState="{disabled:true,selected:null,checked:None,busy:false,expanded:null}" />,
904904
);
905905
Fantom.runTask(() => {
906906
root.render(<View accessible={true} />);
@@ -922,7 +922,7 @@ describe('<View>', () => {
922922
expect(
923923
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
924924
).toEqual(
925-
<rn-view accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:true}" />,
925+
<rn-view accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:true}" />,
926926
);
927927
});
928928

@@ -934,7 +934,7 @@ describe('<View>', () => {
934934
expect(
935935
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
936936
).toEqual(
937-
<rn-view accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:true}" />,
937+
<rn-view accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:true}" />,
938938
);
939939
Fantom.runTask(() => {
940940
root.render(<View accessible={true} />);
@@ -990,7 +990,7 @@ describe('<View>', () => {
990990
expect(
991991
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
992992
).toEqual(
993-
<rn-view accessibilityState="{disabled:false,selected:false,checked:Checked,busy:false,expanded:null}" />,
993+
<rn-view accessibilityState="{disabled:false,selected:null,checked:Checked,busy:false,expanded:null}" />,
994994
);
995995
});
996996

@@ -1002,7 +1002,7 @@ describe('<View>', () => {
10021002
expect(
10031003
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
10041004
).toEqual(
1005-
<rn-view accessibilityState="{disabled:false,selected:false,checked:Checked,busy:false,expanded:null}" />,
1005+
<rn-view accessibilityState="{disabled:false,selected:null,checked:Checked,busy:false,expanded:null}" />,
10061006
);
10071007
Fantom.runTask(() => {
10081008
root.render(<View accessible={true} />);

packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxButton-itest.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
exports[`LogBoxButton should render Pressable and pass through props 1`] = `
44
<rn-view
5-
accessibilityState="{disabled:false,selected:false,checked:None,busy:false,expanded:null}"
5+
accessibilityState="{disabled:false,selected:null,checked:None,busy:false,expanded:null}"
66
accessible="true"
77
backgroundColor="rgba(0, 0, 0, 1)"
88
>

0 commit comments

Comments
 (0)