Skip to content
This repository was archived by the owner on Jul 2, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 4 additions & 19 deletions packages/core/src/StyleProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 = ({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the following comment

/**
 * @deprecated This provider should not be used
 */

children,
colorScheme = "auto",
}: React.PropsWithChildren<StyleProviderProps>) => {
const systemColorScheme = useColorScheme();

const value = React.useMemo<React.ContextType<typeof StyleContext>>(() => {
return {
isDarkMode:
colorScheme === "dark" ||
(colorScheme === "auto" && systemColorScheme === "dark"),
};
}, [colorScheme, systemColorScheme]);

return (
<StyleContext.Provider value={value}>{children}</StyleContext.Provider>
);
return <>{children}</>;
};
65 changes: 35 additions & 30 deletions packages/core/src/createStyleBuilder.test.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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(
<StyledText classes={["bg:red-100"]} darkClasses={["bg:blue-100"]}>
Hello world
</StyledText>,
{ wrapper: Wrapper }
</StyledText>
);

// @ts-expect-error HALP. How do I type this mock?
Expand All @@ -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(<MyText>Hey world</MyText>);

Expand All @@ -195,6 +197,7 @@ describe("createStyleBuilder().styled", () => {
});

it("accepts configuration object", () => {
const { styled } = createStyleBuilder();
const MyText = styled(Text)({
classes: ["color:red-200"],
});
Expand All @@ -207,54 +210,56 @@ 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(<MyText>Hey world</MyText>, { wrapper: Wrapper });
render(<MyText>Hey world</MyText>);
// @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(<MyText>Hey world</MyText>, { wrapper: Wrapper });
const MyText2 = getMyText();
render(<MyText2>Hey world</MyText2>);
// @ts-expect-error HALP. How do I type this mock?
expect(MockText.calls?.at(-1)?.[0].style[0]).toEqual({
color: DefaultTheme.colors["blue-100"],
});
});

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(<MyText>Hello world</MyText>, { wrapper: Wrapper });
render(<MyText>Hello world</MyText>);
// @ts-expect-error HALP. How do I type this mock?
expect(MockText.calls?.at(-1)?.[0].style[0]).toEqual({});

// with isItalic prop
render(<MyText isItalic>Hello world</MyText>, { wrapper: Wrapper });
render(<MyText isItalic>Hello world</MyText>);
// @ts-expect-error HALP. How do I type this mock?
expect(MockText.calls?.at(-1)?.[0].style[0]).toEqual({
fontStyle: "italic",
});

// Dark mode
colorScheme = "dark";
render(<MyText isItalic>Hello world</MyText>, { wrapper: Wrapper });
const MyText2 = getMyText();
render(<MyText2 isItalic>Hello world</MyText2>);
// @ts-expect-error HALP. How do I type this mock?
expect(MockText.calls?.at(-1)?.[0].style[0]).toEqual({
fontStyle: "italic",
color: DefaultTheme.colors["red-100"],
});
});
});

const Wrapper = ({ children }: PropsWithChildren) => (
<StyleProvider>{children}</StyleProvider>
);
35 changes: 31 additions & 4 deletions packages/core/src/createStyleBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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.
Expand All @@ -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 });
Expand All @@ -57,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.emitUpdatedValue();
});

type DefaultTheme = typeof baseTheme;
type GetKey<
UserThemeConstraints,
Expand Down Expand Up @@ -409,7 +425,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);
Expand Down Expand Up @@ -522,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 = /^(.+):(.+)$/;
Expand Down
83 changes: 83 additions & 0 deletions packages/core/src/darkMode.test.ts
Original file line number Diff line number Diff line change
@@ -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"],
});
});
});
Loading