diff --git a/packages/core/src/StyleProvider.tsx b/packages/core/src/StyleProvider.tsx index 66244df..4e1b8b2 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"; }; +/** + * @deprecated 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..9e79bfe 100644 --- a/packages/core/src/createStyleBuilder.test.tsx +++ b/packages/core/src/createStyleBuilder.test.tsx @@ -1,22 +1,27 @@ 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 { render } from "@testing-library/react-native"; -import { StyleProvider } from "./StyleProvider"; -import { PropsWithChildren } from "react"; import { renderHook } from "@testing-library/react-hooks"; 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 +130,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 +162,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 +181,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 +197,7 @@ describe("createStyleBuilder().styled", () => { }); it("accepts configuration object", () => { + const { styled } = createStyleBuilder(); const MyText = styled(Text)({ classes: ["color:red-200"], }); @@ -207,19 +210,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 +233,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 +254,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 +263,3 @@ describe("createStyleBuilder().styled", () => { }); }); }); - -const Wrapper = ({ children }: PropsWithChildren) => ( - {children} -); diff --git a/packages/core/src/createStyleBuilder.tsx b/packages/core/src/createStyleBuilder.tsx index 30c52a0..268c1f4 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. @@ -37,11 +37,13 @@ export const createStyleBuilder = < overrideTheme, extendTheme, baseFontSize = 14, + colorScheme = "auto", }: { extraHandlers?: ExtraStyleHandlers; overrideTheme?: Theme | ((args: { baseFontSize: number }) => Theme); extendTheme?: ThemeExt | ((args: { baseFontSize: number }) => ThemeExt); baseFontSize?: number; + colorScheme?: "light" | "dark" | "auto"; } = {}) => { const cache = new SimpleConstrainedCache({ maxNumRecords: 400 }); const baseTheme = createDefaultTheme({ baseFontSize }); @@ -57,6 +59,18 @@ export const createStyleBuilder = < : extendTheme, }); + /** + * Internal state for dark mode + */ + const isDarkModeStore = new SimpleStore({ + initialValue: Appearance.getColorScheme(), + transformer: (s) => + colorScheme === "dark" || (colorScheme === "auto" && s === "dark"), + }); + const { remove } = Appearance.addChangeListener((r) => { + isDarkModeStore.updateValue(r.colorScheme); + }); + type DefaultTheme = typeof baseTheme; type GetKey< UserThemeConstraints, @@ -409,7 +423,7 @@ export const createStyleBuilder = < classes?: CnArg[]; darkClasses?: CnArg[]; }) => { - const { isDarkMode } = React.useContext(StyleContext); + const isDarkMode = isDarkModeStore.useStoreValue(); return React.useMemo(() => { const allClasses = [...classes].concat(isDarkMode ? darkClasses : []); return styles(...allClasses); @@ -522,7 +536,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..f410984 100644 --- a/packages/core/src/handlers/createColorHandlers.test.ts +++ b/packages/core/src/handlers/createColorHandlers.test.ts @@ -1,13 +1,7 @@ -import { vi, describe, expect, it } 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.colors; diff --git a/packages/core/src/handlers/createOpacityHandlers.test.ts b/packages/core/src/handlers/createOpacityHandlers.test.ts index f05b909..91d1820 100644 --- a/packages/core/src/handlers/createOpacityHandlers.test.ts +++ b/packages/core/src/handlers/createOpacityHandlers.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({ 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..51d2334 100644 --- a/packages/core/src/handlers/createShadowHandlers.test.ts +++ b/packages/core/src/handlers/createShadowHandlers.test.ts @@ -4,7 +4,16 @@ import { DefaultTheme } from "../theme"; let platform = "android"; -vi.mock("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.test.ts b/packages/core/src/utils/SimpleStore.test.ts new file mode 100644 index 0000000..941aa80 --- /dev/null +++ b/packages/core/src/utils/SimpleStore.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } 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", () => { + 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" + 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 new file mode 100644 index 0000000..ab431aa --- /dev/null +++ b/packages/core/src/utils/SimpleStore.ts @@ -0,0 +1,51 @@ +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 { + #value!: OutputValue; + #ee = new SimpleEventEmitter(); + #transformer!: (val: InitialValue) => OutputValue; + + constructor({ + initialValue, + transformer, + }: { + initialValue: InitialValue; + transformer: (val: InitialValue) => OutputValue; + }) { + this.#value = transformer(initialValue); + if (transformer) { + this.#transformer = transformer; + } + } + + 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.#value); + + 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.ts b/packages/sample/styled.tsx similarity index 77% rename from packages/sample/styled.ts rename to packages/sample/styled.tsx index 3245cec..f84bdb5 100644 --- a/packages/sample/styled.ts +++ b/packages/sample/styled.tsx @@ -1,15 +1,13 @@ +import * as React from "react"; import { createStyleBuilder, extractTwColor } from "react-native-zephyr"; 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], - }, }), // baseFontSize: 20, }); @@ -20,3 +18,11 @@ export const StyledTouchableOpacity = makeStyledComponent( Animated.createAnimatedComponent(TouchableOpacity) ); export const StyledImage = makeStyledComponent(Image); + +export const MyComp = () => { + const styles = useStyles({ + classes: ["p:1"], + }); + + return ; +}; 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, + }, +}));