diff --git a/packages/react-strict-dom/src/native/compat.js b/packages/react-strict-dom/src/native/compat.js index a0a820ac..f0cbfc56 100644 --- a/packages/react-strict-dom/src/native/compat.js +++ b/packages/react-strict-dom/src/native/compat.js @@ -8,6 +8,7 @@ */ import type { StrictProps } from '../types/StrictProps'; +import type { StrictReactNativeMetaProps } from '../types/renderer.native'; import * as React from 'react'; @@ -31,7 +32,9 @@ import { createStrictDOMTextInputComponent as createStrictTextInput } from './mo * aria-label="label" * as="text" * > - * {(nativeProps: React.PropsOf)) => ( + * {(nativeProps: React.PropsOf, meta) => ( + * // `meta.inheritedTextStyle` and `meta.resolveStyleValue` allow resolving + * // CSS inheritance and `var(...)` for non-RSD host components. * * )} * @@ -42,7 +45,7 @@ const defaultProps = {}; type StrictPropsOnlyCompat = { ...StrictProps, as?: 'div' | 'img' | 'input' | 'span' | 'textarea', - children: (nativeProps: T) => React.Node + children: (nativeProps: T, meta: StrictReactNativeMetaProps) => React.Node }; const StrictText = createStrictText('span', defaultProps) as $FlowFixMe; diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js index 419eca47..dbbc57bf 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js @@ -8,6 +8,7 @@ */ import type { ReactNativeProps } from '../../types/renderer.native'; +import type { StrictReactNativeMetaProps } from '../../types/renderer.native'; import type { StrictProps as StrictPropsOriginal } from '../../types/StrictProps'; import * as React from 'react'; @@ -15,7 +16,11 @@ import * as ReactNative from '../react-native'; import { ProvideCustomProperties } from './ContextCustomProperties'; import { ProvideDisplayInside, useDisplayInside } from './ContextDisplayInside'; -import { ProvideInheritedStyles } from './ContextInheritedStyles'; +import { + ProvideInheritedStyles, + useInheritedStyles +} from './ContextInheritedStyles'; +import { flattenStyle } from './flattenStyle'; import { TextString } from './TextString'; import { errorMsg } from '../../shared/logUtils'; import { useNativeProps } from './useNativeProps'; @@ -23,7 +28,9 @@ import { useStrictDOMElement } from './useStrictDOMElement'; type StrictProps = Readonly<{ ...StrictPropsOriginal, - children?: React.Node | ((ReactNativeProps) => React.Node) + children?: + | React.Node + | ((ReactNativeProps, StrictReactNativeMetaProps) => React.Node) }>; const AnimatedPressable = ReactNative.Animated.createAnimatedComponent( @@ -54,15 +61,22 @@ export function createStrictDOMComponent( * Resolve global HTML and style props */ - const { customProperties, nativeProps, inheritableStyle } = useNativeProps( - defaultProps, - props, - { - provideInheritableStyle, - withInheritedStyle: false, - withTextStyle: false - } - ); + const { + customProperties, + resolveStyleValue, + nativeProps, + inheritableStyle + } = useNativeProps(defaultProps, props, { + provideInheritableStyle, + withInheritedStyle: false, + withTextStyle: false + }); + + const ancestorInheritedStyle = useInheritedStyles(); + const inheritedTextStyle = flattenStyle([ + ancestorInheritedStyle, + inheritableStyle + ]); if ( nativeProps.onPress != null && @@ -193,7 +207,7 @@ export function createStrictDOMComponent( let element: React.Node = typeof props.children === 'function' ? ( - props.children(nativeProps) + props.children(nativeProps, { inheritedTextStyle, resolveStyleValue }) ) : ( // $FlowFixMe[incompatible-type] diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js index 8dd285a5..72963548 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js @@ -13,6 +13,8 @@ import * as React from 'react'; import * as ReactNative from '../react-native'; import { useNativeProps } from './useNativeProps'; +import { useInheritedStyles } from './ContextInheritedStyles'; +import { flattenStyle } from './flattenStyle'; import { useStrictDOMElement } from './useStrictDOMElement'; import * as css from '../css'; @@ -52,11 +54,21 @@ export function createStrictDOMImageComponent< ] }; - const { nativeProps } = useNativeProps(defaultProps, props, { - provideInheritableStyle: false, - withInheritedStyle: false, - withTextStyle: false - }); + const { resolveStyleValue, nativeProps, inheritableStyle } = useNativeProps( + defaultProps, + props, + { + provideInheritableStyle: false, + withInheritedStyle: false, + withTextStyle: false + } + ); + + const ancestorInheritedStyle = useInheritedStyles(); + const inheritedTextStyle = flattenStyle([ + ancestorInheritedStyle, + inheritableStyle + ]); // Tag-specific props @@ -122,7 +134,7 @@ export function createStrictDOMImageComponent< const element: React.Node = typeof props.children === 'function' ? ( - props.children(nativeProps) + props.children(nativeProps, { inheritedTextStyle, resolveStyleValue }) ) : ( // strict-dom's wide ReactNativeProps spreads onto RN 0.83's exact // ImageProps; harmless extras are ignored at runtime. diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js index 30deca61..07f220d3 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js @@ -8,20 +8,27 @@ */ import type { ReactNativeProps } from '../../types/renderer.native'; +import type { StrictReactNativeMetaProps } from '../../types/renderer.native'; import type { StrictProps as StrictPropsOriginal } from '../../types/StrictProps'; import * as React from 'react'; import * as ReactNative from '../react-native'; import { ProvideCustomProperties } from './ContextCustomProperties'; -import { ProvideInheritedStyles } from './ContextInheritedStyles'; +import { + ProvideInheritedStyles, + useInheritedStyles +} from './ContextInheritedStyles'; +import { flattenStyle } from './flattenStyle'; import { errorMsg } from '../../shared/logUtils'; import { useNativeProps } from './useNativeProps'; import { useStrictDOMElement } from './useStrictDOMElement'; type StrictProps = Readonly<{ ...StrictPropsOriginal, - children?: React.Node | ((ReactNativeProps) => React.Node) + children?: + | React.Node + | ((ReactNativeProps, StrictReactNativeMetaProps) => React.Node) }>; function hasElementChildren(children: unknown): boolean { @@ -44,19 +51,26 @@ export function createStrictDOMTextComponent( * Resolve global HTML and style props */ - const { customProperties, nativeProps, inheritableStyle } = useNativeProps( - defaultProps, - props, - { - provideInheritableStyle: - tagName !== 'br' || - // $FlowFixMe[invalid-compare] - tagName !== 'option' || - hasElementChildren(props.children), - withInheritedStyle: true, - withTextStyle: true - } - ); + const { + customProperties, + resolveStyleValue, + nativeProps, + inheritableStyle + } = useNativeProps(defaultProps, props, { + provideInheritableStyle: + tagName !== 'br' || + // $FlowFixMe[invalid-compare] + tagName !== 'option' || + hasElementChildren(props.children), + withInheritedStyle: true, + withTextStyle: true + }); + + const ancestorInheritedStyle = useInheritedStyles(); + const inheritedTextStyle = flattenStyle([ + ancestorInheritedStyle, + inheritableStyle + ]); // Tag-specific props @@ -128,7 +142,7 @@ export function createStrictDOMTextComponent( let element: React.Node = typeof props.children === 'function' ? ( - props.children(nativeProps) + props.children(nativeProps, { inheritedTextStyle, resolveStyleValue }) ) : ( // $FlowFixMe[incompatible-type] diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js index 9cc4f74c..7bb71d11 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js @@ -15,6 +15,8 @@ import * as ReactNative from '../react-native'; import { errorMsg } from '../../shared/logUtils'; import { mergeRefs } from '../../shared/mergeRefs'; +import { useInheritedStyles } from './ContextInheritedStyles'; +import { flattenStyle } from './flattenStyle'; import { useNativeProps } from './useNativeProps'; import { useStrictDOMElement } from './useStrictDOMElement'; @@ -76,11 +78,21 @@ export function createStrictDOMTextInputComponent< * Resolve global HTML and style props */ - const { nativeProps } = useNativeProps(defaultProps, props, { - provideInheritableStyle: false, - withInheritedStyle: false, - withTextStyle: true - }); + const { resolveStyleValue, nativeProps, inheritableStyle } = useNativeProps( + defaultProps, + props, + { + provideInheritableStyle: false, + withInheritedStyle: false, + withTextStyle: true + } + ); + + const ancestorInheritedStyle = useInheritedStyles(); + const inheritedTextStyle = flattenStyle([ + ancestorInheritedStyle, + inheritableStyle + ]); // Tag-specific props @@ -246,7 +258,7 @@ export function createStrictDOMTextInputComponent< const element = typeof props.children === 'function' ? ( - props.children(nativeProps) + props.children(nativeProps, { inheritedTextStyle, resolveStyleValue }) ) : ( // strict-dom's wide ReactNativeProps spreads onto RN 0.83's exact // TextInputProps; harmless extras are ignored at runtime. diff --git a/packages/react-strict-dom/src/native/modules/useNativeProps.js b/packages/react-strict-dom/src/native/modules/useNativeProps.js index b185cbee..c4923119 100644 --- a/packages/react-strict-dom/src/native/modules/useNativeProps.js +++ b/packages/react-strict-dom/src/native/modules/useNativeProps.js @@ -8,19 +8,32 @@ */ import type { CustomProperties } from '../../types/styles'; -import type { ReactNativeProps } from '../../types/renderer.native'; +import type { + ReactNativeProps, + StrictReactNativeMetaProps +} from '../../types/renderer.native'; import type { StrictProps as StrictPropsOriginal } from '../../types/StrictProps'; import type { Style } from '../../types/styles'; +import * as ReactNative from '../react-native'; +import { CSSUnparsedValue } from '../css/typed-om/CSSUnparsedValue'; +import { + resolveVariableReferences, + stringContainsVariables +} from '../css/customProperties'; import { errorMsg, warnMsg } from '../../shared/logUtils'; import { extractStyleThemes } from './extractStyleThemes'; import { isPropAllowed } from '../../shared/isPropAllowed'; import { useCustomProperties } from './ContextCustomProperties'; import { useStyleProps } from './useStyleProps'; +export type ResolveStyleValue = (value: string) => string | number | null; + type StrictProps = Readonly<{ ...StrictPropsOriginal, - children?: React.Node | ((ReactNativeProps) => React.Node) + children?: + | React.Node + | ((ReactNativeProps, StrictReactNativeMetaProps) => React.Node) }>; /** @@ -83,6 +96,7 @@ type OptionsType = {| |}; type ReturnType = {| customProperties: ?CustomProperties, + resolveStyleValue: ResolveStyleValue, nativeProps: ReactNativeProps, inheritableStyle: ?Style |}; @@ -158,6 +172,19 @@ export function useNativeProps( extractStyleThemes(renderStyle); const customProperties = useCustomProperties(customPropertiesFromThemes); + const colorScheme = ReactNative.useColorScheme(); + const resolveStyleValue: ResolveStyleValue = (value) => { + if (typeof value !== 'string' || !stringContainsVariables(value)) { + return value; + } + return resolveVariableReferences( + 'styleValue', + CSSUnparsedValue.parse('styleValue', value), + customProperties, + colorScheme === 'dark' ? 'dark' : 'light' + ); + }; + const { nativeProps, inheritableStyle } = useStyleProps(extractedStyle, { customProperties, provideInheritableStyle: options.provideInheritableStyle, @@ -440,6 +467,7 @@ export function useNativeProps( return { customProperties: customPropertiesFromThemes != null ? customProperties : null, + resolveStyleValue, nativeProps, inheritableStyle }; diff --git a/packages/react-strict-dom/src/types/renderer.native.js b/packages/react-strict-dom/src/types/renderer.native.js index 703cc602..1fe1c864 100644 --- a/packages/react-strict-dom/src/types/renderer.native.js +++ b/packages/react-strict-dom/src/types/renderer.native.js @@ -32,6 +32,7 @@ import type { // $FlowFixMe[nonstrict-import] ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes'; +import type { Style } from './styles'; type ReactNativeProps = { accessible?: ViewProps['accessible'], @@ -147,11 +148,17 @@ type ReactNativeStyleValue = type ReactNativeStyle = { [string]: ?ReactNativeStyleValue }; +type StrictReactNativeMetaProps = { + +inheritedTextStyle: Style, + +resolveStyleValue: (value: string) => string | number | null +}; + export type { CompositeAnimation, ReactNativeProps, ReactNativeStyle, ReactNativeStyleValue, ReactNativeTransform, + StrictReactNativeMetaProps, SyntheticEvent }; diff --git a/packages/react-strict-dom/tests/compat/compat-test.native.js b/packages/react-strict-dom/tests/compat/compat-test.native.js index f02f840e..da03df7b 100644 --- a/packages/react-strict-dom/tests/compat/compat-test.native.js +++ b/packages/react-strict-dom/tests/compat/compat-test.native.js @@ -180,6 +180,70 @@ describe('', () => { expect(root.toJSON()).toMatchSnapshot('nested'); }); + test('exposes inheritedTextStyle (resolved inherited color) to the function child', () => { + const styles = css.create({ + red: { color: 'red' } + }); + + let captured; + act(() => { + create( + + + {(nativeProps, meta) => { + captured = meta; + return ; + }} + + + ); + }); + expect(captured.inheritedTextStyle.color).toBe('red'); + }); + + test('own inheritable style overrides inherited in inheritedTextStyle', () => { + const ancestor = css.create({ red: { color: 'red' } }); + const own = css.create({ green: { color: 'green' } }); + + let captured; + act(() => { + create( + + + {(nativeProps, meta) => { + captured = meta; + return ; + }} + + + ); + }); + expect(captured.inheritedTextStyle.color).toBe('green'); + }); + + test('exposes resolveStyleValue so var(...) can be resolved on host-only props', () => { + const vars = css.defineVars({ brand: 'blue' }); + const theme = css.createTheme(vars, { brand: 'magenta' }); + + let captured; + act(() => { + create( + + + {(nativeProps, meta) => { + captured = meta; + return ; + }} + + + ); + }); + // a literal value is returned unchanged + expect(captured.resolveStyleValue('red')).toBe('red'); + // a theme token resolves through the cascade + expect(captured.resolveStyleValue(vars.brand)).toBe('magenta'); + }); + test('styled', () => { const styles = css.create({ block: {