@jobber/components-native 0.113.3 → 0.113.4-implement--97f86be.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/AtlantisThemeContext/AtlantisThemeContext.md +27 -10
- package/dist/docs/Select/Select.md +276 -143
- package/dist/package.json +2 -2
- package/dist/src/AtlantisThemeContext/AtlantisThemeContext.js +35 -12
- package/dist/src/AtlantisThemeContext/AtlantisThemeContext.test.js +103 -10
- package/dist/src/ContentOverlay/ContentOverlay.js +2 -2
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/AtlantisThemeContext/AtlantisThemeContext.d.ts +1 -1
- package/dist/types/src/AtlantisThemeContext/index.d.ts +1 -1
- package/dist/types/src/AtlantisThemeContext/types.d.ts +19 -4
- package/package.json +2 -2
- package/src/AtlantisThemeContext/AtlantisThemeContext.guide.md +53 -0
- package/src/AtlantisThemeContext/AtlantisThemeContext.test.tsx +169 -10
- package/src/AtlantisThemeContext/AtlantisThemeContext.tsx +52 -11
- package/src/AtlantisThemeContext/docs/AtlantisThemeContextBasicExample.tsx +8 -2
- package/src/AtlantisThemeContext/docs/AtlantisThemeContextForceThemeForProviderExample.tsx +8 -4
- package/src/AtlantisThemeContext/index.ts +1 -0
- package/src/AtlantisThemeContext/types.ts +23 -4
- package/src/ContentOverlay/ContentOverlay.tsx +2 -2
|
@@ -1,32 +1,55 @@
|
|
|
1
1
|
import { androidTokens, darkTokens, iosTokens } from "@jobber/design";
|
|
2
|
-
import React, { createContext, useContext, useState } from "react";
|
|
2
|
+
import React, { createContext, useCallback, useContext, useState } from "react";
|
|
3
3
|
import merge from "lodash/merge";
|
|
4
|
-
import { Platform } from "react-native";
|
|
4
|
+
import { Platform, useColorScheme } from "react-native";
|
|
5
5
|
const lightTokens = Platform.select({
|
|
6
6
|
ios: () => iosTokens,
|
|
7
7
|
android: () => androidTokens,
|
|
8
8
|
default: () => androidTokens,
|
|
9
9
|
})();
|
|
10
10
|
const completeDarkTokens = merge({}, lightTokens, darkTokens);
|
|
11
|
-
const
|
|
11
|
+
const defaultAtlantisThemeContextValue = {
|
|
12
12
|
theme: "light",
|
|
13
|
+
effectiveTheme: "light",
|
|
13
14
|
tokens: lightTokens,
|
|
14
15
|
setTheme: () => {
|
|
15
16
|
console.error("useAtlantisTheme accessed outside of AtlantisThemeContextProvider");
|
|
16
17
|
},
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
18
|
+
};
|
|
19
|
+
const AtlantisThemeContext = createContext(undefined);
|
|
20
|
+
export function AtlantisThemeContextProvider({ children, theme, onThemeChange, dangerouslyOverrideTheme, }) {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
const parentThemeContext = useContext(AtlantisThemeContext);
|
|
23
|
+
const [localTheme, setLocalTheme] = useState(undefined);
|
|
24
|
+
const colorScheme = useColorScheme();
|
|
25
|
+
const setTheme = useCallback((nextTheme) => {
|
|
26
|
+
onThemeChange === null || onThemeChange === void 0 ? void 0 : onThemeChange(nextTheme);
|
|
27
|
+
if (theme !== undefined)
|
|
28
|
+
return;
|
|
29
|
+
// A forced visual override should preserve the parent subtree's selected
|
|
30
|
+
// theme semantics while still rendering with the forced tokens.
|
|
31
|
+
if (dangerouslyOverrideTheme !== undefined && parentThemeContext) {
|
|
32
|
+
parentThemeContext.setTheme(nextTheme);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setLocalTheme(nextTheme);
|
|
36
|
+
}, [dangerouslyOverrideTheme, onThemeChange, parentThemeContext, theme]);
|
|
37
|
+
const currentTheme = (_b = (_a = theme !== null && theme !== void 0 ? theme : localTheme) !== null && _a !== void 0 ? _a : parentThemeContext === null || parentThemeContext === void 0 ? void 0 : parentThemeContext.theme) !== null && _b !== void 0 ? _b : "light";
|
|
38
|
+
const effectiveTheme = dangerouslyOverrideTheme !== null && dangerouslyOverrideTheme !== void 0 ? dangerouslyOverrideTheme : resolveEffectiveTheme(currentTheme, colorScheme);
|
|
39
|
+
const currentTokens = effectiveTheme === "dark" ? completeDarkTokens : lightTokens;
|
|
24
40
|
return (React.createElement(AtlantisThemeContext.Provider, { value: {
|
|
25
41
|
theme: currentTheme,
|
|
42
|
+
effectiveTheme,
|
|
26
43
|
tokens: currentTokens,
|
|
27
|
-
setTheme
|
|
44
|
+
setTheme,
|
|
28
45
|
} }, children));
|
|
29
46
|
}
|
|
30
47
|
export function useAtlantisTheme() {
|
|
31
|
-
|
|
48
|
+
var _a;
|
|
49
|
+
return (_a = useContext(AtlantisThemeContext)) !== null && _a !== void 0 ? _a : defaultAtlantisThemeContextValue;
|
|
50
|
+
}
|
|
51
|
+
function resolveEffectiveTheme(theme, colorScheme) {
|
|
52
|
+
if (theme !== "system")
|
|
53
|
+
return theme;
|
|
54
|
+
return colorScheme === "dark" ? "dark" : "light";
|
|
32
55
|
}
|
|
@@ -11,22 +11,36 @@ import React from "react";
|
|
|
11
11
|
import { act, renderHook } from "@testing-library/react-native";
|
|
12
12
|
import { darkTokens, iosTokens } from "@jobber/design";
|
|
13
13
|
import merge from "lodash/merge";
|
|
14
|
+
import { useColorScheme } from "react-native";
|
|
14
15
|
import { AtlantisThemeContextProvider, useAtlantisTheme, } from "./AtlantisThemeContext";
|
|
16
|
+
jest.mock("react-native", () => {
|
|
17
|
+
const actual = jest.requireActual("react-native");
|
|
18
|
+
Object.defineProperty(actual, "useColorScheme", {
|
|
19
|
+
configurable: true,
|
|
20
|
+
value: jest.fn(),
|
|
21
|
+
});
|
|
22
|
+
return actual;
|
|
23
|
+
});
|
|
15
24
|
const expectedDarkTokens = merge({}, iosTokens, darkTokens);
|
|
16
25
|
const expectedLightTokens = iosTokens;
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
const mockUseColorScheme = useColorScheme;
|
|
27
|
+
function Wrapper({ children, theme, onThemeChange, dangerouslyOverrideTheme, }) {
|
|
28
|
+
return (React.createElement(AtlantisThemeContextProvider, { theme: theme, onThemeChange: onThemeChange, dangerouslyOverrideTheme: dangerouslyOverrideTheme }, children));
|
|
19
29
|
}
|
|
20
30
|
function WrapperWithOverride({ children, dangerouslyOverrideTheme, }) {
|
|
21
31
|
return (React.createElement(Wrapper, null,
|
|
22
32
|
React.createElement(AtlantisThemeContextProvider, { dangerouslyOverrideTheme: dangerouslyOverrideTheme }, children)));
|
|
23
33
|
}
|
|
24
34
|
describe("ThemeContext", () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
mockUseColorScheme.mockReturnValue("light");
|
|
37
|
+
});
|
|
25
38
|
it("defaults to the light theme", () => {
|
|
26
39
|
const { result } = renderHook(useAtlantisTheme, {
|
|
27
40
|
wrapper: (props) => (React.createElement(Wrapper, Object.assign({}, props))),
|
|
28
41
|
});
|
|
29
42
|
expect(result.current.theme).toBe("light");
|
|
43
|
+
expect(result.current.effectiveTheme).toBe("light");
|
|
30
44
|
expect(result.current.tokens).toEqual(expectedLightTokens);
|
|
31
45
|
});
|
|
32
46
|
it("updates the theme and tokens", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -35,30 +49,109 @@ describe("ThemeContext", () => {
|
|
|
35
49
|
});
|
|
36
50
|
yield act(() => __awaiter(void 0, void 0, void 0, function* () { return result.current.setTheme("dark"); }));
|
|
37
51
|
expect(result.current.theme).toBe("dark");
|
|
52
|
+
expect(result.current.effectiveTheme).toBe("dark");
|
|
38
53
|
expect(result.current.tokens).toEqual(expectedDarkTokens);
|
|
39
54
|
yield act(() => __awaiter(void 0, void 0, void 0, function* () { return result.current.setTheme("light"); }));
|
|
40
55
|
expect(result.current.theme).toBe("light");
|
|
56
|
+
expect(result.current.effectiveTheme).toBe("light");
|
|
57
|
+
expect(result.current.tokens).toEqual(expectedLightTokens);
|
|
58
|
+
}));
|
|
59
|
+
it("resolves the system theme from the OS color scheme", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
60
|
+
mockUseColorScheme.mockReturnValue("dark");
|
|
61
|
+
const { result, rerender } = renderHook(useAtlantisTheme, {
|
|
62
|
+
wrapper: (props) => (React.createElement(Wrapper, Object.assign({}, props))),
|
|
63
|
+
});
|
|
64
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () { return result.current.setTheme("system"); }));
|
|
65
|
+
expect(result.current.theme).toBe("system");
|
|
66
|
+
expect(result.current.effectiveTheme).toBe("dark");
|
|
67
|
+
expect(result.current.tokens).toEqual(expectedDarkTokens);
|
|
68
|
+
mockUseColorScheme.mockReturnValue("light");
|
|
69
|
+
rerender({});
|
|
70
|
+
expect(result.current.theme).toBe("system");
|
|
71
|
+
expect(result.current.effectiveTheme).toBe("light");
|
|
72
|
+
expect(result.current.tokens).toEqual(expectedLightTokens);
|
|
73
|
+
}));
|
|
74
|
+
it("uses a controlled selected theme", () => {
|
|
75
|
+
mockUseColorScheme.mockReturnValue("dark");
|
|
76
|
+
const { result } = renderHook(useAtlantisTheme, {
|
|
77
|
+
wrapper: (props) => (React.createElement(Wrapper, Object.assign({}, props, { theme: "system" }))),
|
|
78
|
+
});
|
|
79
|
+
expect(result.current.theme).toBe("system");
|
|
80
|
+
expect(result.current.effectiveTheme).toBe("dark");
|
|
81
|
+
expect(result.current.tokens).toEqual(expectedDarkTokens);
|
|
82
|
+
});
|
|
83
|
+
it("reports selected theme changes from a controlled provider", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
+
const onThemeChange = jest.fn();
|
|
85
|
+
const { result } = renderHook(useAtlantisTheme, {
|
|
86
|
+
wrapper: (props) => (React.createElement(Wrapper, Object.assign({}, props, { theme: "light", onThemeChange: onThemeChange }))),
|
|
87
|
+
});
|
|
88
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () { return result.current.setTheme("system"); }));
|
|
89
|
+
expect(onThemeChange).toHaveBeenCalledWith("system");
|
|
90
|
+
expect(result.current.theme).toBe("light");
|
|
91
|
+
expect(result.current.effectiveTheme).toBe("light");
|
|
92
|
+
expect(result.current.tokens).toEqual(expectedLightTokens);
|
|
93
|
+
}));
|
|
94
|
+
it("falls back to light when system color scheme is unavailable", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
mockUseColorScheme.mockReturnValue(null);
|
|
96
|
+
const { result } = renderHook(useAtlantisTheme, {
|
|
97
|
+
wrapper: (props) => (React.createElement(Wrapper, Object.assign({}, props))),
|
|
98
|
+
});
|
|
99
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () { return result.current.setTheme("system"); }));
|
|
100
|
+
expect(result.current.theme).toBe("system");
|
|
101
|
+
expect(result.current.effectiveTheme).toBe("light");
|
|
41
102
|
expect(result.current.tokens).toEqual(expectedLightTokens);
|
|
42
103
|
}));
|
|
43
104
|
describe("when theme is forced for provider", () => {
|
|
44
|
-
it("
|
|
105
|
+
it("preserves the forced effective theme when the selected theme changes", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
106
|
const { result } = renderHook(useAtlantisTheme, {
|
|
46
107
|
wrapper: (props) => (React.createElement(WrapperWithOverride, Object.assign({}, props, { dangerouslyOverrideTheme: "light" }))),
|
|
47
108
|
});
|
|
48
|
-
// Update the
|
|
109
|
+
// Update the selected theme through the forced subtree.
|
|
49
110
|
yield act(() => __awaiter(void 0, void 0, void 0, function* () { return result.current.setTheme("dark"); }));
|
|
50
|
-
|
|
51
|
-
expect(result.current.
|
|
111
|
+
expect(result.current.theme).toBe("dark");
|
|
112
|
+
expect(result.current.effectiveTheme).toBe("light");
|
|
113
|
+
expect(result.current.tokens).toEqual(expectedLightTokens);
|
|
114
|
+
}));
|
|
115
|
+
it("preserves the parent selected theme while forcing the effective theme", () => {
|
|
116
|
+
mockUseColorScheme.mockReturnValue("dark");
|
|
117
|
+
const { result } = renderHook(useAtlantisTheme, {
|
|
118
|
+
wrapper: (props) => (React.createElement(Wrapper, { theme: "system" },
|
|
119
|
+
React.createElement(AtlantisThemeContextProvider, Object.assign({ dangerouslyOverrideTheme: "light" }, props)))),
|
|
120
|
+
});
|
|
121
|
+
expect(result.current.theme).toBe("system");
|
|
122
|
+
expect(result.current.effectiveTheme).toBe("light");
|
|
123
|
+
expect(result.current.tokens).toEqual(expectedLightTokens);
|
|
124
|
+
});
|
|
125
|
+
it("forwards selected theme changes through a forced subtree", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
const { result } = renderHook(useAtlantisTheme, {
|
|
127
|
+
wrapper: (props) => (React.createElement(Wrapper, null,
|
|
128
|
+
React.createElement(AtlantisThemeContextProvider, Object.assign({ dangerouslyOverrideTheme: "light" }, props)))),
|
|
129
|
+
});
|
|
130
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () { return result.current.setTheme("dark"); }));
|
|
131
|
+
expect(result.current.theme).toBe("dark");
|
|
132
|
+
expect(result.current.effectiveTheme).toBe("light");
|
|
52
133
|
expect(result.current.tokens).toEqual(expectedLightTokens);
|
|
53
134
|
}));
|
|
54
135
|
it.each([
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
136
|
+
{
|
|
137
|
+
defaultTheme: "light",
|
|
138
|
+
colorScheme: "dark",
|
|
139
|
+
expectedEffectiveTheme: "light",
|
|
140
|
+
expectedTokens: expectedLightTokens,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
defaultTheme: "dark",
|
|
144
|
+
colorScheme: "light",
|
|
145
|
+
expectedEffectiveTheme: "dark",
|
|
146
|
+
expectedTokens: expectedDarkTokens,
|
|
147
|
+
},
|
|
148
|
+
])("provides the dangerouslyOverrideTheme $defaultTheme tokens", ({ defaultTheme, colorScheme, expectedEffectiveTheme, expectedTokens, }) => {
|
|
149
|
+
mockUseColorScheme.mockReturnValue(colorScheme);
|
|
58
150
|
const { result } = renderHook(useAtlantisTheme, {
|
|
59
151
|
wrapper: (props) => (React.createElement(WrapperWithOverride, Object.assign({}, props, { dangerouslyOverrideTheme: defaultTheme }))),
|
|
60
152
|
});
|
|
61
|
-
expect(result.current.theme).toBe(
|
|
153
|
+
expect(result.current.theme).toBe("light");
|
|
154
|
+
expect(result.current.effectiveTheme).toBe(expectedEffectiveTheme);
|
|
62
155
|
expect(result.current.tokens).toEqual(expectedTokens);
|
|
63
156
|
});
|
|
64
157
|
});
|
|
@@ -54,7 +54,7 @@ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen
|
|
|
54
54
|
const [currentPosition, setCurrentPosition] = useState(-1);
|
|
55
55
|
const styles = useStyles();
|
|
56
56
|
const { t } = useAtlantisI18n();
|
|
57
|
-
const {
|
|
57
|
+
const { effectiveTheme, tokens } = useAtlantisTheme();
|
|
58
58
|
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
59
59
|
const behavior = computeContentOverlayBehavior({
|
|
60
60
|
fullScreen,
|
|
@@ -179,7 +179,7 @@ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen
|
|
|
179
179
|
return (React.createElement(Backdrop, Object.assign({}, props, { pressBehavior: isCloseableOnOverlayTap ? "close" : "none" })));
|
|
180
180
|
}, [isCloseableOnOverlayTap]);
|
|
181
181
|
return (React.createElement(BottomSheetModal, { ref: bottomSheetModalRef, containerComponent: Platform.OS === "ios" ? Container : undefined, onChange: handleChange, style: sheetStyle, backgroundStyle: backgroundStyle, handleStyle: styles.handleWrapper, handleIndicatorStyle: handleIndicatorStyles, backdropComponent: backdropComponent, snapPoints: snapPoints, enablePanDownToClose: enablePanDownToClose !== null && enablePanDownToClose !== void 0 ? enablePanDownToClose : effectiveIsDraggable, enableContentPanningGesture: enableContentPanningGesture !== null && enableContentPanningGesture !== void 0 ? enableContentPanningGesture : effectiveIsDraggable, enableHandlePanningGesture: effectiveIsDraggable, enableDynamicSizing: !customSnapPoints && behavior.initialHeight === "contentHeight", keyboardBehavior: "interactive", keyboardBlurBehavior: "restore", topInset: topInset, onDismiss: () => onClose === null || onClose === void 0 ? void 0 : onClose() },
|
|
182
|
-
React.createElement(AtlantisThemeContextProvider, { dangerouslyOverrideTheme:
|
|
182
|
+
React.createElement(AtlantisThemeContextProvider, { dangerouslyOverrideTheme: effectiveTheme },
|
|
183
183
|
React.createElement(ContentOverlayKeyboardContext.Provider, { value: scrollEnabled }, scrollEnabled ? (React.createElement(BottomSheetKeyboardAwareScrollView, { ref: scrollViewRef, contentContainerStyle: { paddingBottom: insets.bottom }, keyboardShouldPersistTaps: keyboardShouldPersistTaps ? "handled" : "never", showsVerticalScrollIndicator: false, onScroll: handleOnScroll, stickyHeaderIndices: [0], bottomOffset: KEYBOARD_TOP_PADDING_AUTO_SCROLL },
|
|
184
184
|
renderHeader(),
|
|
185
185
|
React.createElement(View, { testID: "ATL-Overlay-Children" }, children))) : (React.createElement(BottomSheetView, null,
|