From 06dfb42d200d4abd9e8822197dafed20f45876a0 Mon Sep 17 00:00:00 2001 From: Martin Booth Date: Thu, 24 Jul 2025 16:28:13 -0700 Subject: [PATCH 1/2] Add support for setting a custom viewport width --- .../benchmarks/perf/tests/css-props-tests.js | 2 + packages/react-strict-dom/src/native/index.js | 31 +++++++++- .../native/modules/ContextViewportScale.js | 28 +++++++++ .../src/native/modules/useStrictDOMElement.js | 18 +++++- .../src/native/modules/useStyleProps.js | 3 + .../src/native/stylex/CSSLengthUnitValue.js | 20 +++---- .../src/native/stylex/CSSTransformValue.js | 59 +++++++++++++++++++ .../stylex/__tests__/parseTransform-test.js | 6 +- .../src/native/stylex/index.js | 21 ++++++- .../src/native/stylex/lengthProps.js | 52 ++++++++++++++++ .../src/native/stylex/parseTransform.js | 14 ++--- .../html-test.native.js.snap-native | 37 ++++++++++++ .../react-strict-dom/tests/css-test.native.js | 6 +- .../tests/html-test.native.js | 36 +++++++++++ 14 files changed, 310 insertions(+), 23 deletions(-) create mode 100644 packages/react-strict-dom/src/native/modules/ContextViewportScale.js create mode 100644 packages/react-strict-dom/src/native/stylex/CSSTransformValue.js create mode 100644 packages/react-strict-dom/src/native/stylex/lengthProps.js diff --git a/packages/benchmarks/perf/tests/css-props-tests.js b/packages/benchmarks/perf/tests/css-props-tests.js index da4a4cd8..7896b3d6 100644 --- a/packages/benchmarks/perf/tests/css-props-tests.js +++ b/packages/benchmarks/perf/tests/css-props-tests.js @@ -20,6 +20,7 @@ function runSuite(opts) { hover: true, inheritedFontSize: 16, viewportHeight: 600, + viewportScale: 1, viewportWidth: 1024 }; @@ -29,6 +30,7 @@ function runSuite(opts) { hover: true, inheritedFontSize: 16, viewportHeight: 600, + viewportScale: 1, viewportWidth: 1024 }; diff --git a/packages/react-strict-dom/src/native/index.js b/packages/react-strict-dom/src/native/index.js index 6eb983ac..5aa7c94b 100644 --- a/packages/react-strict-dom/src/native/index.js +++ b/packages/react-strict-dom/src/native/index.js @@ -18,10 +18,13 @@ import type { import typeof * as TStyleX from '@stylexjs/stylex'; import * as React from 'react'; +import { useMemo } from 'react'; import * as compat from './compat'; import * as html from './html'; import * as stylex from './stylex'; import { ProvideCustomProperties } from './modules/ContextCustomProperties'; +import { ProvideViewportScale } from './modules/ContextViewportScale'; +import * as ReactNative from './react-native'; type StyleTheme = Theme; type StyleVars = VarGroup; @@ -35,6 +38,11 @@ type ProviderProps = $ReadOnly<{ customProperties: ProviderValue }>; +type ViewportProviderProps = $ReadOnly<{ + children: React.Node, + viewportWidth: number +}>; + export type { StaticStyles, StyleTheme, StyleVars, Styles, StylesWithout }; function ThemeProvider(props: ProviderProps): React.Node { @@ -47,8 +55,29 @@ function ThemeProvider(props: ProviderProps): React.Node { ); } +function ViewportProvider({ + viewportWidth: logicalViewportWidth, + children +}: ViewportProviderProps): React.Node { + const { width: viewportWidth } = ReactNative.useWindowDimensions(); + + const viewportScale = useMemo( + () => ({ + scale: viewportWidth / logicalViewportWidth + }), + [logicalViewportWidth, viewportWidth] + ); + + return ( + + {children} + + ); +} + const contexts = { - ThemeProvider: ThemeProvider as typeof ThemeProvider + ThemeProvider: ThemeProvider as typeof ThemeProvider, + ViewportProvider: ViewportProvider as typeof ViewportProvider }; // Export using StyleX types as the shim has divergent types internally. diff --git a/packages/react-strict-dom/src/native/modules/ContextViewportScale.js b/packages/react-strict-dom/src/native/modules/ContextViewportScale.js new file mode 100644 index 00000000..6fbc1207 --- /dev/null +++ b/packages/react-strict-dom/src/native/modules/ContextViewportScale.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +import React from 'react'; + +type ViewportScale = $ReadOnly<{ + scale: number +}>; + +const ContextViewportScale: React.Context = React.createContext({ + scale: 1 +}); + +if (__DEV__) { + ContextViewportScale.displayName = 'ContextViewportScale'; +} + +export const ProvideViewportScale = ContextViewportScale.Provider; + +export function useViewportScale(): ViewportScale { + return React.useContext(ContextViewportScale); +} diff --git a/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js b/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js index 663cf76d..28bc4142 100644 --- a/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js +++ b/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js @@ -13,6 +13,7 @@ import * as React from 'react'; import { useElementCallback } from '../../shared/useElementCallback'; import { errorMsg } from '../../shared/logUtils'; +import { useViewportScale } from './ContextViewportScale'; function errorUnimplemented(name: string) { if (__DEV__) { @@ -33,6 +34,7 @@ type Options = { }; export function useStrictDOMElement({ tagName }: Options): CallbackRef { + const { scale: viewportScale } = useViewportScale(); const elementCallback = useElementCallback( React.useCallback( // $FlowFixMe[unclear-type] @@ -83,6 +85,20 @@ export function useStrictDOMElement({ tagName }: Options): CallbackRef { }; } + if (viewportScale !== 1) { + const getBoundingClientRect = node.getBoundingClientRect; + node.getBoundingClientRect = function () { + const rect = getBoundingClientRect.call(node); + + return new DOMRect( + rect.x / viewportScale, + rect.y / viewportScale, + rect.width / viewportScale, + rect.height / viewportScale + ); + }; + } + const { getRootNode } = node; if (getRootNode == null) { node.getRootNode = () => errorUnimplemented('getRootNode'); @@ -157,7 +173,7 @@ export function useStrictDOMElement({ tagName }: Options): CallbackRef { } } }, - [tagName] + [tagName, viewportScale] ) ); diff --git a/packages/react-strict-dom/src/native/modules/useStyleProps.js b/packages/react-strict-dom/src/native/modules/useStyleProps.js index ed604680..2f509d61 100644 --- a/packages/react-strict-dom/src/native/modules/useStyleProps.js +++ b/packages/react-strict-dom/src/native/modules/useStyleProps.js @@ -20,6 +20,7 @@ import { flattenStyle } from './flattenStyle'; import { useInheritedStyles } from './ContextInheritedStyles'; import { usePseudoStates } from './usePseudoStates'; import { useStyleTransition } from './useStyleTransition'; +import { useViewportScale } from './ContextViewportScale'; type StyleOptions = { customProperties: ?CustomProperties, @@ -66,6 +67,7 @@ export function useStyleProps( const { fontScale, height, width } = ReactNative.useWindowDimensions(); const colorScheme = ReactNative.useColorScheme(); + const { scale: viewportScale } = useViewportScale(); // These values are already computed const { @@ -104,6 +106,7 @@ export function useStyleProps( inheritedFontSize: typeof inheritedFontSize === 'number' ? inheritedFontSize : undefined, viewportHeight: height, + viewportScale, viewportWidth: width }, flatStyle as $FlowFixMe diff --git a/packages/react-strict-dom/src/native/stylex/CSSLengthUnitValue.js b/packages/react-strict-dom/src/native/stylex/CSSLengthUnitValue.js index 5f8a5613..f7ba3490 100644 --- a/packages/react-strict-dom/src/native/stylex/CSSLengthUnitValue.js +++ b/packages/react-strict-dom/src/native/stylex/CSSLengthUnitValue.js @@ -17,16 +17,15 @@ type ResolvePixelValueOptions = $ReadOnly<{ fontScale: number | void, inheritedFontSize: ?number, viewportHeight: number, + viewportScale: number, viewportWidth: number }>; -type ParsedValue = [+value: number, +unit: CSSLengthUnitType] | null; - -const memoizedValues = new Map(); +const memoizedValues = new Map(); // TODO: this only works on simple values export class CSSLengthUnitValue { - static parse(input: string): ParsedValue { + static parse(input: string): CSSLengthUnitValue | null { const memoizedValue = memoizedValues.get(input); if (memoizedValue !== undefined) { return memoizedValue; @@ -40,9 +39,9 @@ export class CSSLengthUnitValue { const value = match[1]; const unit: $FlowFixMe = match[2]; const parsedFloat: number = parseFloat(value); - const parsedValue: ParsedValue = [parsedFloat, unit]; - memoizedValues.set(input, parsedValue); - return parsedValue; + const cssLengthUnitValue = new CSSLengthUnitValue(parsedFloat, unit); + memoizedValues.set(input, cssLengthUnitValue); + return cssLengthUnitValue; } value: number; @@ -58,7 +57,8 @@ export class CSSLengthUnitValue { viewportWidth, viewportHeight, fontScale = 1, - inheritedFontSize + inheritedFontSize, + viewportScale } = options; const unit = this.unit; const value = this.value; @@ -72,10 +72,10 @@ export class CSSLengthUnitValue { } } case 'px': { - return value; + return value * viewportScale; } case 'rem': { - return fontScale * 16 * value; + return fontScale * 16 * value * viewportScale; } case 'vh': { return viewportHeight * valuePercent; diff --git a/packages/react-strict-dom/src/native/stylex/CSSTransformValue.js b/packages/react-strict-dom/src/native/stylex/CSSTransformValue.js new file mode 100644 index 00000000..70c6d30c --- /dev/null +++ b/packages/react-strict-dom/src/native/stylex/CSSTransformValue.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +import type { ReactNativeTransform } from '../../types/renderer.native'; + +export class CSSTransformValue { + value: $ReadOnlyArray; + cachedScaledTransform: void | { + viewportScale: number, + value: $ReadOnlyArray + }; + + constructor(value: $ReadOnlyArray) { + this.value = value; + } + + resolveTransformValue( + viewportScale: number + ): $ReadOnlyArray { + if (viewportScale === 1) { + return this.value; + } + if ( + this.cachedScaledTransform != null && + this.cachedScaledTransform.viewportScale === viewportScale + ) { + return this.cachedScaledTransform.value; + } + + const scaledTransform = this.value.map((transform) => { + if ( + transform.translateX != null && + typeof transform.translateX === 'number' + ) { + return { translateX: transform.translateX * viewportScale }; + } + if ( + transform.translateY != null && + typeof transform.translateY === 'number' + ) { + return { translateY: transform.translateY * viewportScale }; + } + return transform; + }); + + this.cachedScaledTransform = { + viewportScale, + value: scaledTransform + }; + + return scaledTransform; + } +} diff --git a/packages/react-strict-dom/src/native/stylex/__tests__/parseTransform-test.js b/packages/react-strict-dom/src/native/stylex/__tests__/parseTransform-test.js index 49b93530..e9eff5e4 100644 --- a/packages/react-strict-dom/src/native/stylex/__tests__/parseTransform-test.js +++ b/packages/react-strict-dom/src/native/stylex/__tests__/parseTransform-test.js @@ -5,7 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import { parseTransform } from '../parseTransform'; +import { parseTransform as parseTransformImpl } from '../parseTransform'; + +function parseTransform(transform) { + return parseTransformImpl(transform).resolveTransformValue(1); +} describe('parseTransform', () => { test('perspective', () => { diff --git a/packages/react-strict-dom/src/native/stylex/index.js b/packages/react-strict-dom/src/native/stylex/index.js index 9931568d..e3deb8bc 100644 --- a/packages/react-strict-dom/src/native/stylex/index.js +++ b/packages/react-strict-dom/src/native/stylex/index.js @@ -34,6 +34,8 @@ import { stringContainsVariables } from './customProperties'; import { version } from '../modules/version'; +import { CSSTransformValue } from './CSSTransformValue'; +import { LENGTH_PROPS } from './lengthProps'; type ResolveStyleOptions = $ReadOnly<{ active?: ?boolean, @@ -44,6 +46,7 @@ type ResolveStyleOptions = $ReadOnly<{ hover?: ?boolean, inheritedFontSize: ?number, viewportHeight: number, + viewportScale: number, viewportWidth: number, writingDirection?: ?'ltr' | 'rtl' }>; @@ -197,7 +200,7 @@ function processStyle( const maybeLengthUnitValue = CSSLengthUnitValue.parse(styleValue); if (maybeLengthUnitValue != null) { - result[propName] = new CSSLengthUnitValue(...maybeLengthUnitValue); + result[propName] = maybeLengthUnitValue; continue; // React Native doesn't support these keywords or functions } else if (styleValue === 'inherit' || styleValue === 'unset') { @@ -295,6 +298,7 @@ function resolveStyle( hover, inheritedFontSize, viewportHeight, + viewportScale, viewportWidth } = options; const colorScheme = options.colorScheme || 'light'; @@ -331,6 +335,7 @@ function resolveStyle( fontScale, inheritedFontSize: inheritedFontSize, viewportHeight, + viewportScale, viewportWidth }); continue; @@ -343,6 +348,7 @@ function resolveStyle( fontScale, inheritedFontSize: fontSize, viewportHeight, + viewportScale, viewportWidth }); } else { @@ -352,6 +358,19 @@ function resolveStyle( } } + if (styleValue instanceof CSSTransformValue) { + result[propName] = styleValue.resolveTransformValue(viewportScale); + continue; + } + if ( + viewportScale !== 1 && + typeof styleValue === 'number' && + LENGTH_PROPS.has(propName) + ) { + result[propName] = styleValue * viewportScale; + continue; + } + // Resolve the stylex object-value syntax if ( styleValue != null && diff --git a/packages/react-strict-dom/src/native/stylex/lengthProps.js b/packages/react-strict-dom/src/native/stylex/lengthProps.js new file mode 100644 index 00000000..d978bd0e --- /dev/null +++ b/packages/react-strict-dom/src/native/stylex/lengthProps.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +function allSides(fn: (side: 'Top' | 'Right' | 'Bottom' | 'Left') => string) { + return [fn('Top'), fn('Right'), fn('Bottom'), fn('Left')]; +} + +export const LENGTH_PROPS: Set = new Set([ + 'margin', + ...allSides((prop) => `margin${prop}`), + 'padding', + ...allSides((prop) => `padding${prop}`), + ...allSides((prop) => prop.toLowerCase()), + 'maxHeight', + 'maxWidth', + 'minHeight', + 'minWidth', + 'height', + 'width', + 'marginInline', + 'marginBlock', + 'marginBlockStart', + 'marginBlockEnd', + 'marginInlineStart', + 'marginInlineEnd', + 'paddingInline', + 'paddingBlock', + 'paddingBlockStart', + 'paddingBlockEnd', + 'paddingInlineStart', + 'paddingInlineEnd', + 'borderWidth', + ...allSides((prop) => `border${prop}Width`), + 'borderRadius', + 'borderTopLeftRadius', + 'borderTopRightRadius', + 'borderBottomLeftRadius', + 'borderBottomRightRadius', + 'outlineWidth', + ...allSides((prop) => `outline${prop}Width`), + 'outlineOffset', + ...allSides((prop) => `${prop}Offset`), + 'gap', + 'columnGap', + 'rowGap' +]); diff --git a/packages/react-strict-dom/src/native/stylex/parseTransform.js b/packages/react-strict-dom/src/native/stylex/parseTransform.js index 8c0f13de..aca32836 100644 --- a/packages/react-strict-dom/src/native/stylex/parseTransform.js +++ b/packages/react-strict-dom/src/native/stylex/parseTransform.js @@ -9,16 +9,16 @@ import type { ReactNativeTransform } from '../../types/renderer.native'; +import { CSSTransformValue } from './CSSTransformValue'; + const transformRegex1 = /(perspective|scale|scaleX|scaleY|scaleZ|translateX|translateY)\(([0-9.+\-eE]+)(px|%)?\)/; const transformRegex2 = /(rotate|rotateX|rotateY|rotateZ|skewX|skewY)\((.*)\)/; const transformRegex3 = /matrix\((.*)\)/; -const memoizedValues = new Map(); +const memoizedValues = new Map(); -export function parseTransform( - transform: string -): $ReadOnlyArray { +export function parseTransform(transform: string): CSSTransformValue { const memoizedValue = memoizedValues.get(transform); if (memoizedValue != null) { return memoizedValue; @@ -114,7 +114,7 @@ export function parseTransform( } } - memoizedValues.set(transform, parsedTransforms); - - return parsedTransforms; + const cssTransformValue = new CSSTransformValue(parsedTransforms); + memoizedValues.set(transform, cssTransformValue); + return cssTransformValue; } diff --git a/packages/react-strict-dom/tests/__snapshots__/html-test.native.js.snap-native b/packages/react-strict-dom/tests/__snapshots__/html-test.native.js.snap-native index 8807fc0d..c35715d4 100644 --- a/packages/react-strict-dom/tests/__snapshots__/html-test.native.js.snap-native +++ b/packages/react-strict-dom/tests/__snapshots__/html-test.native.js.snap-native @@ -2022,3 +2022,40 @@ exports[` style polyfills lineClamp workaround for Android 1`] = ` text `; + +exports[` viewport width lengths are scaled according to viewport width: scaled lengths 1`] = ` + + + Scaled content + + +`; diff --git a/packages/react-strict-dom/tests/css-test.native.js b/packages/react-strict-dom/tests/css-test.native.js index e1b52a48..5af3b6ea 100644 --- a/packages/react-strict-dom/tests/css-test.native.js +++ b/packages/react-strict-dom/tests/css-test.native.js @@ -10,7 +10,8 @@ import { css } from 'react-strict-dom'; const mockOptions = { hover: false, viewportHeight: 600, - viewportWidth: 320 + viewportWidth: 320, + viewportScale: 1 }; /** @@ -1731,7 +1732,8 @@ expect.extend({ const theProps = css.props.call( { viewportHeight: height, - viewportWidth: width + viewportWidth: width, + viewportScale: 1 }, underTest ); diff --git a/packages/react-strict-dom/tests/html-test.native.js b/packages/react-strict-dom/tests/html-test.native.js index 27ff635e..3c54e381 100644 --- a/packages/react-strict-dom/tests/html-test.native.js +++ b/packages/react-strict-dom/tests/html-test.native.js @@ -1630,4 +1630,40 @@ describe('', () => { }); */ }); + + describe('viewport width', () => { + test('lengths are scaled according to viewport width', () => { + const { ViewportProvider } = contexts; + const ReactNative = require('../src/native/react-native'); + jest + .spyOn(ReactNative, 'useWindowDimensions') + .mockReturnValue({ width: 960 }); + + const styles = css.create({ + container: { + margin: '20px', + padding: '15px', + width: '200px', + height: '100px', + borderWidth: '5px', + transform: 'translateX(10px) translateY(20px)' + } + }); + + let root; + act(() => { + root = create( + + + Scaled content + + + ); + }); + + expect(root.toJSON()).toMatchSnapshot('scaled lengths'); + + ReactNative.useWindowDimensions.mockRestore(); + }); + }); }); From 2e4045a9526a03debb7ea590e01a0fdba7274475 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Thu, 24 Jul 2025 21:03:31 -0700 Subject: [PATCH 2/2] 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. --- apps/examples/src/components/App.js | 1 + packages/react-strict-dom/src/native/index.js | 29 +------ .../native/modules/ContextViewportScale.js | 40 +++++++-- .../src/native/modules/useStrictDOMElement.js | 4 +- .../src/native/stylex/CSSLengthUnitValue.js | 8 +- .../src/native/stylex/index.js | 6 +- .../src/native/stylex/isLengthStyleKey.js | 86 +++++++++++++++++++ .../src/native/stylex/lengthProps.js | 52 ----------- .../html-test.native.js.snap-native | 6 +- .../tests/html-test.native.js | 13 ++- 10 files changed, 142 insertions(+), 103 deletions(-) create mode 100644 packages/react-strict-dom/src/native/stylex/isLengthStyleKey.js delete mode 100644 packages/react-strict-dom/src/native/stylex/lengthProps.js diff --git a/apps/examples/src/components/App.js b/apps/examples/src/components/App.js index ceab9d99..1801e4a7 100644 --- a/apps/examples/src/components/App.js +++ b/apps/examples/src/components/App.js @@ -54,6 +54,7 @@ const themedStyles = css.create({ container: { display: 'flex', flex: 1, + flexDirection: 'column', justifyContent: 'center', backgroundColor: '#bbb', padding: 8 diff --git a/packages/react-strict-dom/src/native/index.js b/packages/react-strict-dom/src/native/index.js index 5aa7c94b..b14c019b 100644 --- a/packages/react-strict-dom/src/native/index.js +++ b/packages/react-strict-dom/src/native/index.js @@ -18,13 +18,11 @@ import type { import typeof * as TStyleX from '@stylexjs/stylex'; import * as React from 'react'; -import { useMemo } from 'react'; import * as compat from './compat'; import * as html from './html'; import * as stylex from './stylex'; import { ProvideCustomProperties } from './modules/ContextCustomProperties'; import { ProvideViewportScale } from './modules/ContextViewportScale'; -import * as ReactNative from './react-native'; type StyleTheme = Theme; type StyleVars = VarGroup; @@ -38,11 +36,6 @@ type ProviderProps = $ReadOnly<{ customProperties: ProviderValue }>; -type ViewportProviderProps = $ReadOnly<{ - children: React.Node, - viewportWidth: number -}>; - export type { StaticStyles, StyleTheme, StyleVars, Styles, StylesWithout }; function ThemeProvider(props: ProviderProps): React.Node { @@ -55,29 +48,9 @@ function ThemeProvider(props: ProviderProps): React.Node { ); } -function ViewportProvider({ - viewportWidth: logicalViewportWidth, - children -}: ViewportProviderProps): React.Node { - const { width: viewportWidth } = ReactNative.useWindowDimensions(); - - const viewportScale = useMemo( - () => ({ - scale: viewportWidth / logicalViewportWidth - }), - [logicalViewportWidth, viewportWidth] - ); - - return ( - - {children} - - ); -} - const contexts = { ThemeProvider: ThemeProvider as typeof ThemeProvider, - ViewportProvider: ViewportProvider as typeof ViewportProvider + ViewportProvider: ProvideViewportScale as typeof ProvideViewportScale }; // Export using StyleX types as the shim has divergent types internally. diff --git a/packages/react-strict-dom/src/native/modules/ContextViewportScale.js b/packages/react-strict-dom/src/native/modules/ContextViewportScale.js index 6fbc1207..eab7e791 100644 --- a/packages/react-strict-dom/src/native/modules/ContextViewportScale.js +++ b/packages/react-strict-dom/src/native/modules/ContextViewportScale.js @@ -4,25 +4,49 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow strict + * @flow strict-local */ -import React from 'react'; +import * as React from 'react'; +import * as ReactNative from '../react-native'; -type ViewportScale = $ReadOnly<{ +type Value = $ReadOnly<{ scale: number }>; -const ContextViewportScale: React.Context = React.createContext({ - scale: 1 -}); +type ProviderProps = $ReadOnly<{ + children: React.Node, + viewportWidth: number +}>; + +const defaultContext = { scale: 1 }; +const ContextViewportScale: React.Context = + React.createContext(defaultContext); if (__DEV__) { ContextViewportScale.displayName = 'ContextViewportScale'; } -export const ProvideViewportScale = ContextViewportScale.Provider; +export function ProvideViewportScale({ + viewportWidth: logicalViewportWidth, + children +}: ProviderProps): React.Node { + const { width: viewportWidth } = ReactNative.useWindowDimensions(); + + const viewportScale = React.useMemo( + () => ({ + scale: viewportWidth / logicalViewportWidth + }), + [logicalViewportWidth, viewportWidth] + ); + + return ( + + {children} + + ); +} -export function useViewportScale(): ViewportScale { +export function useViewportScale(): Value { return React.useContext(ContextViewportScale); } diff --git a/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js b/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js index 28bc4142..8825366a 100644 --- a/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js +++ b/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js @@ -4,15 +4,15 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow strict + * @flow strict-local */ import type { CallbackRef } from '../../types/react'; import * as React from 'react'; -import { useElementCallback } from '../../shared/useElementCallback'; import { errorMsg } from '../../shared/logUtils'; +import { useElementCallback } from '../../shared/useElementCallback'; import { useViewportScale } from './ContextViewportScale'; function errorUnimplemented(name: string) { diff --git a/packages/react-strict-dom/src/native/stylex/CSSLengthUnitValue.js b/packages/react-strict-dom/src/native/stylex/CSSLengthUnitValue.js index f7ba3490..8e64ecd5 100644 --- a/packages/react-strict-dom/src/native/stylex/CSSLengthUnitValue.js +++ b/packages/react-strict-dom/src/native/stylex/CSSLengthUnitValue.js @@ -54,11 +54,11 @@ export class CSSLengthUnitValue { resolvePixelValue(options: ResolvePixelValueOptions): number { const { - viewportWidth, - viewportHeight, fontScale = 1, inheritedFontSize, - viewportScale + viewportHeight, + viewportScale, + viewportWidth } = options; const unit = this.unit; const value = this.value; @@ -66,7 +66,7 @@ export class CSSLengthUnitValue { switch (unit) { case 'em': { if (inheritedFontSize == null) { - return fontScale * 16 * value; + return fontScale * 16 * value * viewportScale; } else { return inheritedFontSize * value; } diff --git a/packages/react-strict-dom/src/native/stylex/index.js b/packages/react-strict-dom/src/native/stylex/index.js index e3deb8bc..1b4ad203 100644 --- a/packages/react-strict-dom/src/native/stylex/index.js +++ b/packages/react-strict-dom/src/native/stylex/index.js @@ -19,12 +19,14 @@ import type { } from '../../types/styles'; import { CSSLengthUnitValue } from './CSSLengthUnitValue'; +import { CSSTransformValue } from './CSSTransformValue'; import { CSSUnparsedValue } from './typed-om/CSSUnparsedValue'; import { errorMsg, warnMsg } from '../../shared/logUtils'; import { fixContentBox } from './fixContentBox'; import { flattenStyle } from './flattenStyleXStyles'; import { isAllowedShortFormValue } from './isAllowedShortFormValue'; import { isAllowedStyleKey } from './isAllowedStyleKey'; +import { lengthStyleKeySet } from './isLengthStyleKey'; import { mediaQueryMatches } from './mediaQueryMatches'; import { parseTextShadow } from './parseTextShadow'; import { parseTimeValue } from './parseTimeValue'; @@ -34,8 +36,6 @@ import { stringContainsVariables } from './customProperties'; import { version } from '../modules/version'; -import { CSSTransformValue } from './CSSTransformValue'; -import { LENGTH_PROPS } from './lengthProps'; type ResolveStyleOptions = $ReadOnly<{ active?: ?boolean, @@ -365,7 +365,7 @@ function resolveStyle( if ( viewportScale !== 1 && typeof styleValue === 'number' && - LENGTH_PROPS.has(propName) + lengthStyleKeySet.has(propName) ) { result[propName] = styleValue * viewportScale; continue; diff --git a/packages/react-strict-dom/src/native/stylex/isLengthStyleKey.js b/packages/react-strict-dom/src/native/stylex/isLengthStyleKey.js new file mode 100644 index 00000000..7c77763d --- /dev/null +++ b/packages/react-strict-dom/src/native/stylex/isLengthStyleKey.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +import { version } from '../modules/version'; + +export const lengthStyleKeySet: Set = new Set([ + 'blockSize', + 'borderBottomLeftRadius', + 'borderBottomRightRadius', + 'borderBottomWidth', + 'borderBlockWidth', + 'borderBlockEndWidth', + 'borderBlockStartWidth', + 'borderEndEndRadius', + 'borderEndStartRadius', + 'borderInlineWidth', + 'borderInlineEndWidth', + 'borderInlineStartWidth', + 'borderLeftWidth', + 'borderRadius', + 'borderRightWidth', + 'borderStartEndRadius', + 'borderStartStartRadius', + 'borderTopLeftRadius', + 'borderTopRightRadius', + 'borderTopWidth', + 'borderWidth', + 'bottom', + 'columnGap', + 'gap', + 'height', + 'inlineSize', + 'inset', + 'insetBlock', + 'insetBlockEnd', + 'insetBlockStart', + 'insetInline', + 'insetInlineEnd', + 'insetInlineStart', + 'left', + 'margin', + 'marginBlock', + 'marginBlockEnd', + 'marginBlockStart', + 'marginBottom', + 'marginInline', + 'marginInlineEnd', + 'marginInlineStart', + 'marginLeft', + 'marginRight', + 'marginTop', + 'maxBlockSize', + 'maxHeight', + 'maxInlineSize', + 'maxWidth', + 'minBlockSize', + 'minHeight', + 'minInlineSize', + 'minWidth', + 'padding', + 'paddingBlock', + 'paddingBlockEnd', + 'paddingBlockStart', + 'paddingBottom', + 'paddingInline', + 'paddingInlineEnd', + 'paddingInlineStart', + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'right', + 'rowGap', + 'top', + 'width' +]); + +if (version.experimental) { + lengthStyleKeySet.add('outlineOffset'); + lengthStyleKeySet.add('outlineWidth'); +} diff --git a/packages/react-strict-dom/src/native/stylex/lengthProps.js b/packages/react-strict-dom/src/native/stylex/lengthProps.js deleted file mode 100644 index d978bd0e..00000000 --- a/packages/react-strict-dom/src/native/stylex/lengthProps.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict - */ - -function allSides(fn: (side: 'Top' | 'Right' | 'Bottom' | 'Left') => string) { - return [fn('Top'), fn('Right'), fn('Bottom'), fn('Left')]; -} - -export const LENGTH_PROPS: Set = new Set([ - 'margin', - ...allSides((prop) => `margin${prop}`), - 'padding', - ...allSides((prop) => `padding${prop}`), - ...allSides((prop) => prop.toLowerCase()), - 'maxHeight', - 'maxWidth', - 'minHeight', - 'minWidth', - 'height', - 'width', - 'marginInline', - 'marginBlock', - 'marginBlockStart', - 'marginBlockEnd', - 'marginInlineStart', - 'marginInlineEnd', - 'paddingInline', - 'paddingBlock', - 'paddingBlockStart', - 'paddingBlockEnd', - 'paddingInlineStart', - 'paddingInlineEnd', - 'borderWidth', - ...allSides((prop) => `border${prop}Width`), - 'borderRadius', - 'borderTopLeftRadius', - 'borderTopRightRadius', - 'borderBottomLeftRadius', - 'borderBottomRightRadius', - 'outlineWidth', - ...allSides((prop) => `outline${prop}Width`), - 'outlineOffset', - ...allSides((prop) => `${prop}Offset`), - 'gap', - 'columnGap', - 'rowGap' -]); diff --git a/packages/react-strict-dom/tests/__snapshots__/html-test.native.js.snap-native b/packages/react-strict-dom/tests/__snapshots__/html-test.native.js.snap-native index c35715d4..eb16da34 100644 --- a/packages/react-strict-dom/tests/__snapshots__/html-test.native.js.snap-native +++ b/packages/react-strict-dom/tests/__snapshots__/html-test.native.js.snap-native @@ -2028,11 +2028,11 @@ exports[` viewport width lengths are scaled according to viewport width: ref={[Function]} style={ { - "borderWidth": 3.75, + "borderWidth": 0.75, "boxSizing": "content-box", "height": 75, "margin": 15, - "padding": 11.25, + "padding": 24, "position": "static", "transform": [ { @@ -2051,6 +2051,8 @@ exports[` viewport width lengths are scaled according to viewport width: style={ { "boxSizing": "content-box", + "fontSize": 24, + "lineHeight": 24, "position": "static", } } diff --git a/packages/react-strict-dom/tests/html-test.native.js b/packages/react-strict-dom/tests/html-test.native.js index 3c54e381..e12bbffc 100644 --- a/packages/react-strict-dom/tests/html-test.native.js +++ b/packages/react-strict-dom/tests/html-test.native.js @@ -1641,12 +1641,16 @@ describe('', () => { const styles = css.create({ container: { + borderWidth: '1px', + height: '100px', margin: '20px', - padding: '15px', + padding: '2em', width: '200px', - height: '100px', - borderWidth: '5px', transform: 'translateX(10px) translateY(20px)' + }, + text: { + fontSize: '2rem', + lineHeight: '1em' } }); @@ -1655,12 +1659,13 @@ describe('', () => { root = create( - Scaled content + Scaled content ); }); + // scale factor 0.75 expect(root.toJSON()).toMatchSnapshot('scaled lengths'); ReactNative.useWindowDimensions.mockRestore();