Skip to content

Commit 60d8b28

Browse files
adrcotfasmeta-codesync[bot]
authored andcommitted
feat(Pressable): add support for PlatformColor and alpha (#56395)
Summary: `android_ripple` on `Pressable` (and `TouchableNativeFeedback.Ripple()`) only accepted numeric (pre-processed ARGB int) colors. Passing a `PlatformColor` would throw an invariant in JS, making theme-aware ripple colors impossible. This PR removes those guards and wires up the full color pipeline: 1. **JS** (`useAndroidRippleForView.js`, `TouchableNativeFeedback.js`): removed the `invariant` that rejected non-numeric processed colors. `TouchableNativeFeedback.Ripple()` now accepts `ColorValue` instead of `string`. 2. **C++** (`NativeDrawable.h`, `HostPlatformViewProps.cpp`): `color` changed from `int32_t` to `SharedColor` + a separate `colorResourcePaths` field for `PlatformColor` resource paths. The `fromRawValue` pipeline now resolves both plain colors and `PlatformColor` objects. 3. **Kotlin** (`ReactDrawableHelper.kt`): `getColor()` now handles both numeric and `PlatformColor` map values via `ColorPropConverter.getColor()`, catching `JSApplicationCausedNativeException` to fall back gracefully instead of crashing. 4. **Theme change support** (`ReactViewGroup.kt`): the raw drawable description maps are retained on the view, and `onConfigurationChanged` re-applies them so ripple colors update automatically on light/dark mode switch. A new `alpha: number` (0–1) field is also added so users can apply opacity to a `PlatformColor` whose value is fully opaque and can't have alpha embedded at the JS level. It is applied by multiplying into the resolved color's alpha channel. Unresolvable `PlatformColor` resources are handled gracefully: on New Arch, C++ omits the `color` key so Kotlin falls back to `colorControlHighlight`; on Old Arch, `ReactDrawableHelper` catches the `JSApplicationCausedNativeException` and falls back to `colorControlHighlight` instead of crashing. ## Changelog: [ANDROID] [ADDED] - `android_ripple` now accepts `PlatformColor` for theme-aware ripple colors, and a new `alpha` (0–1) parameter for controlling ripple opacity independently of the color value. Pull Request resolved: #56395 Test Plan: **Automated:** - Existing Pressable snapshot tests pass unchanged. - Updated snapshot for `PlatformColor + alpha` to use `?attr/colorAccent` (matching the RNTester example). - Added snapshot test `should not crash with an unresolvable PlatformColor` verifying the component renders at the JS level without throwing. **Manual (RNTester → Pressable → "Pressable with PlatformColor ripple and alpha"):** 1. `PlatformColor('?attr/colorAccent'), no alpha` → full-opacity accent ripple 2. `PlatformColor('?attr/colorAccent'), alpha=0.3` → 30% opacity accent ripple 3. `#FF0000, no alpha` → full red ripple (regression check) 4. `#FF0000, alpha=0.5` → 50% opacity red ripple 5. Toggle system light/dark mode → ripple color updates automatically 6. Pass an intentionally invalid attribute (e.g. `?attr/doesNotExist`) → no crash, ripple falls back to default `colorControlHighlight` grey [Screen_recording_20260410_091000.webm](https://github.com/user-attachments/assets/5607d267-8cf1-4bbe-bad1-a80aac82d027) > [!NOTE] This is required in [React Native Paper](https://github.com/callstack/react-native-paper) to be able to fully support the Material Design specs but also useful as a general purpose feature for providing more flexibility when customizing ripple color. Reviewed By: javache Differential Revision: D105819708 Pulled By: fabriziocucci fbshipit-source-id: d27c43e0b4e619b6af1c8de0cdb2882db05ee33c
1 parent c6dd6f9 commit 60d8b28

17 files changed

Lines changed: 642 additions & 118 deletions

File tree

packages/react-native/Libraries/Components/Pressable/Pressable.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface PressableAndroidRippleConfig {
3030
borderless?: null | boolean | undefined;
3131
radius?: null | number | undefined;
3232
foreground?: null | boolean | undefined;
33+
alpha?: null | number | undefined;
3334
}
3435

3536
export interface PressableProps

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* @format
99
*/
1010

11+
import {PlatformColor} from '../../../StyleSheet/PlatformColorValueTypes';
12+
import Platform from '../../../Utilities/Platform';
1113
import {expectRendersMatchingSnapshot} from '../../../Utilities/ReactNativeTestTools';
1214
import View from '../../View/View';
1315
import Pressable from '../Pressable';
@@ -92,3 +94,68 @@ describe('<Pressable disabled={true} accessibilityState={{disabled: false}} />',
9294
);
9395
});
9496
});
97+
98+
describe('<Pressable android_ripple /> on Android', () => {
99+
let originalOS: string;
100+
101+
beforeEach(() => {
102+
originalOS = Platform.OS;
103+
/* $FlowFixMe[incompatible-type] */
104+
Platform.OS = 'android';
105+
});
106+
107+
afterEach(() => {
108+
/* $FlowFixMe[incompatible-type] */
109+
Platform.OS = originalOS;
110+
});
111+
112+
it('should set nativeBackgroundAndroid with numeric color and alpha', async () => {
113+
await expectRendersMatchingSnapshot(
114+
'Pressable',
115+
() => (
116+
<Pressable android_ripple={{color: '#FF0000', alpha: 0.5}}>
117+
<View />
118+
</Pressable>
119+
),
120+
() => {
121+
jest.dontMock('../Pressable');
122+
},
123+
);
124+
});
125+
126+
it('should set nativeBackgroundAndroid with PlatformColor and alpha', async () => {
127+
await expectRendersMatchingSnapshot(
128+
'Pressable',
129+
() => (
130+
<Pressable
131+
android_ripple={{
132+
color: PlatformColor('?attr/colorAccent'),
133+
alpha: 0.3,
134+
}}>
135+
<View />
136+
</Pressable>
137+
),
138+
() => {
139+
jest.dontMock('../Pressable');
140+
},
141+
);
142+
});
143+
144+
it('should not crash with an unresolvable PlatformColor', async () => {
145+
await expectRendersMatchingSnapshot(
146+
'Pressable',
147+
() => (
148+
<Pressable
149+
android_ripple={{
150+
color: PlatformColor('?attr/doesNotExist'),
151+
alpha: 0.5,
152+
}}>
153+
<View />
154+
</Pressable>
155+
),
156+
() => {
157+
jest.dontMock('../Pressable');
158+
},
159+
);
160+
});
161+
});

packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,292 @@ exports[`<Pressable /> should render as expected: should deep render when not mo
7272
</View>
7373
`;
7474

75+
exports[`<Pressable android_ripple /> on Android should not crash with an unresolvable PlatformColor: should deep render when mocked (please verify output manually) 1`] = `
76+
<View
77+
accessibilityState={
78+
Object {
79+
"busy": undefined,
80+
"checked": undefined,
81+
"disabled": undefined,
82+
"expanded": undefined,
83+
"selected": undefined,
84+
}
85+
}
86+
accessibilityValue={
87+
Object {
88+
"max": undefined,
89+
"min": undefined,
90+
"now": undefined,
91+
"text": undefined,
92+
}
93+
}
94+
accessible={true}
95+
collapsable={false}
96+
focusable={true}
97+
nativeBackgroundAndroid={
98+
Object {
99+
"alpha": 0.5,
100+
"borderless": false,
101+
"color": Object {
102+
"semantic": Array [
103+
"?attr/doesNotExist",
104+
],
105+
},
106+
"rippleRadius": undefined,
107+
"type": "RippleAndroid",
108+
}
109+
}
110+
onBlur={[Function]}
111+
onClick={[Function]}
112+
onFocus={[Function]}
113+
onResponderGrant={[Function]}
114+
onResponderMove={[Function]}
115+
onResponderRelease={[Function]}
116+
onResponderTerminate={[Function]}
117+
onResponderTerminationRequest={[Function]}
118+
onStartShouldSetResponder={[Function]}
119+
>
120+
<View />
121+
</View>
122+
`;
123+
124+
exports[`<Pressable android_ripple /> on Android should not crash with an unresolvable PlatformColor: should deep render when not mocked (please verify output manually) 1`] = `
125+
<View
126+
accessibilityState={
127+
Object {
128+
"busy": undefined,
129+
"checked": undefined,
130+
"disabled": undefined,
131+
"expanded": undefined,
132+
"selected": undefined,
133+
}
134+
}
135+
accessibilityValue={
136+
Object {
137+
"max": undefined,
138+
"min": undefined,
139+
"now": undefined,
140+
"text": undefined,
141+
}
142+
}
143+
accessible={true}
144+
collapsable={false}
145+
focusable={true}
146+
nativeBackgroundAndroid={
147+
Object {
148+
"alpha": 0.5,
149+
"borderless": false,
150+
"color": Object {
151+
"semantic": Array [
152+
"?attr/doesNotExist",
153+
],
154+
},
155+
"rippleRadius": undefined,
156+
"type": "RippleAndroid",
157+
}
158+
}
159+
onBlur={[Function]}
160+
onClick={[Function]}
161+
onFocus={[Function]}
162+
onResponderGrant={[Function]}
163+
onResponderMove={[Function]}
164+
onResponderRelease={[Function]}
165+
onResponderTerminate={[Function]}
166+
onResponderTerminationRequest={[Function]}
167+
onStartShouldSetResponder={[Function]}
168+
>
169+
<View />
170+
</View>
171+
`;
172+
173+
exports[`<Pressable android_ripple /> on Android should set nativeBackgroundAndroid with PlatformColor and alpha: should deep render when mocked (please verify output manually) 1`] = `
174+
<View
175+
accessibilityState={
176+
Object {
177+
"busy": undefined,
178+
"checked": undefined,
179+
"disabled": undefined,
180+
"expanded": undefined,
181+
"selected": undefined,
182+
}
183+
}
184+
accessibilityValue={
185+
Object {
186+
"max": undefined,
187+
"min": undefined,
188+
"now": undefined,
189+
"text": undefined,
190+
}
191+
}
192+
accessible={true}
193+
collapsable={false}
194+
focusable={true}
195+
nativeBackgroundAndroid={
196+
Object {
197+
"alpha": 0.3,
198+
"borderless": false,
199+
"color": Object {
200+
"semantic": Array [
201+
"?attr/colorAccent",
202+
],
203+
},
204+
"rippleRadius": undefined,
205+
"type": "RippleAndroid",
206+
}
207+
}
208+
onBlur={[Function]}
209+
onClick={[Function]}
210+
onFocus={[Function]}
211+
onResponderGrant={[Function]}
212+
onResponderMove={[Function]}
213+
onResponderRelease={[Function]}
214+
onResponderTerminate={[Function]}
215+
onResponderTerminationRequest={[Function]}
216+
onStartShouldSetResponder={[Function]}
217+
>
218+
<View />
219+
</View>
220+
`;
221+
222+
exports[`<Pressable android_ripple /> on Android should set nativeBackgroundAndroid with PlatformColor and alpha: should deep render when not mocked (please verify output manually) 1`] = `
223+
<View
224+
accessibilityState={
225+
Object {
226+
"busy": undefined,
227+
"checked": undefined,
228+
"disabled": undefined,
229+
"expanded": undefined,
230+
"selected": undefined,
231+
}
232+
}
233+
accessibilityValue={
234+
Object {
235+
"max": undefined,
236+
"min": undefined,
237+
"now": undefined,
238+
"text": undefined,
239+
}
240+
}
241+
accessible={true}
242+
collapsable={false}
243+
focusable={true}
244+
nativeBackgroundAndroid={
245+
Object {
246+
"alpha": 0.3,
247+
"borderless": false,
248+
"color": Object {
249+
"semantic": Array [
250+
"?attr/colorAccent",
251+
],
252+
},
253+
"rippleRadius": undefined,
254+
"type": "RippleAndroid",
255+
}
256+
}
257+
onBlur={[Function]}
258+
onClick={[Function]}
259+
onFocus={[Function]}
260+
onResponderGrant={[Function]}
261+
onResponderMove={[Function]}
262+
onResponderRelease={[Function]}
263+
onResponderTerminate={[Function]}
264+
onResponderTerminationRequest={[Function]}
265+
onStartShouldSetResponder={[Function]}
266+
>
267+
<View />
268+
</View>
269+
`;
270+
271+
exports[`<Pressable android_ripple /> on Android should set nativeBackgroundAndroid with numeric color and alpha: should deep render when mocked (please verify output manually) 1`] = `
272+
<View
273+
accessibilityState={
274+
Object {
275+
"busy": undefined,
276+
"checked": undefined,
277+
"disabled": undefined,
278+
"expanded": undefined,
279+
"selected": undefined,
280+
}
281+
}
282+
accessibilityValue={
283+
Object {
284+
"max": undefined,
285+
"min": undefined,
286+
"now": undefined,
287+
"text": undefined,
288+
}
289+
}
290+
accessible={true}
291+
collapsable={false}
292+
focusable={true}
293+
nativeBackgroundAndroid={
294+
Object {
295+
"alpha": 0.5,
296+
"borderless": false,
297+
"color": -65536,
298+
"rippleRadius": undefined,
299+
"type": "RippleAndroid",
300+
}
301+
}
302+
onBlur={[Function]}
303+
onClick={[Function]}
304+
onFocus={[Function]}
305+
onResponderGrant={[Function]}
306+
onResponderMove={[Function]}
307+
onResponderRelease={[Function]}
308+
onResponderTerminate={[Function]}
309+
onResponderTerminationRequest={[Function]}
310+
onStartShouldSetResponder={[Function]}
311+
>
312+
<View />
313+
</View>
314+
`;
315+
316+
exports[`<Pressable android_ripple /> on Android should set nativeBackgroundAndroid with numeric color and alpha: should deep render when not mocked (please verify output manually) 1`] = `
317+
<View
318+
accessibilityState={
319+
Object {
320+
"busy": undefined,
321+
"checked": undefined,
322+
"disabled": undefined,
323+
"expanded": undefined,
324+
"selected": undefined,
325+
}
326+
}
327+
accessibilityValue={
328+
Object {
329+
"max": undefined,
330+
"min": undefined,
331+
"now": undefined,
332+
"text": undefined,
333+
}
334+
}
335+
accessible={true}
336+
collapsable={false}
337+
focusable={true}
338+
nativeBackgroundAndroid={
339+
Object {
340+
"alpha": 0.5,
341+
"borderless": false,
342+
"color": -65536,
343+
"rippleRadius": undefined,
344+
"type": "RippleAndroid",
345+
}
346+
}
347+
onBlur={[Function]}
348+
onClick={[Function]}
349+
onFocus={[Function]}
350+
onResponderGrant={[Function]}
351+
onResponderMove={[Function]}
352+
onResponderRelease={[Function]}
353+
onResponderTerminate={[Function]}
354+
onResponderTerminationRequest={[Function]}
355+
onStartShouldSetResponder={[Function]}
356+
>
357+
<View />
358+
</View>
359+
`;
360+
75361
exports[`<Pressable disabled={true} /> should be disabled when disabled is true: should deep render when mocked (please verify output manually) 1`] = `
76362
<View
77363
accessibilityState={

0 commit comments

Comments
 (0)