Skip to content

Commit da1f2de

Browse files
martinboothnecolas
andauthored
Add support for setting a custom viewport width (#344)
* Add support for setting a custom viewport width * Iterate on viewport scaling polyfill * Add scaling for default 'em' unit polyfill. * Move scale context provider into context file. * Expand list of length style properties. --------- Co-authored-by: Nicolas Gallagher <necolas@meta.com>
1 parent c5251b8 commit da1f2de

15 files changed

Lines changed: 354 additions & 28 deletions

File tree

apps/examples/src/components/App.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const themedStyles = css.create({
5454
container: {
5555
display: 'flex',
5656
flex: 1,
57+
flexDirection: 'column',
5758
justifyContent: 'center',
5859
backgroundColor: '#bbb',
5960
padding: 8

packages/benchmarks/perf/tests/css-props-tests.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function runSuite(opts) {
2020
hover: true,
2121
inheritedFontSize: 16,
2222
viewportHeight: 600,
23+
viewportScale: 1,
2324
viewportWidth: 1024
2425
};
2526

@@ -29,6 +30,7 @@ function runSuite(opts) {
2930
hover: true,
3031
inheritedFontSize: 16,
3132
viewportHeight: 600,
33+
viewportScale: 1,
3234
viewportWidth: 1024
3335
};
3436

packages/react-strict-dom/src/native/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as compat from './compat';
2222
import * as html from './html';
2323
import * as stylex from './stylex';
2424
import { ProvideCustomProperties } from './modules/ContextCustomProperties';
25+
import { ProvideViewportScale } from './modules/ContextViewportScale';
2526

2627
type StyleTheme<V, T> = Theme<V, T>;
2728
type StyleVars<T> = VarGroup<T>;
@@ -48,7 +49,8 @@ function ThemeProvider(props: ProviderProps): React.Node {
4849
}
4950

5051
const contexts = {
51-
ThemeProvider: ThemeProvider as typeof ThemeProvider
52+
ThemeProvider: ThemeProvider as typeof ThemeProvider,
53+
ViewportProvider: ProvideViewportScale as typeof ProvideViewportScale
5254
};
5355

5456
// Export using StyleX types as the shim has divergent types internally.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
*/
9+
10+
import * as React from 'react';
11+
import * as ReactNative from '../react-native';
12+
13+
type Value = $ReadOnly<{
14+
scale: number
15+
}>;
16+
17+
type ProviderProps = $ReadOnly<{
18+
children: React.Node,
19+
viewportWidth: number
20+
}>;
21+
22+
const defaultContext = { scale: 1 };
23+
const ContextViewportScale: React.Context<Value> =
24+
React.createContext(defaultContext);
25+
26+
if (__DEV__) {
27+
ContextViewportScale.displayName = 'ContextViewportScale';
28+
}
29+
30+
export function ProvideViewportScale({
31+
viewportWidth: logicalViewportWidth,
32+
children
33+
}: ProviderProps): React.Node {
34+
const { width: viewportWidth } = ReactNative.useWindowDimensions();
35+
36+
const viewportScale = React.useMemo(
37+
() => ({
38+
scale: viewportWidth / logicalViewportWidth
39+
}),
40+
[logicalViewportWidth, viewportWidth]
41+
);
42+
43+
return (
44+
<ContextViewportScale.Provider value={viewportScale}>
45+
{children}
46+
</ContextViewportScale.Provider>
47+
);
48+
}
49+
50+
export function useViewportScale(): Value {
51+
return React.useContext(ContextViewportScale);
52+
}

packages/react-strict-dom/src/native/modules/useStrictDOMElement.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @flow strict
7+
* @flow strict-local
88
*/
99

1010
import type { CallbackRef } from '../../types/react';
1111

1212
import * as React from 'react';
1313

14-
import { useElementCallback } from '../../shared/useElementCallback';
1514
import { errorMsg } from '../../shared/logUtils';
15+
import { useElementCallback } from '../../shared/useElementCallback';
16+
import { useViewportScale } from './ContextViewportScale';
1617

1718
function errorUnimplemented(name: string) {
1819
if (__DEV__) {
@@ -33,6 +34,7 @@ type Options = {
3334
};
3435

3536
export function useStrictDOMElement<T>({ tagName }: Options): CallbackRef<T> {
37+
const { scale: viewportScale } = useViewportScale();
3638
const elementCallback = useElementCallback(
3739
React.useCallback(
3840
// $FlowFixMe[unclear-type]
@@ -83,6 +85,20 @@ export function useStrictDOMElement<T>({ tagName }: Options): CallbackRef<T> {
8385
};
8486
}
8587

88+
if (viewportScale !== 1) {
89+
const getBoundingClientRect = node.getBoundingClientRect;
90+
node.getBoundingClientRect = function () {
91+
const rect = getBoundingClientRect.call(node);
92+
93+
return new DOMRect(
94+
rect.x / viewportScale,
95+
rect.y / viewportScale,
96+
rect.width / viewportScale,
97+
rect.height / viewportScale
98+
);
99+
};
100+
}
101+
86102
const { getRootNode } = node;
87103
if (getRootNode == null) {
88104
node.getRootNode = () => errorUnimplemented('getRootNode');
@@ -157,7 +173,7 @@ export function useStrictDOMElement<T>({ tagName }: Options): CallbackRef<T> {
157173
}
158174
}
159175
},
160-
[tagName]
176+
[tagName, viewportScale]
161177
)
162178
);
163179

packages/react-strict-dom/src/native/modules/useStyleProps.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { flattenStyle } from './flattenStyle';
2020
import { useInheritedStyles } from './ContextInheritedStyles';
2121
import { usePseudoStates } from './usePseudoStates';
2222
import { useStyleTransition } from './useStyleTransition';
23+
import { useViewportScale } from './ContextViewportScale';
2324

2425
type StyleOptions = {
2526
customProperties: ?CustomProperties,
@@ -66,6 +67,7 @@ export function useStyleProps(
6667

6768
const { fontScale, height, width } = ReactNative.useWindowDimensions();
6869
const colorScheme = ReactNative.useColorScheme();
70+
const { scale: viewportScale } = useViewportScale();
6971

7072
// These values are already computed
7173
const {
@@ -104,6 +106,7 @@ export function useStyleProps(
104106
inheritedFontSize:
105107
typeof inheritedFontSize === 'number' ? inheritedFontSize : undefined,
106108
viewportHeight: height,
109+
viewportScale,
107110
viewportWidth: width
108111
},
109112
flatStyle as $FlowFixMe

packages/react-strict-dom/src/native/stylex/CSSLengthUnitValue.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@ type ResolvePixelValueOptions = $ReadOnly<{
1717
fontScale: number | void,
1818
inheritedFontSize: ?number,
1919
viewportHeight: number,
20+
viewportScale: number,
2021
viewportWidth: number
2122
}>;
2223

23-
type ParsedValue = [+value: number, +unit: CSSLengthUnitType] | null;
24-
25-
const memoizedValues = new Map<string, ParsedValue>();
24+
const memoizedValues = new Map<string, CSSLengthUnitValue | null>();
2625

2726
// TODO: this only works on simple values
2827
export class CSSLengthUnitValue {
29-
static parse(input: string): ParsedValue {
28+
static parse(input: string): CSSLengthUnitValue | null {
3029
const memoizedValue = memoizedValues.get(input);
3130
if (memoizedValue !== undefined) {
3231
return memoizedValue;
@@ -40,9 +39,9 @@ export class CSSLengthUnitValue {
4039
const value = match[1];
4140
const unit: $FlowFixMe = match[2];
4241
const parsedFloat: number = parseFloat(value);
43-
const parsedValue: ParsedValue = [parsedFloat, unit];
44-
memoizedValues.set(input, parsedValue);
45-
return parsedValue;
42+
const cssLengthUnitValue = new CSSLengthUnitValue(parsedFloat, unit);
43+
memoizedValues.set(input, cssLengthUnitValue);
44+
return cssLengthUnitValue;
4645
}
4746

4847
value: number;
@@ -55,27 +54,28 @@ export class CSSLengthUnitValue {
5554

5655
resolvePixelValue(options: ResolvePixelValueOptions): number {
5756
const {
58-
viewportWidth,
59-
viewportHeight,
6057
fontScale = 1,
61-
inheritedFontSize
58+
inheritedFontSize,
59+
viewportHeight,
60+
viewportScale,
61+
viewportWidth
6262
} = options;
6363
const unit = this.unit;
6464
const value = this.value;
6565
const valuePercent = value / 100;
6666
switch (unit) {
6767
case 'em': {
6868
if (inheritedFontSize == null) {
69-
return fontScale * 16 * value;
69+
return fontScale * 16 * value * viewportScale;
7070
} else {
7171
return inheritedFontSize * value;
7272
}
7373
}
7474
case 'px': {
75-
return value;
75+
return value * viewportScale;
7676
}
7777
case 'rem': {
78-
return fontScale * 16 * value;
78+
return fontScale * 16 * value * viewportScale;
7979
}
8080
case 'vh': {
8181
return viewportHeight * valuePercent;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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
8+
*/
9+
10+
import type { ReactNativeTransform } from '../../types/renderer.native';
11+
12+
export class CSSTransformValue {
13+
value: $ReadOnlyArray<ReactNativeTransform>;
14+
cachedScaledTransform: void | {
15+
viewportScale: number,
16+
value: $ReadOnlyArray<ReactNativeTransform>
17+
};
18+
19+
constructor(value: $ReadOnlyArray<ReactNativeTransform>) {
20+
this.value = value;
21+
}
22+
23+
resolveTransformValue(
24+
viewportScale: number
25+
): $ReadOnlyArray<ReactNativeTransform> {
26+
if (viewportScale === 1) {
27+
return this.value;
28+
}
29+
if (
30+
this.cachedScaledTransform != null &&
31+
this.cachedScaledTransform.viewportScale === viewportScale
32+
) {
33+
return this.cachedScaledTransform.value;
34+
}
35+
36+
const scaledTransform = this.value.map((transform) => {
37+
if (
38+
transform.translateX != null &&
39+
typeof transform.translateX === 'number'
40+
) {
41+
return { translateX: transform.translateX * viewportScale };
42+
}
43+
if (
44+
transform.translateY != null &&
45+
typeof transform.translateY === 'number'
46+
) {
47+
return { translateY: transform.translateY * viewportScale };
48+
}
49+
return transform;
50+
});
51+
52+
this.cachedScaledTransform = {
53+
viewportScale,
54+
value: scaledTransform
55+
};
56+
57+
return scaledTransform;
58+
}
59+
}

packages/react-strict-dom/src/native/stylex/__tests__/parseTransform-test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { parseTransform } from '../parseTransform';
8+
import { parseTransform as parseTransformImpl } from '../parseTransform';
9+
10+
function parseTransform(transform) {
11+
return parseTransformImpl(transform).resolveTransformValue(1);
12+
}
913

1014
describe('parseTransform', () => {
1115
test('perspective', () => {

packages/react-strict-dom/src/native/stylex/index.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ import type {
1919
} from '../../types/styles';
2020

2121
import { CSSLengthUnitValue } from './CSSLengthUnitValue';
22+
import { CSSTransformValue } from './CSSTransformValue';
2223
import { CSSUnparsedValue } from './typed-om/CSSUnparsedValue';
2324
import { errorMsg, warnMsg } from '../../shared/logUtils';
2425
import { fixContentBox } from './fixContentBox';
2526
import { flattenStyle } from './flattenStyleXStyles';
2627
import { isAllowedShortFormValue } from './isAllowedShortFormValue';
2728
import { isAllowedStyleKey } from './isAllowedStyleKey';
29+
import { lengthStyleKeySet } from './isLengthStyleKey';
2830
import { mediaQueryMatches } from './mediaQueryMatches';
2931
import { parseTextShadow } from './parseTextShadow';
3032
import { parseTimeValue } from './parseTimeValue';
@@ -44,6 +46,7 @@ type ResolveStyleOptions = $ReadOnly<{
4446
hover?: ?boolean,
4547
inheritedFontSize: ?number,
4648
viewportHeight: number,
49+
viewportScale: number,
4750
viewportWidth: number,
4851
writingDirection?: ?'ltr' | 'rtl'
4952
}>;
@@ -197,7 +200,7 @@ function processStyle(
197200

198201
const maybeLengthUnitValue = CSSLengthUnitValue.parse(styleValue);
199202
if (maybeLengthUnitValue != null) {
200-
result[propName] = new CSSLengthUnitValue(...maybeLengthUnitValue);
203+
result[propName] = maybeLengthUnitValue;
201204
continue;
202205
// React Native doesn't support these keywords or functions
203206
} else if (styleValue === 'inherit' || styleValue === 'unset') {
@@ -295,6 +298,7 @@ function resolveStyle(
295298
hover,
296299
inheritedFontSize,
297300
viewportHeight,
301+
viewportScale,
298302
viewportWidth
299303
} = options;
300304
const colorScheme = options.colorScheme || 'light';
@@ -331,6 +335,7 @@ function resolveStyle(
331335
fontScale,
332336
inheritedFontSize: inheritedFontSize,
333337
viewportHeight,
338+
viewportScale,
334339
viewportWidth
335340
});
336341
continue;
@@ -343,6 +348,7 @@ function resolveStyle(
343348
fontScale,
344349
inheritedFontSize: fontSize,
345350
viewportHeight,
351+
viewportScale,
346352
viewportWidth
347353
});
348354
} else {
@@ -352,6 +358,19 @@ function resolveStyle(
352358
}
353359
}
354360

361+
if (styleValue instanceof CSSTransformValue) {
362+
result[propName] = styleValue.resolveTransformValue(viewportScale);
363+
continue;
364+
}
365+
if (
366+
viewportScale !== 1 &&
367+
typeof styleValue === 'number' &&
368+
lengthStyleKeySet.has(propName)
369+
) {
370+
result[propName] = styleValue * viewportScale;
371+
continue;
372+
}
373+
355374
// Resolve the stylex object-value syntax
356375
if (
357376
styleValue != null &&

0 commit comments

Comments
 (0)