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",
"slug": "expo-example",
"version": "1.0.0",
"orientation": "portrait",
"orientation": "default",
"icon": "./assets/icon.png",
"entryPoint": "./src/app.tsx",
"userInterfaceStyle": "light",

View File

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

View File

@@ -3,12 +3,20 @@
// 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, YoshikiStyle, hasState, processStyleList } from "../type";
import {
Breakpoints,
YoshikiStyle,
hasState,
processStyleList,
processStyleListWithChild,
assignChilds,
} from "../type";
import { isBreakpoints } from "../utils";
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 { width } = useWindowDimensions();
@@ -46,12 +54,19 @@ const propertyMapper = (
return [[key, value]];
};
const useForceRerender = () => {
return useReducer((x) => x + 1, 0)[1];
};
export const useYoshiki = () => {
const breakpoint = useBreakpoint();
const theme = useTheme();
const rerender = useForceRerender();
const childStyles = useRef<Record<string, NativeStyle | undefined>>({});
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 ret = Object.fromEntries(
@@ -62,35 +77,63 @@ export const useYoshiki = () => {
return ret;
};
if (hasState<Record<string, unknown>>(css)) {
if (hasState<Record<string, ViewStyle>>(css)) {
const { hover, focus, press, ...inline } = css;
const ret: StyleFunc<unknown> = ({ hovered, focused, pressed }) => ({
...processStyle(inline),
...(hovered ? processStyle(hover ?? {}) : {}),
...(focused ? processStyle(focus ?? {}) : {}),
...(pressed ? processStyle(press ?? {}) : {}),
...(leftOvers?.style
? typeof leftOvers?.style === "function"
? processStyleList(leftOvers?.style({ hovered, focused, pressed }))
: processStyleList(leftOvers?.style)
: {}),
});
const { onPressIn, onPressOut, onHoverIn, onHoverOut, onFocus, onBlur } =
leftOvers as PressableProps;
const ret: StyleFunc<unknown> = ({ hovered, focused, pressed }) => {
childStyles.current = {};
if (hovered) assignChilds(childStyles.current, hover);
if (focused) assignChilds(childStyles.current, focus);
if (pressed) assignChilds(childStyles.current, press);
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 {
...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 {
const loStyles =
leftOvers?.style && typeof leftOvers?.style !== "function"
? processStyleList(leftOvers.style)
: {};
const ret = {
return {
...leftOvers,
style: { ...processStyle(css), ...loStyles },
};
return ret as any;
style: [processStyle(css), leftOvers?.style],
} as any;
}
};

View File

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

View File

@@ -15,9 +15,9 @@ export type Breakpoints<Property> = {
};
export type WithState<Style> = {
hover: Style;
focus: Style;
press: Style;
hover: { self?: Style; [key: string]: Style | undefined };
focus: { self?: Style; [key: string]: Style | undefined };
press: { self?: Style; [key: string]: Style | undefined };
};
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) }), {});
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;
};