Add pressable support on react native

This commit is contained in:
Zoe Roux
2022-11-16 00:04:02 +09:00
parent c7a1a3b598
commit 47c98245a3
6 changed files with 136 additions and 25 deletions

View File

@@ -17,6 +17,7 @@
""
],
1
]
],
"@typescript-eslint/ban-ts-ignore": "off"
}
}

View File

@@ -6,7 +6,7 @@
import { StatusBar } from "expo-status-bar";
import { Text, View } from "react-native";
import { registerRootComponent } from "expo";
import { Stylable, useYoshiki } from "yoshiki/native";
import { Pressable, Stylable, useYoshiki } from "yoshiki/native";
const CustomBox = ({ color, ...props }: { color: string } & Stylable) => {
const { css } = useYoshiki();
@@ -22,9 +22,29 @@ const BoxWithoutProps = (props: Stylable) => {
const { css } = useYoshiki();
return (
<View {...css({ backgroundColor: { xs: "#00ff00", md: "#ff0000" } }, props)}>
<Text>Text inside the box without props (green on small screens, red on bigs)</Text>
</View>
<Pressable
{...css(
{
backgroundColor: { xs: "#00ff00", md: "#ff0000" },
hover: { alignContent: "center" },
press: { alignContent: "center" },
focus: { alignContent: "center" },
},
props,
)}
>
<Text
{...css(
{
backgroundColor: { xs: "#00ff00", md: "#ff0000" },
hover: { alignContent: "center" },
},
props,
)}
>
Text inside the box without props (green on small screens, red on bigs)
</Text>
</Pressable>
);
};

View File

@@ -13,14 +13,9 @@
"bugs": {
"url": "https://github.com/AnonymusRaccoon/yoshiki/issues"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "src/index.ts",
"types": "src/index.ts",
"source": "src/index.ts",
"exports": {
".": "./dist/index.ts",
"./native": "./dist/native/index.ts",
"./web": "./dist/index.ts"
},
"scripts": {
"build": "tsc --build ."
},

View File

@@ -54,28 +54,70 @@ const propertyMapper = <
return [[key, value]];
};
type WithState<Style> = {
hover: Style;
focus: Style;
press: Style;
};
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]
const hasState = <Style,>(obj: unknown): obj is WithState<Style> => {
if (!obj || typeof obj !== "object") return false;
return "hover" in obj || "focus" in obj || "press" in obj;
};
export const useYoshiki = () => {
const breakpoint = useBreakpoint();
const theme = useTheme();
return {
css: <Style extends ViewStyle | TextStyle | ImageStyle>(
css: EnhancedStyle<Style>,
css: <
Style extends ViewStyle | TextStyle | ImageStyle,
State extends Partial<WithState<EnhancedStyle<Style>>> | Record<string, never>,
>(
css: EnhancedStyle<Style> & State,
leftOvers?: { style?: Style },
): { style: Style } => {
): State extends AtLeastOne<WithState<unknown>>
? { style: (state: { pressed: boolean; focused: boolean; hovered: boolean }) => Style }
: { style: Style } => {
const { style, ...leftOverProps } = leftOvers ?? {};
// @ts-ignore propertyMapper always returns key that are valid for the current Style
const inline: Style = Object.fromEntries(
Object.entries(css).flatMap(([key, value]) =>
propertyMapper(key, value, { breakpoint, theme }),
),
);
return {
style: { ...inline, ...style },
...leftOverProps,
const processStyle = (styleList: EnhancedStyle<Style>): Style => {
// @ts-ignore propertyMapper always returns key that are valid for the current Style
return Object.fromEntries(
Object.entries(styleList).flatMap(([key, value]) =>
propertyMapper(key, value, { breakpoint, theme }),
),
);
};
if (hasState<Style>(css)) {
const { hover, focus, press, ...inline } = css;
const ret: {
style: (state: { hovered: boolean; focused: boolean; pressed: boolean }) => Style;
} = {
style: ({ hovered, focused, pressed }) => ({
// @ts-ignore EnhancedStyle<Style> is not assignable to EnhancedStyle<Style>...
...processStyle(inline),
...(hovered ? hover : {}),
...(focused ? focus : {}),
...(pressed ? press : {}),
...style,
}),
...leftOverProps,
};
// @ts-ignore Ts is not able to identify the type of the return type (the condition throws him off).
return ret;
} else {
const ret: { style: Style } = {
style: { ...processStyle(css), ...style },
...leftOverProps,
};
// @ts-ignore Ts is not able to identify the type of the return type (the condition throws him off).
return ret;
}
},
theme: theme,
};

View File

@@ -0,0 +1,51 @@
//
// Copyright (c) Zoe Roux and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
//
import { useState } from "react";
import { Pressable as RPressable, PressableProps, StyleProp, ViewStyle } from "react-native";
export const Pressable = ({
children,
onHoverIn,
onHoverOut,
onFocus,
onBlur,
style,
...props
}: {
style:
| PressableProps["style"]
| ((state: { pressed: boolean; hovered: boolean; focused: boolean }) => StyleProp<ViewStyle>);
} & PressableProps) => {
const [hovered, setHover] = useState(false);
const [focused, setFocus] = useState(false);
return (
<RPressable
onHoverIn={(e) => {
onHoverIn?.call(null, e);
setHover(true);
}}
onHoverOut={(e) => {
onHoverOut?.call(null, e);
setHover(false);
}}
onFocus={(e) => {
onFocus?.call(null, e);
setFocus(true);
}}
onBlur={(e) => {
onBlur?.call(null, e);
setFocus(false);
}}
style={
typeof style === "function" ? ({ pressed }) => style({ pressed, hovered, focused }) : style
}
{...props}
>
{children}
</RPressable>
);
};

View File

@@ -6,3 +6,5 @@
export { type Theme, breakpoints, useTheme, ThemeProvider } from "../theme";
export { useYoshiki, type Stylable } from "./generator";
export { Pressable } from "./hover";