Skip to content

Commit cc149d7

Browse files
feat: add backdrop-filter js support
1 parent 83eb67a commit cc149d7

8 files changed

Lines changed: 764 additions & 0 deletions

File tree

‎packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export const filterAttribute: AnyAttributeType = nativeCSSParsing
4040
? true
4141
: {process: processFilter};
4242

43+
export const backdropFilterAttribute: AnyAttributeType = nativeCSSParsing
44+
? true
45+
: {process: processFilter};
46+
4347
export const boxShadowAttribute: AnyAttributeType = nativeCSSParsing
4448
? true
4549
: {process: processBoxShadow};
@@ -181,6 +185,11 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
181185
*/
182186
filter: filterAttribute,
183187

188+
/**
189+
* BackdropFilter
190+
*/
191+
backdropFilter: backdropFilterAttribute,
192+
184193
/**
185194
* MixBlendMode
186195
*/

‎packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {PartialViewConfigWithoutName} from './PlatformBaseViewConfig';
1212

1313
import ReactNativeStyleAttributes from '../Components/View/ReactNativeStyleAttributes';
1414
import {
15+
backdropFilterAttribute,
1516
backgroundImageAttribute,
1617
backgroundPositionAttribute,
1718
backgroundRepeatAttribute,
@@ -223,6 +224,7 @@ const validAttributesForNonEventProps = {
223224
backgroundRepeat: backgroundRepeatAttribute,
224225
experimental_backgroundRepeat: backgroundRepeatAttribute,
225226
boxShadow: boxShadowAttribute,
227+
backdropFilter: backdropFilterAttribute,
226228
filter: filterAttribute,
227229
mixBlendMode: true,
228230
isolation: true,

‎packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {PartialViewConfigWithoutName} from './PlatformBaseViewConfig';
1212

1313
import ReactNativeStyleAttributes from '../Components/View/ReactNativeStyleAttributes';
1414
import {
15+
backdropFilterAttribute,
1516
boxShadowAttribute,
1617
colorAttribute,
1718
filterAttribute,
@@ -251,6 +252,7 @@ const validAttributesForNonEventProps = {
251252
collapsable: true,
252253
collapsableChildren: true,
253254
filter: filterAttribute,
255+
backdropFilter: backdropFilterAttribute,
254256
boxShadow: boxShadowAttribute,
255257
mixBlendMode: true,
256258
isolation: true,

‎packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,7 @@ export type ____ViewStyle_InternalBase = Readonly<{
890890
cursor?: CursorValue,
891891
boxShadow?: ReadonlyArray<BoxShadowValue> | string,
892892
filter?: ReadonlyArray<FilterFunction> | string,
893+
backdropFilter?: ReadonlyArray<FilterFunction> | string,
893894
mixBlendMode?: ____BlendMode_Internal,
894895
backgroundImage?: ReadonlyArray<BackgroundImageValue> | string,
895896
experimental_backgroundImage?: ReadonlyArray<BackgroundImageValue> | string,
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
14+
15+
import type {FilterFunction} from '../StyleSheetTypes';
16+
17+
import processColor from '../processColor';
18+
19+
const processFilter = require('../processFilter').default;
20+
21+
describe('processBackdropFilter', () => {
22+
testStandardFilter('brightness');
23+
testStandardFilter('opacity');
24+
testStandardFilter('contrast');
25+
testStandardFilter('saturate');
26+
testStandardFilter('grayscale');
27+
testStandardFilter('sepia');
28+
testStandardFilter('invert');
29+
30+
testNumericFilter('blur', 5, [{blur: 5}]);
31+
testNumericFilter('blur', -5, []);
32+
testUnitFilter('blur', 5, '%', []);
33+
testUnitFilter('blur', 5, 'px', [{blur: 5}]);
34+
35+
testNumericFilter('hue-rotate', 0, [{hueRotate: 0}]);
36+
testUnitFilter('hue-rotate', 90, 'deg', [{hueRotate: 90}]);
37+
testUnitFilter('hue-rotate', 1.5708, 'rad', [
38+
{hueRotate: (180 * 1.5708) / Math.PI},
39+
]);
40+
testUnitFilter('hue-rotate', -90, 'deg', [{hueRotate: -90}]);
41+
testUnitFilter('hue-rotate', 1.5, 'grad', []);
42+
testNumericFilter('hue-rotate', 90, []);
43+
testUnitFilter('hue-rotate', 50, '%', []);
44+
45+
it('multiple filters (array)', () => {
46+
expect(
47+
processFilter([
48+
{brightness: 0.5},
49+
{opacity: 0.5},
50+
{blur: 5},
51+
{hueRotate: '90deg'},
52+
]),
53+
).toEqual([{brightness: 0.5}, {opacity: 0.5}, {blur: 5}, {hueRotate: 90}]);
54+
});
55+
56+
it('multiple filters (string)', () => {
57+
expect(
58+
processFilter('brightness(0.5) opacity(0.5) blur(5px) hue-rotate(90deg)'),
59+
).toEqual([{brightness: 0.5}, {opacity: 0.5}, {blur: 5}, {hueRotate: 90}]);
60+
});
61+
62+
it('multiple filters — one invalid returns []', () => {
63+
expect(
64+
processFilter([
65+
{brightness: 0.5},
66+
{opacity: 0.5},
67+
{blur: 5},
68+
{hueRotate: '90foo'},
69+
]),
70+
).toEqual([]);
71+
});
72+
73+
it('string multiple filters — one invalid returns []', () => {
74+
expect(
75+
processFilter('brightness(0.5) opacity(0.5) blur(5px) hue-rotate(90foo)'),
76+
).toEqual([]);
77+
});
78+
79+
it('string chained blur + saturate (percentage)', () => {
80+
expect(processFilter('blur(4px) saturate(150%)')).toEqual([
81+
{blur: 4},
82+
{saturate: 1.5},
83+
]);
84+
});
85+
86+
it('string chained blur + hue-rotate + invert', () => {
87+
expect(processFilter('blur(3px) hue-rotate(90deg) invert(20%)')).toEqual([
88+
{blur: 3},
89+
{hueRotate: 90},
90+
{invert: 0.2},
91+
]);
92+
});
93+
94+
it('drop-shadow: string 2-length (offsetX offsetY)', () => {
95+
expect(processFilter('drop-shadow(4px 4px)')).toEqual([
96+
{dropShadow: {offsetX: 4, offsetY: 4}},
97+
]);
98+
});
99+
100+
it('drop-shadow: string 3-length (offsetX offsetY blur)', () => {
101+
expect(processFilter('drop-shadow(4px 4px 8px)')).toEqual([
102+
{dropShadow: {offsetX: 4, offsetY: 4, standardDeviation: 8}},
103+
]);
104+
});
105+
106+
it('drop-shadow: string lengths + named color', () => {
107+
expect(processFilter('drop-shadow(4px 4px 10px blue)')).toEqual([
108+
{
109+
dropShadow: {
110+
offsetX: 4,
111+
offsetY: 4,
112+
standardDeviation: 10,
113+
color: processColor('blue'),
114+
},
115+
},
116+
]);
117+
});
118+
119+
it('drop-shadow: string color first', () => {
120+
expect(processFilter('drop-shadow(#ee2233 4px 4px 10px)')).toEqual([
121+
{
122+
dropShadow: {
123+
offsetX: 4,
124+
offsetY: 4,
125+
standardDeviation: 10,
126+
color: processColor('#ee2233'),
127+
},
128+
},
129+
]);
130+
});
131+
132+
it('drop-shadow: string negative offsets', () => {
133+
expect(processFilter('drop-shadow(-4px -4px)')).toEqual([
134+
{dropShadow: {offsetX: -4, offsetY: -4}},
135+
]);
136+
});
137+
138+
it('drop-shadow: string rgba color', () => {
139+
expect(
140+
processFilter('drop-shadow(4px 4px 10px rgba(0, 0, 0, 0.8))'),
141+
).toEqual([
142+
{
143+
dropShadow: {
144+
offsetX: 4,
145+
offsetY: 4,
146+
standardDeviation: 10,
147+
color: processColor('rgba(0, 0, 0, 0.8)'),
148+
},
149+
},
150+
]);
151+
});
152+
153+
it('drop-shadow: object form', () => {
154+
expect(
155+
processFilter([
156+
{
157+
dropShadow: {
158+
offsetX: 4,
159+
offsetY: 4,
160+
standardDeviation: '6px',
161+
color: 'rgba(0,0,0,0.6)',
162+
},
163+
},
164+
]),
165+
).toEqual([
166+
{
167+
dropShadow: {
168+
offsetX: 4,
169+
offsetY: 4,
170+
standardDeviation: 6,
171+
color: processColor('rgba(0,0,0,0.6)'),
172+
},
173+
},
174+
]);
175+
});
176+
177+
it('drop-shadow: negative blur radius is invalid', () => {
178+
expect(processFilter('drop-shadow(4px 4px -8px)')).toEqual([]);
179+
});
180+
181+
it('drop-shadow: comma-separated args are invalid', () => {
182+
expect(processFilter('drop-shadow(4px, 4px, 10px, red)')).toEqual([]);
183+
});
184+
185+
it('drop-shadow: chained with other filters', () => {
186+
expect(
187+
processFilter('drop-shadow(4px 4px 10px red) brightness(0.5)'),
188+
).toEqual([
189+
{
190+
dropShadow: {
191+
offsetX: 4,
192+
offsetY: 4,
193+
standardDeviation: 10,
194+
color: processColor('red'),
195+
},
196+
},
197+
{brightness: 0.5},
198+
]);
199+
});
200+
201+
it('url() reference is not supported and returns []', () => {
202+
// $FlowExpectedError[incompatible-call]
203+
expect(processFilter('url("filters.svg#filter")')).toEqual([]);
204+
});
205+
206+
it('url() mixed with valid filters returns []', () => {
207+
// $FlowExpectedError[incompatible-call]
208+
expect(
209+
processFilter('url("filters.svg#filter") blur(4px) saturate(150%)'),
210+
).toEqual([]);
211+
});
212+
213+
it('"inherit" is not a valid filter value and returns []', () => {
214+
// $FlowExpectedError[incompatible-call]
215+
expect(processFilter('inherit')).toEqual([]);
216+
});
217+
218+
it('"initial" is not a valid filter value and returns []', () => {
219+
// $FlowExpectedError[incompatible-call]
220+
expect(processFilter('initial')).toEqual([]);
221+
});
222+
223+
it('"revert" is not a valid filter value and returns []', () => {
224+
// $FlowExpectedError[incompatible-call]
225+
expect(processFilter('revert')).toEqual([]);
226+
});
227+
228+
it('"revert-layer" is not a valid filter value and returns []', () => {
229+
// $FlowExpectedError[incompatible-call]
230+
expect(processFilter('revert-layer')).toEqual([]);
231+
});
232+
233+
it('"unset" is not a valid filter value and returns []', () => {
234+
// $FlowExpectedError[incompatible-call]
235+
expect(processFilter('unset')).toEqual([]);
236+
});
237+
238+
it('empty array returns []', () => {
239+
expect(processFilter([])).toEqual([]);
240+
});
241+
242+
it('empty string returns []', () => {
243+
expect(processFilter('')).toEqual([]);
244+
});
245+
246+
it('unknown filter name returns []', () => {
247+
// $FlowExpectedError[incompatible-type]
248+
expect(processFilter([{foo: 5}])).toEqual([]);
249+
});
250+
251+
it('mixed case filter names are accepted', () => {
252+
expect(processFilter('bRiGhTnEsS(0.5) hUe-RoTaTe(90deg)')).toEqual([
253+
{brightness: 0.5},
254+
{hueRotate: 90},
255+
]);
256+
});
257+
});
258+
259+
function testStandardFilter(filter: string): void {
260+
const value = 0.5;
261+
const expected = createFilterPrimitive(filter, value);
262+
const percentExpected = createFilterPrimitive(filter, value / 100);
263+
264+
testNumericFilter(filter, value, [expected]);
265+
testNumericFilter(filter, -value, []);
266+
testUnitFilter(filter, value, 'px', [expected]);
267+
testUnitFilter(filter, value, '%', [percentExpected]);
268+
}
269+
270+
function testNumericFilter(
271+
filter: string,
272+
value: number,
273+
expected: Array<FilterFunction>,
274+
): void {
275+
const filterObject = createFilterPrimitive(filter, value);
276+
const filterString = filter + '(' + value.toString() + ')';
277+
278+
it(filterString, () => {
279+
expect(processFilter([filterObject])).toEqual(expected);
280+
});
281+
it('string ' + filterString, () => {
282+
expect(processFilter(filterString)).toEqual(expected);
283+
});
284+
}
285+
286+
function testUnitFilter(
287+
filter: string,
288+
value: number,
289+
unit: string,
290+
expected: Array<FilterFunction>,
291+
): void {
292+
const unitAmount = value + unit;
293+
const filterObject = createFilterPrimitive(filter, unitAmount);
294+
const filterString = filter + '(' + unitAmount + ')';
295+
296+
it(filterString, () => {
297+
expect(processFilter([filterObject])).toEqual(expected);
298+
});
299+
it('string ' + filterString, () => {
300+
expect(processFilter(filterString)).toEqual(expected);
301+
});
302+
}
303+
304+
function createFilterPrimitive(
305+
filter: string,
306+
value: number | string,
307+
): FilterFunction {
308+
switch (filter) {
309+
case 'brightness':
310+
return {brightness: value};
311+
case 'blur':
312+
return {blur: value};
313+
case 'contrast':
314+
return {contrast: value};
315+
case 'grayscale':
316+
return {grayscale: value};
317+
case 'hue-rotate':
318+
return {hueRotate: value};
319+
case 'invert':
320+
return {invert: value};
321+
case 'opacity':
322+
return {opacity: value};
323+
case 'saturate':
324+
return {saturate: value};
325+
case 'sepia':
326+
return {sepia: value};
327+
default:
328+
throw new Error('Invalid filter: ' + filter);
329+
}
330+
}

0 commit comments

Comments
 (0)