From 1d56abb9840761099e5e45d65fc3a0f5a22c381c Mon Sep 17 00:00:00 2001 From: Grant Sander Date: Wed, 6 Jul 2022 10:19:33 -0400 Subject: [PATCH 1/7] Configuration options for breakpoints --- packages/core/src/createStyleBuilder.tsx | 9 ++++++++- packages/sample/{styled.ts => styled.tsx} | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) rename packages/sample/{styled.ts => styled.tsx} (74%) diff --git a/packages/core/src/createStyleBuilder.tsx b/packages/core/src/createStyleBuilder.tsx index 30c52a0..8d9c755 100644 --- a/packages/core/src/createStyleBuilder.tsx +++ b/packages/core/src/createStyleBuilder.tsx @@ -31,18 +31,22 @@ import { applyOpacityToColor } from "./utils/applyOpacityToColor"; export const createStyleBuilder = < Theme extends ThemeConstraints, ThemeExt extends ThemeConstraints, + Breakpoints extends Record, ExtraStyleHandlers extends StyleHandlerSet | undefined = undefined >({ extraHandlers, overrideTheme, extendTheme, baseFontSize = 14, + breakpoints, }: { extraHandlers?: ExtraStyleHandlers; overrideTheme?: Theme | ((args: { baseFontSize: number }) => Theme); extendTheme?: ThemeExt | ((args: { baseFontSize: number }) => ThemeExt); baseFontSize?: number; + breakpoints?: Breakpoints; } = {}) => { + console.log(breakpoints); const cache = new SimpleConstrainedCache({ maxNumRecords: 400 }); const baseTheme = createDefaultTheme({ baseFontSize }); const mergedTheme = mergeThemes({ @@ -402,13 +406,16 @@ export const createStyleBuilder = < /** * Core hook to apply styles based on props/style object */ + type ResponsiveClasses = { + [Key in `${NonSymbol}Classes`]?: CnArg[]; + }; const useStyles = ({ classes = [], darkClasses = [], }: { classes?: CnArg[]; darkClasses?: CnArg[]; - }) => { + } & ResponsiveClasses) => { const { isDarkMode } = React.useContext(StyleContext); return React.useMemo(() => { const allClasses = [...classes].concat(isDarkMode ? darkClasses : []); diff --git a/packages/sample/styled.ts b/packages/sample/styled.tsx similarity index 74% rename from packages/sample/styled.ts rename to packages/sample/styled.tsx index 3245cec..bc3a4f1 100644 --- a/packages/sample/styled.ts +++ b/packages/sample/styled.tsx @@ -11,6 +11,11 @@ export const { makeStyledComponent, styles, styled, useStyles } = tiny: [0.7 * baseFontSize, 1 * baseFontSize], }, }), + breakpoints: { + sm: 300, + md: 450, + lg: 600, + }, // baseFontSize: 20, }); @@ -20,3 +25,14 @@ export const StyledTouchableOpacity = makeStyledComponent( Animated.createAnimatedComponent(TouchableOpacity) ); export const StyledImage = makeStyledComponent(Image); + +export const MyComp = () => { + const styles = useStyles({ + classes: ["p:1"], + smClasses: ["p:2"], + mdClasses: ["p:4"], + lgClasses: ["p:40"], + }); + + return ; +}; From f10bbaaa137de5a4a819602f8abf0dbad83c0a62 Mon Sep 17 00:00:00 2001 From: Grant Sander Date: Wed, 6 Jul 2022 12:46:04 -0400 Subject: [PATCH 2/7] Remove StyleProvider requirement, update tests --- packages/core/src/StyleProvider.tsx | 23 +---- packages/core/src/createStyleBuilder.test.tsx | 68 +++++++------ packages/core/src/createStyleBuilder.tsx | 44 ++++++--- packages/core/src/darkMode.test.ts | 83 ++++++++++++++++ packages/core/src/darkMode.test.tsx | 95 ------------------- .../handlers/createAspectRatioHandler.test.ts | 8 +- .../src/handlers/createBorderHandlers.test.ts | 8 +- .../src/handlers/createColorHandlers.test.ts | 6 -- .../handlers/createOpacityHandlers.test.ts | 6 -- .../handlers/createRoundedHandlers.test.ts | 8 +- .../src/handlers/createShadowHandlers.test.ts | 13 ++- .../handlers/createSpacingHandlers.test.ts | 8 +- .../handlers/createTypographyHandlers.test.ts | 8 +- .../src/handlers/defaultFlexHandlers.test.ts | 8 +- .../core/src/handlers/imageHandlers.test.ts | 8 +- .../src/handlers/positionHandlers.test.ts | 8 +- .../core/src/utils/SimpleEventEmitter.test.ts | 41 ++++++++ packages/core/src/utils/SimpleEventEmitter.ts | 20 ++++ packages/core/src/utils/SimpleStore.ts | 29 ++++++ packages/sample/App.tsx | 7 +- packages/sample/styled.tsx | 5 +- vitest.config.js | 1 + vitest.setup.ts | 15 +++ 23 files changed, 284 insertions(+), 236 deletions(-) create mode 100644 packages/core/src/darkMode.test.ts delete mode 100644 packages/core/src/darkMode.test.tsx create mode 100644 packages/core/src/utils/SimpleEventEmitter.test.ts create mode 100644 packages/core/src/utils/SimpleEventEmitter.ts create mode 100644 packages/core/src/utils/SimpleStore.ts create mode 100644 vitest.setup.ts diff --git a/packages/core/src/StyleProvider.tsx b/packages/core/src/StyleProvider.tsx index 66244df..c693069 100644 --- a/packages/core/src/StyleProvider.tsx +++ b/packages/core/src/StyleProvider.tsx @@ -1,29 +1,14 @@ import * as React from "react"; -import { useColorScheme } from "react-native"; - -export const StyleContext = React.createContext({ - isDarkMode: false, -}); type StyleProviderProps = { colorScheme?: "light" | "dark" | "auto"; }; +/** + * TODO: Deprecate this. No longer needed, but leaving for now. + */ export const StyleProvider = ({ children, - colorScheme = "auto", }: React.PropsWithChildren) => { - const systemColorScheme = useColorScheme(); - - const value = React.useMemo>(() => { - return { - isDarkMode: - colorScheme === "dark" || - (colorScheme === "auto" && systemColorScheme === "dark"), - }; - }, [colorScheme, systemColorScheme]); - - return ( - {children} - ); + return <>{children}; }; diff --git a/packages/core/src/createStyleBuilder.test.tsx b/packages/core/src/createStyleBuilder.test.tsx index 1aabb6b..d8fb12f 100644 --- a/packages/core/src/createStyleBuilder.test.tsx +++ b/packages/core/src/createStyleBuilder.test.tsx @@ -1,22 +1,28 @@ import * as React from "react"; -import { vi, describe, it, expect, beforeEach } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createStyleBuilder } from "./createStyleBuilder"; import { DefaultTheme } from "./theme"; -import { Text } from "react-native"; +import { StyleSheet, Text } from "react-native"; import { render } from "@testing-library/react-native"; -import { StyleProvider } from "./StyleProvider"; -import { PropsWithChildren } from "react"; import { renderHook } from "@testing-library/react-hooks"; +import create = StyleSheet.create; let colorScheme = "light"; const MockText = vi.fn(); vi.mock("react-native", () => ({ + Appearance: { + getColorScheme: () => colorScheme, + addChangeListener: () => ({ + remove: () => { + /* ... */ + }, + }), + }, StyleSheet: { hairlineWidth: 0.5, }, Text: (...args: unknown[]) => MockText(...args), - useColorScheme: () => colorScheme, })); const C = DefaultTheme.spacing; @@ -125,12 +131,10 @@ describe("createStyleBuilder().useStyles", () => { }); it("should add darkMode styles if in dark mode", () => { - const { useStyles } = createStyleBuilder(); colorScheme = "dark"; - const { result } = renderHook( - () => - useStyles({ classes: ["bg:red-100"], darkClasses: ["color:blue-100"] }), - { wrapper: Wrapper } + const { useStyles } = createStyleBuilder(); + const { result } = renderHook(() => + useStyles({ classes: ["bg:red-100"], darkClasses: ["color:blue-100"] }) ); expect(result.current).toEqual({ @@ -159,14 +163,13 @@ describe("createStyleBuilder().makeStyledComponent", () => { }); it("creates a wrapped component that supports dark mode", () => { + colorScheme = "dark"; const { makeStyledComponent } = createStyleBuilder(); const StyledText = makeStyledComponent(Text); - colorScheme = "dark"; render( Hello world - , - { wrapper: Wrapper } + ); // @ts-expect-error HALP. How do I type this mock? @@ -179,12 +182,12 @@ describe("createStyleBuilder().makeStyledComponent", () => { }); describe("createStyleBuilder().styled", () => { - const { styled } = createStyleBuilder(); beforeEach(() => { colorScheme = "light"; }); it("wraps a component and adds style.", () => { + const { styled } = createStyleBuilder(); const MyText = styled(Text)("color:red-100"); render(Hey world); @@ -195,6 +198,7 @@ describe("createStyleBuilder().styled", () => { }); it("accepts configuration object", () => { + const { styled } = createStyleBuilder(); const MyText = styled(Text)({ classes: ["color:red-200"], }); @@ -207,19 +211,22 @@ describe("createStyleBuilder().styled", () => { }); it("handles dark-mode classes", () => { - const MyText = styled(Text)({ - classes: ["color:red-100"], - darkClasses: ["color:blue-100"], - }); + const getMyText = () => + createStyleBuilder().styled(Text)({ + classes: ["color:red-100"], + darkClasses: ["color:blue-100"], + }); + const MyText = getMyText(); - render(Hey world, { wrapper: Wrapper }); + render(Hey world); // @ts-expect-error HALP. How do I type this mock? expect(MockText.calls?.at(-1)?.[0].style[0]).toEqual({ color: DefaultTheme.colors["red-100"], }); colorScheme = "dark"; - render(Hey world, { wrapper: Wrapper }); + const MyText2 = getMyText(); + render(Hey world); // @ts-expect-error HALP. How do I type this mock? expect(MockText.calls?.at(-1)?.[0].style[0]).toEqual({ color: DefaultTheme.colors["blue-100"], @@ -227,18 +234,20 @@ describe("createStyleBuilder().styled", () => { }); it("handles function as an argument to classes and darkClasses", () => { - const MyText = styled(Text)<{ isItalic?: boolean }>({ - classes: ({ isItalic }) => [isItalic && "italic"], - darkClasses: ({ isItalic }) => [isItalic && "color:red-100"], - }); + const getMyText = () => + createStyleBuilder().styled(Text)<{ isItalic?: boolean }>({ + classes: ({ isItalic }) => [isItalic && "italic"], + darkClasses: ({ isItalic }) => [isItalic && "color:red-100"], + }); + const MyText = getMyText(); // no isItalic prop - render(Hello world, { wrapper: Wrapper }); + render(Hello world); // @ts-expect-error HALP. How do I type this mock? expect(MockText.calls?.at(-1)?.[0].style[0]).toEqual({}); // with isItalic prop - render(Hello world, { wrapper: Wrapper }); + render(Hello world); // @ts-expect-error HALP. How do I type this mock? expect(MockText.calls?.at(-1)?.[0].style[0]).toEqual({ fontStyle: "italic", @@ -246,7 +255,8 @@ describe("createStyleBuilder().styled", () => { // Dark mode colorScheme = "dark"; - render(Hello world, { wrapper: Wrapper }); + const MyText2 = getMyText(); + render(Hello world); // @ts-expect-error HALP. How do I type this mock? expect(MockText.calls?.at(-1)?.[0].style[0]).toEqual({ fontStyle: "italic", @@ -254,7 +264,3 @@ describe("createStyleBuilder().styled", () => { }); }); }); - -const Wrapper = ({ children }: PropsWithChildren) => ( - {children} -); diff --git a/packages/core/src/createStyleBuilder.tsx b/packages/core/src/createStyleBuilder.tsx index 8d9c755..db144b2 100644 --- a/packages/core/src/createStyleBuilder.tsx +++ b/packages/core/src/createStyleBuilder.tsx @@ -8,10 +8,9 @@ import { StyleHandlerSet, ThemeConstraints, } from "./types"; -import { StyleContext } from "./StyleProvider"; import { SimpleConstrainedCache } from "./utils/SimpleConstrainedCache"; import { createDefaultTheme } from "./theme"; -import { FlexStyle, ImageStyle, TextStyle } from "react-native"; +import { Appearance, FlexStyle, ImageStyle, TextStyle } from "react-native"; import { mergeThemes } from "./utils/mergeThemes"; import { createColorHandlers } from "./handlers/createColorHandlers"; import { createSpacingHandlers } from "./handlers/createSpacingHandlers"; @@ -24,6 +23,7 @@ import { cleanMaybeNumberString } from "./utils/cleanMaybeNumberString"; import { createTypographyHandlers } from "./handlers/createTypographyHandlers"; import { flattenClassNameArgs } from "./utils/flattenClassNameArgs"; import { applyOpacityToColor } from "./utils/applyOpacityToColor"; +import { SimpleStore } from "./utils/SimpleStore"; /** * Core builder fn. Takes in a set of handlers, and gives back a hook and component-builder. @@ -31,22 +31,20 @@ import { applyOpacityToColor } from "./utils/applyOpacityToColor"; export const createStyleBuilder = < Theme extends ThemeConstraints, ThemeExt extends ThemeConstraints, - Breakpoints extends Record, ExtraStyleHandlers extends StyleHandlerSet | undefined = undefined >({ extraHandlers, overrideTheme, extendTheme, baseFontSize = 14, - breakpoints, + colorScheme = "auto", }: { extraHandlers?: ExtraStyleHandlers; overrideTheme?: Theme | ((args: { baseFontSize: number }) => Theme); extendTheme?: ThemeExt | ((args: { baseFontSize: number }) => ThemeExt); baseFontSize?: number; - breakpoints?: Breakpoints; + colorScheme?: "light" | "dark" | "auto"; } = {}) => { - console.log(breakpoints); const cache = new SimpleConstrainedCache({ maxNumRecords: 400 }); const baseTheme = createDefaultTheme({ baseFontSize }); const mergedTheme = mergeThemes({ @@ -61,6 +59,20 @@ export const createStyleBuilder = < : extendTheme, }); + /** + * Internal state for dark mode + */ + let systemColorScheme = Appearance.getColorScheme(); + const isDarkModeStore = new SimpleStore( + () => + colorScheme === "dark" || + (colorScheme === "auto" && systemColorScheme === "dark") + ); + const { remove } = Appearance.addChangeListener((r) => { + systemColorScheme = r.colorScheme; + isDarkModeStore.reeval(); + }); + type DefaultTheme = typeof baseTheme; type GetKey< UserThemeConstraints, @@ -406,17 +418,14 @@ export const createStyleBuilder = < /** * Core hook to apply styles based on props/style object */ - type ResponsiveClasses = { - [Key in `${NonSymbol}Classes`]?: CnArg[]; - }; const useStyles = ({ classes = [], darkClasses = [], }: { classes?: CnArg[]; darkClasses?: CnArg[]; - } & ResponsiveClasses) => { - const { isDarkMode } = React.useContext(StyleContext); + }) => { + const isDarkMode = isDarkModeStore.useStoreValue(); return React.useMemo(() => { const allClasses = [...classes].concat(isDarkMode ? darkClasses : []); return styles(...allClasses); @@ -529,7 +538,18 @@ export const createStyleBuilder = < }; }; - return { styles, styled, useStyles, makeStyledComponent, theme: mergedTheme }; + const teardown = () => { + remove(); + }; + + return { + styles, + styled, + useStyles, + makeStyledComponent, + theme: mergedTheme, + teardown, + }; }; const HandlerArgRegExp = /^(.+):(.+)$/; diff --git a/packages/core/src/darkMode.test.ts b/packages/core/src/darkMode.test.ts new file mode 100644 index 0000000..c2b1b45 --- /dev/null +++ b/packages/core/src/darkMode.test.ts @@ -0,0 +1,83 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createStyleBuilder } from "./createStyleBuilder"; +import { renderHook } from "@testing-library/react-hooks"; +import { DefaultTheme } from "./theme"; + +let colorScheme = "light"; +vi.mock("react-native", () => ({ + Appearance: { + getColorScheme: () => colorScheme, + addChangeListener: () => ({ + remove: () => { + /* ... */ + }, + }), + }, + StyleSheet: { + hairlineWidth: 0.5, + }, +})); + +describe("Dark mode support", () => { + beforeEach(() => { + colorScheme = "light"; + }); + + it("returns only base styles in light mode", () => { + colorScheme = "light"; + const { useStyles } = createStyleBuilder(); + const { result } = renderHook(() => { + return useStyles({ + classes: ["p:0"], + darkClasses: ["m:3"], + }); + }); + + expect(result.current).toEqual({ padding: 0 }); + }); + + it("returns base+dark styles in dark mode", () => { + colorScheme = "dark"; + const { useStyles } = createStyleBuilder(); + const { result } = renderHook(() => { + return useStyles({ + classes: ["p:0"], + darkClasses: ["p:3", "m:3"], + }); + }); + + expect(result.current).toEqual({ + padding: DefaultTheme.spacing["3"], + margin: DefaultTheme.spacing["3"], + }); + }); + + it("allows StyleProvider to override system default (dark system, light override)", () => { + colorScheme = "dark"; + const { useStyles } = createStyleBuilder({ colorScheme: "light" }); + const { result } = renderHook(() => { + return useStyles({ + classes: ["p:0"], + darkClasses: ["p:3", "m:3"], + }); + }); + + expect(result.current).toEqual({ padding: 0 }); + }); + + it("allows StyleProvider to override system default (light system, dark override)", () => { + colorScheme = "light"; + const { useStyles } = createStyleBuilder({ colorScheme: "dark" }); + const { result } = renderHook(() => { + return useStyles({ + classes: ["p:0"], + darkClasses: ["m:3"], + }); + }); + + expect(result.current).toEqual({ + padding: 0, + margin: DefaultTheme.spacing["3"], + }); + }); +}); diff --git a/packages/core/src/darkMode.test.tsx b/packages/core/src/darkMode.test.tsx deleted file mode 100644 index 3972874..0000000 --- a/packages/core/src/darkMode.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import * as React from "react"; -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { createStyleBuilder } from "./createStyleBuilder"; -import { StyleProvider } from "./StyleProvider"; -import { PropsWithChildren, ComponentProps } from "react"; -import { renderHook } from "@testing-library/react-hooks"; -import { DefaultTheme } from "./theme"; - -let colorScheme = "light"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, - useColorScheme: () => colorScheme, -})); - -const { useStyles } = createStyleBuilder(); - -const makeWrapper = - (colorScheme: ComponentProps["colorScheme"]) => - ({ children }: PropsWithChildren) => { - return {children}; - }; - -describe("Dark mode support", () => { - beforeEach(() => { - colorScheme = "light"; - }); - - it("returns only base styles in light mode", () => { - colorScheme = "light"; - const { result } = renderHook( - () => { - return useStyles({ - classes: ["p:0"], - darkClasses: ["m:3"], - }); - }, - { wrapper: makeWrapper("auto") } - ); - - expect(result.current).toEqual({ padding: 0 }); - }); - - it("returns base+dark styles in dark mode", () => { - colorScheme = "dark"; - const { result } = renderHook( - () => { - return useStyles({ - classes: ["p:0"], - darkClasses: ["p:3", "m:3"], - }); - }, - { wrapper: makeWrapper("auto") } - ); - - expect(result.current).toEqual({ - padding: DefaultTheme.spacing["3"], - margin: DefaultTheme.spacing["3"], - }); - }); - - it("allows StyleProvider to override system default (dark system, light override)", () => { - colorScheme = "dark"; - const { result } = renderHook( - () => { - return useStyles({ - classes: ["p:0"], - darkClasses: ["p:3", "m:3"], - }); - }, - { wrapper: makeWrapper("light") } // 👈 override system default - ); - - expect(result.current).toEqual({ padding: 0 }); - }); - - it("allows StyleProvider to override system default (light system, dark override)", () => { - colorScheme = "light"; - const { result } = renderHook( - () => { - return useStyles({ - classes: ["p:0"], - darkClasses: ["m:3"], - }); - }, - { wrapper: makeWrapper("dark") } // 👈 override system default - ); - - expect(result.current).toEqual({ - padding: 0, - margin: DefaultTheme.spacing["3"], - }); - }); -}); diff --git a/packages/core/src/handlers/createAspectRatioHandler.test.ts b/packages/core/src/handlers/createAspectRatioHandler.test.ts index b158789..f4319b4 100644 --- a/packages/core/src/handlers/createAspectRatioHandler.test.ts +++ b/packages/core/src/handlers/createAspectRatioHandler.test.ts @@ -1,12 +1,6 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - describe("createAspectRatioHandlers", () => { const { styles } = createStyleBuilder({}); diff --git a/packages/core/src/handlers/createBorderHandlers.test.ts b/packages/core/src/handlers/createBorderHandlers.test.ts index 0478870..65ffa78 100644 --- a/packages/core/src/handlers/createBorderHandlers.test.ts +++ b/packages/core/src/handlers/createBorderHandlers.test.ts @@ -1,13 +1,7 @@ -import { vi, describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; import { DefaultTheme } from "../theme"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - const { styles } = createStyleBuilder({}); const C = DefaultTheme.borderSizes; diff --git a/packages/core/src/handlers/createColorHandlers.test.ts b/packages/core/src/handlers/createColorHandlers.test.ts index d9683dd..6e2634c 100644 --- a/packages/core/src/handlers/createColorHandlers.test.ts +++ b/packages/core/src/handlers/createColorHandlers.test.ts @@ -2,12 +2,6 @@ import { vi, describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; import { DefaultTheme } from "../theme"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - const { styles } = createStyleBuilder({}); const C = DefaultTheme.colors; diff --git a/packages/core/src/handlers/createOpacityHandlers.test.ts b/packages/core/src/handlers/createOpacityHandlers.test.ts index f05b909..07f56a8 100644 --- a/packages/core/src/handlers/createOpacityHandlers.test.ts +++ b/packages/core/src/handlers/createOpacityHandlers.test.ts @@ -2,12 +2,6 @@ import { vi, describe, it, expect } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; import { DefaultTheme } from "../theme"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - const { styles } = createStyleBuilder({ extendTheme: { colors: { diff --git a/packages/core/src/handlers/createRoundedHandlers.test.ts b/packages/core/src/handlers/createRoundedHandlers.test.ts index c31f073..efaba2c 100644 --- a/packages/core/src/handlers/createRoundedHandlers.test.ts +++ b/packages/core/src/handlers/createRoundedHandlers.test.ts @@ -1,13 +1,7 @@ -import { vi, describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; import { DefaultTheme } from "../theme"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - const { styles } = createStyleBuilder({}); const C = DefaultTheme.borderRadii; diff --git a/packages/core/src/handlers/createShadowHandlers.test.ts b/packages/core/src/handlers/createShadowHandlers.test.ts index 833b963..07acc6f 100644 --- a/packages/core/src/handlers/createShadowHandlers.test.ts +++ b/packages/core/src/handlers/createShadowHandlers.test.ts @@ -4,7 +4,18 @@ import { DefaultTheme } from "../theme"; let platform = "android"; -vi.mock("react-native", () => ({ +console.log(vi.importMock("react-native")); + +vi.mock("react-native", async () => ({ + // TODO: dedup this. + Appearance: { + getColorScheme: () => "dark", + addChangeListener: () => ({ + remove: () => { + /* ... */ + }, + }), + }, StyleSheet: { hairlineWidth: 0.5, }, diff --git a/packages/core/src/handlers/createSpacingHandlers.test.ts b/packages/core/src/handlers/createSpacingHandlers.test.ts index dba4d1b..fcbc123 100644 --- a/packages/core/src/handlers/createSpacingHandlers.test.ts +++ b/packages/core/src/handlers/createSpacingHandlers.test.ts @@ -1,13 +1,7 @@ -import { vi, describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; import { DefaultTheme } from "../theme"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - const { styles } = createStyleBuilder({}); const C = DefaultTheme.spacing; diff --git a/packages/core/src/handlers/createTypographyHandlers.test.ts b/packages/core/src/handlers/createTypographyHandlers.test.ts index 1c23368..8fd3000 100644 --- a/packages/core/src/handlers/createTypographyHandlers.test.ts +++ b/packages/core/src/handlers/createTypographyHandlers.test.ts @@ -1,13 +1,7 @@ -import { vi, describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; import { DefaultTheme } from "../theme"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - const { styles } = createStyleBuilder(); const FW = DefaultTheme.fontWeights; const FS = DefaultTheme.fontSizes; diff --git a/packages/core/src/handlers/defaultFlexHandlers.test.ts b/packages/core/src/handlers/defaultFlexHandlers.test.ts index af2170e..b8f8621 100644 --- a/packages/core/src/handlers/defaultFlexHandlers.test.ts +++ b/packages/core/src/handlers/defaultFlexHandlers.test.ts @@ -1,12 +1,6 @@ -import { vi, describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - const { styles } = createStyleBuilder({}); describe("defaultFlexHandlers", () => { diff --git a/packages/core/src/handlers/imageHandlers.test.ts b/packages/core/src/handlers/imageHandlers.test.ts index 630101b..269bd20 100644 --- a/packages/core/src/handlers/imageHandlers.test.ts +++ b/packages/core/src/handlers/imageHandlers.test.ts @@ -1,12 +1,6 @@ -import { vi, describe, it, expect } from "vitest"; +import { describe, it, expect } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - describe("imageHandlers", () => { const { styles } = createStyleBuilder(); const cases: [Parameters[0], object][] = [ diff --git a/packages/core/src/handlers/positionHandlers.test.ts b/packages/core/src/handlers/positionHandlers.test.ts index 2f92f1b..d1eef9b 100644 --- a/packages/core/src/handlers/positionHandlers.test.ts +++ b/packages/core/src/handlers/positionHandlers.test.ts @@ -1,12 +1,6 @@ -import { vi, describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; -vi.mock("react-native", () => ({ - StyleSheet: { - hairlineWidth: 0.5, - }, -})); - const { styles } = createStyleBuilder({}); describe("defaultPositionHandlers", () => { diff --git a/packages/core/src/utils/SimpleEventEmitter.test.ts b/packages/core/src/utils/SimpleEventEmitter.test.ts new file mode 100644 index 0000000..5879441 --- /dev/null +++ b/packages/core/src/utils/SimpleEventEmitter.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, vi } from "vitest"; +import { SimpleEventEmitter } from "./SimpleEventEmitter"; + +describe("SimpleEventEmitter", () => { + it("should register listeners", () => { + const ee = new SimpleEventEmitter(); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + ee.subscribe(listener1); + ee.subscribe(listener2); + + ee.emit(3); + + expect(listener1).toHaveBeenCalledWith(3); + expect(listener2).toHaveBeenCalledWith(3); + }); + + it("should unsubscribe listeners", () => { + const ee = new SimpleEventEmitter(); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + // Subscribe both listeners + const { unsubscribe: unsubscribe1 } = ee.subscribe(listener1); + ee.subscribe(listener2); + + // First emit should be picked up by both + ee.emit(3); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); + + // Unsubscribe the first + unsubscribe1(); + + // Second emit should only be picked up by second + ee.emit(5); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/core/src/utils/SimpleEventEmitter.ts b/packages/core/src/utils/SimpleEventEmitter.ts new file mode 100644 index 0000000..e9135ff --- /dev/null +++ b/packages/core/src/utils/SimpleEventEmitter.ts @@ -0,0 +1,20 @@ +export class SimpleEventEmitter { + cbs: ((x: S) => void)[] = []; + + subscribe(cb: (x: S) => void) { + this.cbs.push(cb); + + return { + unsubscribe: () => { + const indexToRemove = this.cbs?.indexOf(cb) ?? -1; + if (indexToRemove >= 0) this.cbs?.splice(indexToRemove, 1); + }, + }; + } + + emit(x: S) { + this.cbs.forEach((cb) => { + cb(x); + }); + } +} diff --git a/packages/core/src/utils/SimpleStore.ts b/packages/core/src/utils/SimpleStore.ts new file mode 100644 index 0000000..c45ada6 --- /dev/null +++ b/packages/core/src/utils/SimpleStore.ts @@ -0,0 +1,29 @@ +import * as React from "react"; +import { SimpleEventEmitter } from "./SimpleEventEmitter"; + +export class SimpleStore { + #getValue!: () => S; + #ee = new SimpleEventEmitter(); + + constructor(getValue: () => S) { + this.#getValue = getValue; + } + + reeval() { + this.#ee.emit(this.#getValue()); + } + + useStoreValue() { + const [val, setVal] = React.useState(() => this.#getValue()); + + React.useEffect(() => { + const { unsubscribe } = this.#ee.subscribe((v) => { + setVal(v); + }); + + return unsubscribe; + }, []); + + return val; + } +} diff --git a/packages/sample/App.tsx b/packages/sample/App.tsx index abb0932..2bcb43d 100644 --- a/packages/sample/App.tsx +++ b/packages/sample/App.tsx @@ -1,11 +1,6 @@ import * as React from "react"; -import { StyleProvider } from "react-native-zephyr"; import { AppBody } from "./AppBody"; export default function App() { - return ( - - - - ); + return ; } diff --git a/packages/sample/styled.tsx b/packages/sample/styled.tsx index bc3a4f1..45e5196 100644 --- a/packages/sample/styled.tsx +++ b/packages/sample/styled.tsx @@ -3,13 +3,10 @@ import { View, Text, TouchableOpacity, Animated, Image } from "react-native"; export const { makeStyledComponent, styles, styled, useStyles } = createStyleBuilder({ - extendTheme: ({ baseFontSize }) => ({ + extendTheme: () => ({ colors: { ...extractTwColor({ twColor: "fuchsia", name: "brown" }), }, - fontSizes: { - tiny: [0.7 * baseFontSize, 1 * baseFontSize], - }, }), breakpoints: { sm: 300, diff --git a/vitest.config.js b/vitest.config.js index acc6ee3..d869c13 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -3,5 +3,6 @@ export default { deps: { inline: ["react-native"], }, + setupFiles: ["./vitest.setup.ts"], }, }; diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..59331b4 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,15 @@ +import { vi } from "vitest"; + +vi.mock("react-native", () => ({ + Appearance: { + getColorScheme: () => "dark", + addChangeListener: () => ({ + remove: () => { + /* ... */ + }, + }), + }, + StyleSheet: { + hairlineWidth: 0.5, + }, +})); From 8eeb4257b5a7d46f682ea33d066e2084d6741316 Mon Sep 17 00:00:00 2001 From: Grant Sander Date: Wed, 6 Jul 2022 12:54:27 -0400 Subject: [PATCH 3/7] Test/fix for useStoreValue --- packages/core/src/createStyleBuilder.tsx | 2 +- packages/core/src/utils/SimpleStore.test.ts | 19 +++++++++++++++++++ packages/core/src/utils/SimpleStore.ts | 14 +++++++++++--- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/utils/SimpleStore.test.ts diff --git a/packages/core/src/createStyleBuilder.tsx b/packages/core/src/createStyleBuilder.tsx index db144b2..122d3c4 100644 --- a/packages/core/src/createStyleBuilder.tsx +++ b/packages/core/src/createStyleBuilder.tsx @@ -70,7 +70,7 @@ export const createStyleBuilder = < ); const { remove } = Appearance.addChangeListener((r) => { systemColorScheme = r.colorScheme; - isDarkModeStore.reeval(); + isDarkModeStore.emitUpdatedValue(); }); type DefaultTheme = typeof baseTheme; diff --git a/packages/core/src/utils/SimpleStore.test.ts b/packages/core/src/utils/SimpleStore.test.ts new file mode 100644 index 0000000..0f8b27f --- /dev/null +++ b/packages/core/src/utils/SimpleStore.test.ts @@ -0,0 +1,19 @@ +import { vitest, it, expect, describe } from "vitest"; +import { SimpleStore } from "./SimpleStore"; +import { renderHook } from "@testing-library/react-hooks"; + +describe("SimpleStore", () => { + it("takes a getValue function and offers a hook that can be triggered", () => { + let theValue = "foo"; + const s = new SimpleStore(() => theValue); + const { result } = renderHook(s.useStoreValue); + + // Initial result should be "foo" + expect(result.current).toEqual("foo"); + + // Update the value, and trigger emit, hook should now return "bar" + theValue = "bar"; + s.emitUpdatedValue(); + expect(result.current).toEqual("bar"); + }); +}); diff --git a/packages/core/src/utils/SimpleStore.ts b/packages/core/src/utils/SimpleStore.ts index c45ada6..aa8ece3 100644 --- a/packages/core/src/utils/SimpleStore.ts +++ b/packages/core/src/utils/SimpleStore.ts @@ -1,6 +1,11 @@ import * as React from "react"; import { SimpleEventEmitter } from "./SimpleEventEmitter"; +/** + * A simple store that can be updated anywhere, with hook-support. + * - Used to hold state (like colorScheme preference), which can be updated from a single + * event listener, and have those updates emitted out to multiple hook-usages. + */ export class SimpleStore { #getValue!: () => S; #ee = new SimpleEventEmitter(); @@ -9,11 +14,14 @@ export class SimpleStore { this.#getValue = getValue; } - reeval() { + emitUpdatedValue() { this.#ee.emit(this.#getValue()); } - useStoreValue() { + /** + * Custom hook that taps into this store. + */ + useStoreValue = () => { const [val, setVal] = React.useState(() => this.#getValue()); React.useEffect(() => { @@ -25,5 +33,5 @@ export class SimpleStore { }, []); return val; - } + }; } From 6afcd55aba8a7cb07eb61be60176056d151b8c72 Mon Sep 17 00:00:00 2001 From: Grant Sander Date: Wed, 6 Jul 2022 12:55:50 -0400 Subject: [PATCH 4/7] Lint cleanup --- packages/core/src/createStyleBuilder.test.tsx | 3 +-- packages/core/src/handlers/createColorHandlers.test.ts | 2 +- packages/core/src/handlers/createOpacityHandlers.test.ts | 2 +- packages/core/src/utils/SimpleStore.test.ts | 2 +- packages/sample/styled.tsx | 9 +-------- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/core/src/createStyleBuilder.test.tsx b/packages/core/src/createStyleBuilder.test.tsx index d8fb12f..9e79bfe 100644 --- a/packages/core/src/createStyleBuilder.test.tsx +++ b/packages/core/src/createStyleBuilder.test.tsx @@ -2,10 +2,9 @@ import * as React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createStyleBuilder } from "./createStyleBuilder"; import { DefaultTheme } from "./theme"; -import { StyleSheet, Text } from "react-native"; +import { Text } from "react-native"; import { render } from "@testing-library/react-native"; import { renderHook } from "@testing-library/react-hooks"; -import create = StyleSheet.create; let colorScheme = "light"; const MockText = vi.fn(); diff --git a/packages/core/src/handlers/createColorHandlers.test.ts b/packages/core/src/handlers/createColorHandlers.test.ts index 6e2634c..f410984 100644 --- a/packages/core/src/handlers/createColorHandlers.test.ts +++ b/packages/core/src/handlers/createColorHandlers.test.ts @@ -1,4 +1,4 @@ -import { vi, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; import { DefaultTheme } from "../theme"; diff --git a/packages/core/src/handlers/createOpacityHandlers.test.ts b/packages/core/src/handlers/createOpacityHandlers.test.ts index 07f56a8..91d1820 100644 --- a/packages/core/src/handlers/createOpacityHandlers.test.ts +++ b/packages/core/src/handlers/createOpacityHandlers.test.ts @@ -1,4 +1,4 @@ -import { vi, describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { createStyleBuilder } from "../createStyleBuilder"; import { DefaultTheme } from "../theme"; diff --git a/packages/core/src/utils/SimpleStore.test.ts b/packages/core/src/utils/SimpleStore.test.ts index 0f8b27f..5fc2940 100644 --- a/packages/core/src/utils/SimpleStore.test.ts +++ b/packages/core/src/utils/SimpleStore.test.ts @@ -1,4 +1,4 @@ -import { vitest, it, expect, describe } from "vitest"; +import { describe, expect, it } from "vitest"; import { SimpleStore } from "./SimpleStore"; import { renderHook } from "@testing-library/react-hooks"; diff --git a/packages/sample/styled.tsx b/packages/sample/styled.tsx index 45e5196..f84bdb5 100644 --- a/packages/sample/styled.tsx +++ b/packages/sample/styled.tsx @@ -1,3 +1,4 @@ +import * as React from "react"; import { createStyleBuilder, extractTwColor } from "react-native-zephyr"; import { View, Text, TouchableOpacity, Animated, Image } from "react-native"; @@ -8,11 +9,6 @@ export const { makeStyledComponent, styles, styled, useStyles } = ...extractTwColor({ twColor: "fuchsia", name: "brown" }), }, }), - breakpoints: { - sm: 300, - md: 450, - lg: 600, - }, // baseFontSize: 20, }); @@ -26,9 +22,6 @@ export const StyledImage = makeStyledComponent(Image); export const MyComp = () => { const styles = useStyles({ classes: ["p:1"], - smClasses: ["p:2"], - mdClasses: ["p:4"], - lgClasses: ["p:40"], }); return ; From 58c3f2aeb5685a07d511fae968d1eb2a2dbbb2dd Mon Sep 17 00:00:00 2001 From: Grant Sander Date: Wed, 6 Jul 2022 13:00:58 -0400 Subject: [PATCH 5/7] RM console.log --- packages/core/src/handlers/createShadowHandlers.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/handlers/createShadowHandlers.test.ts b/packages/core/src/handlers/createShadowHandlers.test.ts index 07acc6f..51d2334 100644 --- a/packages/core/src/handlers/createShadowHandlers.test.ts +++ b/packages/core/src/handlers/createShadowHandlers.test.ts @@ -4,8 +4,6 @@ import { DefaultTheme } from "../theme"; let platform = "android"; -console.log(vi.importMock("react-native")); - vi.mock("react-native", async () => ({ // TODO: dedup this. Appearance: { From 8400d210da239527f9b969437ad23219b0ef8fcd Mon Sep 17 00:00:00 2001 From: Grant Sander Date: Wed, 6 Jul 2022 13:27:35 -0400 Subject: [PATCH 6/7] Improvement to SimpleStore --- packages/core/src/createStyleBuilder.tsx | 14 ++++----- packages/core/src/utils/SimpleStore.test.ts | 21 +++++++++++--- packages/core/src/utils/SimpleStore.ts | 32 +++++++++++++++------ 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/packages/core/src/createStyleBuilder.tsx b/packages/core/src/createStyleBuilder.tsx index 122d3c4..268c1f4 100644 --- a/packages/core/src/createStyleBuilder.tsx +++ b/packages/core/src/createStyleBuilder.tsx @@ -62,15 +62,13 @@ export const createStyleBuilder = < /** * Internal state for dark mode */ - let systemColorScheme = Appearance.getColorScheme(); - const isDarkModeStore = new SimpleStore( - () => - colorScheme === "dark" || - (colorScheme === "auto" && systemColorScheme === "dark") - ); + const isDarkModeStore = new SimpleStore({ + initialValue: Appearance.getColorScheme(), + transformer: (s) => + colorScheme === "dark" || (colorScheme === "auto" && s === "dark"), + }); const { remove } = Appearance.addChangeListener((r) => { - systemColorScheme = r.colorScheme; - isDarkModeStore.emitUpdatedValue(); + isDarkModeStore.updateValue(r.colorScheme); }); type DefaultTheme = typeof baseTheme; diff --git a/packages/core/src/utils/SimpleStore.test.ts b/packages/core/src/utils/SimpleStore.test.ts index 5fc2940..941aa80 100644 --- a/packages/core/src/utils/SimpleStore.test.ts +++ b/packages/core/src/utils/SimpleStore.test.ts @@ -4,16 +4,29 @@ import { renderHook } from "@testing-library/react-hooks"; describe("SimpleStore", () => { it("takes a getValue function and offers a hook that can be triggered", () => { - let theValue = "foo"; - const s = new SimpleStore(() => theValue); + const s = new SimpleStore({ initialValue: "foo", transformer: (v) => v }); const { result } = renderHook(s.useStoreValue); // Initial result should be "foo" expect(result.current).toEqual("foo"); // Update the value, and trigger emit, hook should now return "bar" - theValue = "bar"; - s.emitUpdatedValue(); + s.updateValue("bar"); expect(result.current).toEqual("bar"); }); + + it("transforms values", () => { + const s = new SimpleStore({ + initialValue: "foo" as "foo" | "bar", + transformer: (v) => v === "foo", + }); + const { result } = renderHook(s.useStoreValue); + + // Initial result should be "foo" + expect(result.current).toEqual(true); + + // Update the value, and trigger emit, hook should now return "bar" + s.updateValue("bar"); + expect(result.current).toEqual(false); + }); }); diff --git a/packages/core/src/utils/SimpleStore.ts b/packages/core/src/utils/SimpleStore.ts index aa8ece3..ab431aa 100644 --- a/packages/core/src/utils/SimpleStore.ts +++ b/packages/core/src/utils/SimpleStore.ts @@ -6,23 +6,37 @@ import { SimpleEventEmitter } from "./SimpleEventEmitter"; * - Used to hold state (like colorScheme preference), which can be updated from a single * event listener, and have those updates emitted out to multiple hook-usages. */ -export class SimpleStore { - #getValue!: () => S; - #ee = new SimpleEventEmitter(); +export class SimpleStore { + #value!: OutputValue; + #ee = new SimpleEventEmitter(); + #transformer!: (val: InitialValue) => OutputValue; - constructor(getValue: () => S) { - this.#getValue = getValue; + constructor({ + initialValue, + transformer, + }: { + initialValue: InitialValue; + transformer: (val: InitialValue) => OutputValue; + }) { + this.#value = transformer(initialValue); + if (transformer) { + this.#transformer = transformer; + } } - emitUpdatedValue() { - this.#ee.emit(this.#getValue()); - } + updateValue = (newValue: InitialValue) => { + const _newValue = this.#transformer(newValue); + if (_newValue !== this.#value) { + this.#value = _newValue; + this.#ee.emit(this.#value); + } + }; /** * Custom hook that taps into this store. */ useStoreValue = () => { - const [val, setVal] = React.useState(() => this.#getValue()); + const [val, setVal] = React.useState(() => this.#value); React.useEffect(() => { const { unsubscribe } = this.#ee.subscribe((v) => { From 63a271af0b3501a37ce74945b10a9180e73cbe55 Mon Sep 17 00:00:00 2001 From: Grant Sander Date: Wed, 6 Jul 2022 14:37:57 -0400 Subject: [PATCH 7/7] Update StyleProvider.tsx --- packages/core/src/StyleProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/StyleProvider.tsx b/packages/core/src/StyleProvider.tsx index c693069..4e1b8b2 100644 --- a/packages/core/src/StyleProvider.tsx +++ b/packages/core/src/StyleProvider.tsx @@ -5,7 +5,7 @@ type StyleProviderProps = { }; /** - * TODO: Deprecate this. No longer needed, but leaving for now. + * @deprecated this. No longer needed, but leaving for now. */ export const StyleProvider = ({ children,