Support child states for react native

This commit is contained in:
Zoe Roux
2023-01-09 16:57:43 +09:00
parent 6111e2f27c
commit f0686317c8
5 changed files with 144 additions and 70 deletions

View File

@@ -3,7 +3,7 @@
"name": "expo-example", "name": "expo-example",
"slug": "expo-example", "slug": "expo-example",
"version": "1.0.0", "version": "1.0.0",
"orientation": "portrait", "orientation": "default",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"entryPoint": "./src/app.tsx", "entryPoint": "./src/app.tsx",
"userInterfaceStyle": "light", "userInterfaceStyle": "light",

View File

@@ -31,9 +31,28 @@ const BoxWithoutProps = (props: Stylable) => {
{ {
backgroundColor: { xs: "#00ff00", md: "#ff0000" }, backgroundColor: { xs: "#00ff00", md: "#ff0000" },
transform: [{ scaleY: 0.7 }], transform: [{ scaleY: 0.7 }],
hover: { alignContent: "center", alignItems: "center" }, press: {
press: { alignContent: "center" }, self: {
focus: { alignContent: "center" }, bg: "red",
},
text: {
color: "white",
},
},
hover: {
text: {
color: "blue",
},
},
focus: {
self: {
bg: "yellow"
},
text: {
transform: [{ scale: 2 }],
color: "green"
},
},
}, },
md({ md({
shadowOpacity: 0.5, shadowOpacity: 0.5,
@@ -45,9 +64,12 @@ const BoxWithoutProps = (props: Stylable) => {
)} )}
> >
<H1 <H1
{...css({ {...css([
color: { xs: "black", md: "white" }, {
})} color: { xs: "black", md: "white" },
},
"text",
])}
> >
Text inside the box without props (green on small screens, red on bigs) Text inside the box without props (green on small screens, red on bigs)
</H1> </H1>
@@ -85,12 +107,14 @@ function App() {
<Text>Open up App.tsx to start working on your app!</Text> <Text>Open up App.tsx to start working on your app!</Text>
<CustomBox color="black" {...css({ borderColor: "red", borderWidth: px(3) })} /> <CustomBox color="black" {...css({ borderColor: "red", borderWidth: px(3) })} />
<BoxWithoutProps {...css({ borderColor: "red", borderWidth: px(3) })} /> <BoxWithoutProps {...css({ borderColor: "red", borderWidth: px(3) })} />
<P <Pressable android_ripple={{ color: "black"}}>
accessibilityLabel="toto" <P
style={[undefined, false, { color: "red" }, [{ color: "green" }, false]]} accessibilityLabel="toto"
> style={[undefined, false, { color: "red" }, [{ color: "green" }, false]]}
Test >
</P> Test
</P>
</Pressable>
<StatusBar style="auto" /> <StatusBar style="auto" />
</View> </View>
); );

View File

@@ -3,12 +3,20 @@
// Licensed under the MIT license. See LICENSE file in the project root for details. // Licensed under the MIT license. See LICENSE file in the project root for details.
// //
import { useWindowDimensions } from "react-native"; import { PressableProps, useWindowDimensions, ViewStyle } from "react-native";
import { breakpoints, Theme, useTheme } from "../theme"; import { breakpoints, Theme, useTheme } from "../theme";
import { Breakpoints, YoshikiStyle, hasState, processStyleList } from "../type"; import {
Breakpoints,
YoshikiStyle,
hasState,
processStyleList,
processStyleListWithChild,
assignChilds,
} from "../type";
import { isBreakpoints } from "../utils"; import { isBreakpoints } from "../utils";
import { shorthandsFn } from "../shorthands"; import { shorthandsFn } from "../shorthands";
import { StyleFunc, NativeCssFunc } from "./type"; import { StyleFunc, NativeCssFunc, NativeStyle } from "./type";
import { useReducer, useRef, useState } from "react";
const useBreakpoint = (): number => { const useBreakpoint = (): number => {
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
@@ -46,12 +54,19 @@ const propertyMapper = (
return [[key, value]]; return [[key, value]];
}; };
const useForceRerender = () => {
return useReducer((x) => x + 1, 0)[1];
};
export const useYoshiki = () => { export const useYoshiki = () => {
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();
const theme = useTheme(); const theme = useTheme();
const rerender = useForceRerender();
const childStyles = useRef<Record<string, NativeStyle | undefined>>({});
const css: NativeCssFunc = (cssList, leftOvers) => { const css: NativeCssFunc = (cssList, leftOvers) => {
const css = processStyleList(cssList); // The as any is because we can't be sure the style type is right one.
const css = processStyleListWithChild(cssList, childStyles.current as any);
const processStyle = (styleList: Record<string, YoshikiStyle<unknown>>) => { const processStyle = (styleList: Record<string, YoshikiStyle<unknown>>) => {
const ret = Object.fromEntries( const ret = Object.fromEntries(
@@ -62,35 +77,63 @@ export const useYoshiki = () => {
return ret; return ret;
}; };
if (hasState<Record<string, unknown>>(css)) { if (hasState<Record<string, ViewStyle>>(css)) {
const { hover, focus, press, ...inline } = css; const { hover, focus, press, ...inline } = css;
const ret: StyleFunc<unknown> = ({ hovered, focused, pressed }) => ({ const { onPressIn, onPressOut, onHoverIn, onHoverOut, onFocus, onBlur } =
...processStyle(inline), leftOvers as PressableProps;
...(hovered ? processStyle(hover ?? {}) : {}), const ret: StyleFunc<unknown> = ({ hovered, focused, pressed }) => {
...(focused ? processStyle(focus ?? {}) : {}), childStyles.current = {};
...(pressed ? processStyle(press ?? {}) : {}), if (hovered) assignChilds(childStyles.current, hover);
...(leftOvers?.style if (focused) assignChilds(childStyles.current, focus);
? typeof leftOvers?.style === "function" if (pressed) assignChilds(childStyles.current, press);
? processStyleList(leftOvers?.style({ hovered, focused, pressed }))
: processStyleList(leftOvers?.style) return [
: {}), processStyle(inline),
}); hovered && processStyle(hover?.self ?? {}),
focused && processStyle(focus?.self ?? {}),
pressed && processStyle(press?.self ?? {}),
leftOvers?.style &&
(typeof leftOvers?.style === "function"
? processStyleList(leftOvers?.style({ hovered, focused, pressed }))
: leftOvers?.style),
];
};
return { return {
...leftOvers, ...leftOvers,
style: ret, style: ret as StyleFunc<ViewStyle>,
}; // We must use a setTimeout since the child styles are computed inside the style function (called after onIn/onOut)
// NOTE: The props onIn/onOut are overriden here and the user can't use them. Might want to find a way arround that.
onPressIn: (e) => {
onPressIn?.call(null, e);
setTimeout(rerender);
},
onPressOut: (e) => {
onPressOut?.call(null, e);
setTimeout(rerender);
},
onHoverIn: (e) => {
onHoverIn?.call(null, e);
setTimeout(rerender);
},
onHoverOut: (e) => {
onHoverOut?.call(null, e);
setTimeout(rerender);
},
onFocus: (e) => {
onFocus?.call(null, e);
setTimeout(rerender);
},
onBlur: (e) => {
onBlur?.call(null, e);
setTimeout(rerender);
},
} satisfies PressableProps;
} else { } else {
const loStyles = return {
leftOvers?.style && typeof leftOvers?.style !== "function"
? processStyleList(leftOvers.style)
: {};
const ret = {
...leftOvers, ...leftOvers,
style: { ...processStyle(css), ...loStyles }, style: [processStyle(css), leftOvers?.style],
}; } as any;
return ret as any;
} }
}; };

View File

@@ -5,7 +5,7 @@
import { WithState, YoshikiStyle, StyleList } from "../type"; import { WithState, YoshikiStyle, StyleList } from "../type";
import { shorthandsFn } from "../shorthands"; import { shorthandsFn } from "../shorthands";
import { ImageStyle, StyleProp, TextStyle, ViewStyle } from "react-native"; import { ImageStyle, PressableProps, StyleProp, TextStyle, ViewStyle } from "react-native";
import { Theme } from "../theme"; import { Theme } from "../theme";
import { forceBreakpoint } from "../utils"; import { forceBreakpoint } from "../utils";
@@ -42,33 +42,18 @@ export type StyleFunc<Style> = (state: {
type AddLO<T, LO> = [LO] extends [never] ? T : Omit<LO, "style"> & T; type AddLO<T, LO> = [LO] extends [never] ? T : Omit<LO, "style"> & T;
declare function nativeCss<Leftover = never>( export type NativeStyle = ViewStyle | TextStyle | ImageStyle;
cssList: StyleList<EnhancedStyle<ViewStyle>>,
leftOvers?: Leftover & { style?: StyleProp<ViewStyle> | null },
): AddLO<{ style?: ViewStyle }, Leftover>;
declare function nativeCss<Leftover = never>(
cssList: StyleList<EnhancedStyle<ViewStyle> & Partial<WithState<EnhancedStyle<ViewStyle>>>>,
leftOvers?: Leftover & { style?: StyleProp<ViewStyle> | StyleFunc<StyleProp<ViewStyle>> | null },
): AddLO<{ style?: StyleFunc<ViewStyle> }, Leftover>;
declare function nativeCss<Leftover = never>( declare function nativeCss<Style extends NativeStyle, Leftover = never>(
cssList: StyleList<EnhancedStyle<TextStyle>>, cssList: StyleList<EnhancedStyle<Style> | string>,
leftOvers?: Leftover & { style?: StyleProp<TextStyle> | null }, leftOvers?: Leftover & { style?: StyleProp<Style> | null },
): AddLO<{ style?: TextStyle }, Leftover>; ): AddLO<{ style?: Style }, Leftover>;
declare function nativeCss<Leftover = never>(
cssList: StyleList<EnhancedStyle<TextStyle> & Partial<WithState<EnhancedStyle<TextStyle>>>>,
leftOvers?: Leftover & { style?: StyleProp<TextStyle> | StyleFunc<StyleProp<TextStyle>> | null },
): AddLO<{ style?: StyleFunc<TextStyle> }, Leftover>;
declare function nativeCss<Leftover = never>( declare function nativeCss<Style extends NativeStyle, Leftover = never>(
cssList: StyleList<EnhancedStyle<ImageStyle>>, cssList: StyleList<
leftOvers?: Leftover & { style?: StyleProp<ImageStyle> | null }, (EnhancedStyle<Style> & Partial<WithState<EnhancedStyle<NativeStyle>>>) | string
): AddLO<{ style?: ImageStyle }, Leftover>; >,
declare function nativeCss<Leftover = never>( leftOvers?: Leftover & { style?: StyleProp<Style> | StyleFunc<StyleProp<Style>> | null },
cssList: StyleList<EnhancedStyle<ImageStyle> & Partial<WithState<EnhancedStyle<ImageStyle>>>>, ): AddLO<PressableProps, Leftover>;
leftOvers?: Leftover & {
style?: StyleProp<ImageStyle> | StyleFunc<StyleProp<ImageStyle>> | null;
},
): AddLO<{ style?: StyleFunc<ImageStyle> }, Leftover>;
export type NativeCssFunc = typeof nativeCss; export type NativeCssFunc = typeof nativeCss;

View File

@@ -15,9 +15,9 @@ export type Breakpoints<Property> = {
}; };
export type WithState<Style> = { export type WithState<Style> = {
hover: Style; hover: { self?: Style; [key: string]: Style | undefined };
focus: Style; focus: { self?: Style; [key: string]: Style | undefined };
press: Style; press: { self?: Style; [key: string]: Style | undefined };
}; };
export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]; export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
@@ -33,3 +33,25 @@ export const processStyleList = <Style>(los: StyleList<Style>): Partial<Style> =
if (isReadonlyArray(los)) return los.reduce((acc, x) => ({ ...acc, ...processStyleList(x) }), {}); if (isReadonlyArray(los)) return los.reduce((acc, x) => ({ ...acc, ...processStyleList(x) }), {});
return los ? los : {}; return los ? los : {};
}; };
export const processStyleListWithChild = <Style>(
los: StyleList<Style | string>,
parent: Record<string, Style>,
): Partial<Style> => {
if (isReadonlyArray(los))
return los.reduce((acc, x) => ({ ...acc, ...processStyleListWithChild(x, parent) }), {});
if (!los) return {};
return typeof los === "string" ? parent[los] ?? {} : los;
};
export const assignChilds = <Style>(
target: Record<string, Style | undefined>,
style?: Record<string, Style | undefined>,
) => {
if (!style) return target;
for (const entry in style) {
if (entry === "self") continue;
if (!target[entry]) target[entry] = style[entry];
else target[entry] = { ...target[entry], ...style[entry] } as any;
}
return target;
};