-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(components-native): AtlantisThemeContext for mobile (#2387)
* Add AtlantisThemeContext for components-native * Make Button component use theme tokens * Add todos * Handle changing theme globally * Add mobile story for AtlantisThemeContext * Add tests for mobile AtlantisThemeContextProvider * Update meta.json * Cleanup types * Undo Button changes * Update comment * Generate mobile props for AtlantisThemeContext * Show mobile props * Theme the mobile preview iframe * Add mobile example for AtlantisThemeContext * Update mobile snippet to override the theme * Fix lint * Rename function * Regen props files * Revert "Regen props files" This reverts commit 556789c. * Note differences between web and mobile * Tweak wording
- Loading branch information
Showing
14 changed files
with
444 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import React from "react"; | ||
import { ComponentMeta, ComponentStory } from "@storybook/react"; | ||
import { View } from "react-native"; | ||
import { | ||
AtlantisThemeContextProvider, | ||
Button, | ||
Content, | ||
Text, | ||
useAtlantisTheme, | ||
} from "@jobber/components-native"; | ||
|
||
export default { | ||
title: "Components/Themes/AtlantisThemeContext/Mobile", | ||
component: AtlantisThemeContextProvider, | ||
parameters: { | ||
viewMode: "story", | ||
previewTabs: { | ||
code: { | ||
hidden: false, | ||
}, | ||
}, | ||
viewport: { defaultViewport: "mobile1" }, | ||
}, | ||
} as ComponentMeta<typeof AtlantisThemeContextProvider>; | ||
|
||
function ChildrenComponent({ | ||
message = "It is possible to have multiple Atlantis Theme providers.", | ||
}: { | ||
readonly message?: string; | ||
}) { | ||
const { theme, tokens, setTheme } = useAtlantisTheme(); | ||
|
||
return ( | ||
<View | ||
style={{ | ||
backgroundColor: tokens["color-surface"], | ||
}} | ||
> | ||
<Content> | ||
<Text>{message}</Text> | ||
<Text>{`Current theme: ${theme}`}</Text> | ||
<Text>Tokens can be accessed using tokens[token-name]</Text> | ||
<Text>{`For example color-surface: ${tokens["color-surface"]}`}</Text> | ||
<Button label="Set dark theme" onPress={() => setTheme("dark")} /> | ||
<Button label="Set light theme" onPress={() => setTheme("light")} /> | ||
</Content> | ||
</View> | ||
); | ||
} | ||
|
||
function ForcedDarkThemeComponent() { | ||
const { tokens } = useAtlantisTheme(); | ||
|
||
return ( | ||
<View | ||
style={{ | ||
backgroundColor: tokens["color-surface"], | ||
}} | ||
> | ||
<Content> | ||
<Text>This will always be a dark theme</Text> | ||
</Content> | ||
</View> | ||
); | ||
} | ||
|
||
const BasicTemplate: ComponentStory< | ||
typeof AtlantisThemeContextProvider | ||
> = args => { | ||
return ( | ||
<AtlantisThemeContextProvider {...args}> | ||
<ChildrenComponent /> | ||
</AtlantisThemeContextProvider> | ||
); | ||
}; | ||
|
||
export const Basic = BasicTemplate.bind({}); | ||
BasicTemplate.args = {}; | ||
|
||
const ForceThemeTemplate: ComponentStory< | ||
typeof AtlantisThemeContextProvider | ||
> = args => { | ||
return ( | ||
<AtlantisThemeContextProvider {...args}> | ||
<ChildrenComponent message="It is possible to have a provider ignore Theme Changes" /> | ||
<AtlantisThemeContextProvider dangerouslyOverrideTheme="dark"> | ||
<ForcedDarkThemeComponent /> | ||
</AtlantisThemeContextProvider> | ||
</AtlantisThemeContextProvider> | ||
); | ||
}; | ||
|
||
export const ForceThemeForProvider = ForceThemeTemplate.bind({}); | ||
ForceThemeForProvider.args = {}; |
106 changes: 106 additions & 0 deletions
106
packages/components-native/src/AtlantisThemeContext/AtlantisThemeContext.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import React from "react"; | ||
import { act, renderHook } from "@testing-library/react-native"; | ||
import { darkTokens, iosTokens } from "@jobber/design"; | ||
import merge from "lodash/merge"; | ||
import { | ||
AtlantisThemeContextProvider, | ||
useAtlantisTheme, | ||
} from "./AtlantisThemeContext"; | ||
import { AtlantisThemeContextProviderProps, Theme } from "./types"; | ||
|
||
const expectedDarkTokens = merge({}, iosTokens, darkTokens); | ||
const expectedLightTokens = iosTokens; | ||
|
||
function Wrapper({ | ||
children, | ||
dangerouslyOverrideTheme, | ||
}: AtlantisThemeContextProviderProps) { | ||
return ( | ||
<AtlantisThemeContextProvider | ||
dangerouslyOverrideTheme={dangerouslyOverrideTheme} | ||
> | ||
{children} | ||
</AtlantisThemeContextProvider> | ||
); | ||
} | ||
|
||
function WrapperWithOverride({ | ||
children, | ||
dangerouslyOverrideTheme, | ||
}: AtlantisThemeContextProviderProps) { | ||
return ( | ||
<Wrapper> | ||
<AtlantisThemeContextProvider | ||
dangerouslyOverrideTheme={dangerouslyOverrideTheme} | ||
> | ||
{children} | ||
</AtlantisThemeContextProvider> | ||
</Wrapper> | ||
); | ||
} | ||
|
||
describe("ThemeContext", () => { | ||
it("defaults to the light theme", () => { | ||
const { result } = renderHook(useAtlantisTheme, { | ||
wrapper: (props: AtlantisThemeContextProviderProps) => ( | ||
<Wrapper {...props} /> | ||
), | ||
}); | ||
|
||
expect(result.current.theme).toBe("light"); | ||
expect(result.current.tokens).toEqual(expectedLightTokens); | ||
}); | ||
|
||
it("updates the theme and tokens", () => { | ||
const { result } = renderHook(useAtlantisTheme, { | ||
wrapper: (props: AtlantisThemeContextProviderProps) => ( | ||
<Wrapper {...props} /> | ||
), | ||
}); | ||
|
||
act(() => result.current.setTheme("dark")); | ||
expect(result.current.theme).toBe("dark"); | ||
expect(result.current.tokens).toEqual(expectedDarkTokens); | ||
|
||
act(() => result.current.setTheme("light")); | ||
expect(result.current.theme).toBe("light"); | ||
expect(result.current.tokens).toEqual(expectedLightTokens); | ||
}); | ||
|
||
describe("when theme is forced for provider", () => { | ||
it("ignores updates to the global theme", () => { | ||
const { result } = renderHook(useAtlantisTheme, { | ||
wrapper: (props: AtlantisThemeContextProviderProps) => ( | ||
<WrapperWithOverride {...props} dangerouslyOverrideTheme="light" /> | ||
), | ||
}); | ||
|
||
// Update the global theme | ||
act(() => result.current.setTheme("dark")); | ||
|
||
// This hook shouldn't be affected by it because it's set to the light theme | ||
expect(result.current.theme).toBe("light"); | ||
expect(result.current.tokens).toEqual(expectedLightTokens); | ||
}); | ||
|
||
it.each([ | ||
{ defaultTheme: "light", expectedTokens: expectedLightTokens }, | ||
{ defaultTheme: "dark", expectedTokens: expectedDarkTokens }, | ||
] as { defaultTheme: Theme; expectedTokens: typeof iosTokens }[])( | ||
"provides the dangerouslyOverrideTheme $defaultTheme tokens", | ||
({ defaultTheme, expectedTokens }) => { | ||
const { result } = renderHook(useAtlantisTheme, { | ||
wrapper: (props: AtlantisThemeContextProviderProps) => ( | ||
<WrapperWithOverride | ||
{...props} | ||
dangerouslyOverrideTheme={defaultTheme} | ||
/> | ||
), | ||
}); | ||
|
||
expect(result.current.theme).toBe(defaultTheme); | ||
expect(result.current.tokens).toEqual(expectedTokens); | ||
}, | ||
); | ||
}); | ||
}); |
56 changes: 56 additions & 0 deletions
56
packages/components-native/src/AtlantisThemeContext/AtlantisThemeContext.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { androidTokens, darkTokens, iosTokens } from "@jobber/design"; | ||
import React, { createContext, useContext, useState } from "react"; | ||
import merge from "lodash/merge"; | ||
import { Platform } from "react-native"; | ||
import { | ||
AtlantisThemeContextProviderProps, | ||
AtlantisThemeContextValue, | ||
Theme, | ||
} from "./types"; | ||
|
||
const lightTokens = Platform.select({ | ||
ios: () => iosTokens, | ||
android: () => androidTokens, | ||
default: () => androidTokens, | ||
})(); | ||
|
||
const completeDarkTokens = merge({}, lightTokens, darkTokens); | ||
|
||
const AtlantisThemeContext = createContext<AtlantisThemeContextValue>({ | ||
theme: "light", | ||
tokens: lightTokens, | ||
setTheme: () => { | ||
console.error( | ||
"useAtlantisTheme accessed outside of AtlantisThemeContextProvider", | ||
); | ||
}, | ||
}); | ||
|
||
export function AtlantisThemeContextProvider({ | ||
children, | ||
dangerouslyOverrideTheme, | ||
}: AtlantisThemeContextProviderProps) { | ||
// TODO: check last saved theme from local/device storage | ||
const initialTheme: Theme = "light"; | ||
const [globalTheme, setGlobalTheme] = useState<Theme>(initialTheme); | ||
|
||
const currentTheme = dangerouslyOverrideTheme ?? globalTheme; | ||
const currentTokens = | ||
currentTheme === "dark" ? completeDarkTokens : lightTokens; | ||
|
||
return ( | ||
<AtlantisThemeContext.Provider | ||
value={{ | ||
theme: currentTheme, | ||
tokens: currentTokens, | ||
setTheme: setGlobalTheme, | ||
}} | ||
> | ||
{children} | ||
</AtlantisThemeContext.Provider> | ||
); | ||
} | ||
|
||
export function useAtlantisTheme() { | ||
return useContext(AtlantisThemeContext); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export { | ||
AtlantisThemeContextProvider, | ||
useAtlantisTheme, | ||
} from "./AtlantisThemeContext"; | ||
export { | ||
Theme, | ||
AtlantisThemeContextProviderProps, | ||
AtlantisThemeContextValue, | ||
} from "./types"; |
33 changes: 33 additions & 0 deletions
33
packages/components-native/src/AtlantisThemeContext/types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { iosTokens } from "@jobber/design"; | ||
|
||
export interface AtlantisThemeContextValue { | ||
/** | ||
* The active theme. | ||
*/ | ||
readonly theme: Theme; | ||
|
||
/** | ||
* The design tokens for the current theme. | ||
*/ | ||
readonly tokens: typeof iosTokens; | ||
|
||
/** | ||
* Change the current theme globally. | ||
*/ | ||
readonly setTheme: (theme: Theme) => void; | ||
} | ||
|
||
export interface AtlantisThemeContextProviderProps { | ||
/** | ||
* The children to render. | ||
*/ | ||
readonly children: React.ReactNode; | ||
|
||
/** | ||
* Force the theme for this provider to always be the same as the provided theme. Useful for sections that should remain the same theme regardless of the rest of the application's theme. | ||
* This is dangerous because the children in this provider will not be able to change the theme. | ||
*/ | ||
readonly dangerouslyOverrideTheme?: Theme; | ||
} | ||
|
||
export type Theme = "light" | "dark"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.