@fluentui-react-native/storybook-desktop-runtime 0.2.1 → 0.3.1
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/.cache/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +48 -0
- package/README.md +18 -0
- package/config/metro.cjs +6 -1
- package/jest.config.cjs +1 -0
- package/lib/DesktopStoryRoot.d.ts.map +1 -1
- package/lib/DesktopStoryRoot.js +3 -2
- package/lib/DesktopStoryRoot.js.map +1 -1
- package/lib/StorybookTheme.d.ts +0 -1
- package/lib/StorybookTheme.d.ts.map +1 -1
- package/lib/StorybookTheme.js +29 -27
- package/lib/StorybookTheme.js.map +1 -1
- package/lib/StorybookTheme.test.d.ts +2 -0
- package/lib/StorybookTheme.test.d.ts.map +1 -0
- package/lib/StorybookTheme.test.js +131 -0
- package/lib/StorybookTheme.test.js.map +1 -0
- package/lib/StorybookUI.win32.js +3 -3
- package/lib/StorybookUI.win32.js.map +1 -1
- package/lib/ThemedStorybookUI.d.ts +3 -0
- package/lib/ThemedStorybookUI.d.ts.map +1 -0
- package/lib/ThemedStorybookUI.js +8 -0
- package/lib/ThemedStorybookUI.js.map +1 -0
- package/lib/Win32CalloutPortal.d.ts.map +1 -1
- package/lib/Win32CalloutPortal.js +3 -2
- package/lib/Win32CalloutPortal.js.map +1 -1
- package/lib/createDesktopStorybookApp.d.ts.map +1 -1
- package/lib/createDesktopStorybookApp.js +4 -14
- package/lib/createDesktopStorybookApp.js.map +1 -1
- package/lib/createDesktopStorybookPreview.d.ts.map +1 -1
- package/lib/createDesktopStorybookPreview.js +1 -2
- package/lib/createDesktopStorybookPreview.js.map +1 -1
- package/lib/useStorybookTheme.d.ts +3 -0
- package/lib/useStorybookTheme.d.ts.map +1 -0
- package/lib/useStorybookTheme.js +106 -0
- package/lib/useStorybookTheme.js.map +1 -0
- package/lib/useStorybookTheme.test.d.ts +2 -0
- package/lib/useStorybookTheme.test.d.ts.map +1 -0
- package/lib/useStorybookTheme.test.js +66 -0
- package/lib/useStorybookTheme.test.js.map +1 -0
- package/package.json +13 -6
- package/src/DesktopStoryRoot.tsx +3 -2
- package/src/StorybookTheme.test.tsx +164 -0
- package/src/StorybookTheme.tsx +91 -78
- package/src/StorybookUI.win32.tsx +3 -3
- package/src/ThemedStorybookUI.tsx +9 -0
- package/src/Win32CalloutPortal.tsx +4 -3
- package/src/createDesktopStorybookApp.tsx +6 -25
- package/src/createDesktopStorybookPreview.tsx +5 -8
- package/src/useStorybookTheme.test.tsx +92 -0
- package/src/useStorybookTheme.ts +113 -0
package/src/StorybookTheme.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { createDefaultTheme } from '@fluentui-react-native/default-theme';
|
|
5
|
-
import {
|
|
5
|
+
import { ThemedRoot, themedStyleSheetFactory, useThemeState } from '@fluentui-react-native/design';
|
|
6
6
|
import type { ThemeReference } from '@fluentui-react-native/design/theming';
|
|
7
7
|
|
|
8
8
|
import { useDesktopStorybookTestID } from './DesktopStorybookConfig';
|
|
@@ -16,99 +16,112 @@ const themeChoiceNames = ['none', 'light', 'dark', 'highContrast'] as const;
|
|
|
16
16
|
type ThemeChoiceName = (typeof themeChoiceNames)[number];
|
|
17
17
|
|
|
18
18
|
const themeChoices: Record<ThemeChoiceName, ThemeChoice> = {
|
|
19
|
-
none: { label: '
|
|
19
|
+
none: { label: 'Default Flex' },
|
|
20
20
|
light: { label: 'Light', theme: createDefaultTheme({ appearance: 'light' }) },
|
|
21
21
|
dark: { label: 'Dark', theme: createDefaultTheme({ appearance: 'dark' }) },
|
|
22
22
|
highContrast: { label: 'High contrast', theme: createDefaultTheme({ appearance: 'highContrast' }) },
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const StorybookThemeContext = React.createContext<ThemeReference | undefined>(undefined);
|
|
26
|
-
|
|
27
25
|
export function StorybookThemeHost({ children }: React.PropsWithChildren) {
|
|
28
26
|
const [selectedName, setSelectedName] = React.useState<ThemeChoiceName>('none');
|
|
29
|
-
const
|
|
27
|
+
const testID = useDesktopStorybookTestID('app-root');
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<ThemedRoot style={layoutStyles.root} testID={testID} theme={themeChoices[selectedName].theme}>
|
|
31
|
+
<StorybookThemeContent selectedName={selectedName} onSelect={setSelectedName}>
|
|
32
|
+
{children}
|
|
33
|
+
</StorybookThemeContent>
|
|
34
|
+
</ThemedRoot>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function StorybookThemeContent({
|
|
39
|
+
children,
|
|
40
|
+
selectedName,
|
|
41
|
+
onSelect,
|
|
42
|
+
}: React.PropsWithChildren<{ selectedName: ThemeChoiceName; onSelect: (name: ThemeChoiceName) => void }>) {
|
|
43
|
+
const styles = getStyles(useThemeState());
|
|
30
44
|
const toolbarTestID = useDesktopStorybookTestID('theme-toolbar');
|
|
31
45
|
const optionTestID = useDesktopStorybookTestID('theme');
|
|
32
46
|
|
|
33
47
|
return (
|
|
34
|
-
<
|
|
35
|
-
<View style={styles.
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})}
|
|
54
|
-
</View>
|
|
55
|
-
<View style={styles.storybook}>{children}</View>
|
|
48
|
+
<View style={styles.root}>
|
|
49
|
+
<View accessibilityRole="toolbar" style={styles.header} testID={toolbarTestID}>
|
|
50
|
+
<Text style={styles.label}>Theme</Text>
|
|
51
|
+
{themeChoiceNames.map((name) => {
|
|
52
|
+
const choice = themeChoices[name];
|
|
53
|
+
const selected = name === selectedName;
|
|
54
|
+
return (
|
|
55
|
+
<Pressable
|
|
56
|
+
accessibilityRole="button"
|
|
57
|
+
accessibilityState={{ selected }}
|
|
58
|
+
key={name}
|
|
59
|
+
onPress={() => onSelect(name)}
|
|
60
|
+
style={({ pressed }) => [styles.option, selected && styles.selectedOption, pressed && styles.pressedOption]}
|
|
61
|
+
testID={`${optionTestID}-${name}`}
|
|
62
|
+
>
|
|
63
|
+
<Text style={[styles.optionText, selected && styles.selectedOptionText]}>{choice.label}</Text>
|
|
64
|
+
</Pressable>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
56
67
|
</View>
|
|
57
|
-
|
|
68
|
+
<View style={layoutStyles.root}>{children}</View>
|
|
69
|
+
</View>
|
|
58
70
|
);
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
const theme = React.useContext(StorybookThemeContext);
|
|
63
|
-
return theme ? <ThemeProvider theme={theme}>{children}</ThemeProvider> : children;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const styles = StyleSheet.create({
|
|
73
|
+
const layoutStyles = StyleSheet.create({
|
|
67
74
|
root: {
|
|
68
75
|
flex: 1,
|
|
69
76
|
},
|
|
70
|
-
header: {
|
|
71
|
-
alignItems: 'center',
|
|
72
|
-
backgroundColor: '#f5f5f5',
|
|
73
|
-
borderBottomColor: '#d1d1d1',
|
|
74
|
-
borderBottomWidth: 1,
|
|
75
|
-
flexDirection: 'row',
|
|
76
|
-
flexWrap: 'wrap',
|
|
77
|
-
gap: 8,
|
|
78
|
-
minHeight: 44,
|
|
79
|
-
paddingHorizontal: 12,
|
|
80
|
-
paddingVertical: 6,
|
|
81
|
-
},
|
|
82
|
-
label: {
|
|
83
|
-
color: '#242424',
|
|
84
|
-
fontSize: 14,
|
|
85
|
-
fontWeight: '600',
|
|
86
|
-
marginEnd: 4,
|
|
87
|
-
},
|
|
88
|
-
option: {
|
|
89
|
-
backgroundColor: '#ffffff',
|
|
90
|
-
borderColor: '#b3b3b3',
|
|
91
|
-
borderRadius: 4,
|
|
92
|
-
borderWidth: 1,
|
|
93
|
-
paddingHorizontal: 10,
|
|
94
|
-
paddingVertical: 5,
|
|
95
|
-
},
|
|
96
|
-
selectedOption: {
|
|
97
|
-
backgroundColor: '#0f6cbd',
|
|
98
|
-
borderColor: '#0f6cbd',
|
|
99
|
-
},
|
|
100
|
-
pressedOption: {
|
|
101
|
-
opacity: 0.75,
|
|
102
|
-
},
|
|
103
|
-
optionText: {
|
|
104
|
-
color: '#242424',
|
|
105
|
-
fontSize: 12,
|
|
106
|
-
},
|
|
107
|
-
selectedOptionText: {
|
|
108
|
-
color: '#ffffff',
|
|
109
|
-
fontWeight: '600',
|
|
110
|
-
},
|
|
111
|
-
storybook: {
|
|
112
|
-
flex: 1,
|
|
113
|
-
},
|
|
114
77
|
});
|
|
78
|
+
|
|
79
|
+
const getStyles = themedStyleSheetFactory('StorybookTheme', ({ tokens }) =>
|
|
80
|
+
StyleSheet.create({
|
|
81
|
+
root: {
|
|
82
|
+
flex: 1,
|
|
83
|
+
backgroundColor: tokens.color.surfaceNeutralFar,
|
|
84
|
+
},
|
|
85
|
+
header: {
|
|
86
|
+
alignItems: 'center',
|
|
87
|
+
backgroundColor: tokens.color.backgroundNeutralSubtle,
|
|
88
|
+
borderBottomColor: tokens.color.strokeNeutralSubtle,
|
|
89
|
+
borderBottomWidth: 1,
|
|
90
|
+
flexDirection: 'row',
|
|
91
|
+
flexWrap: 'wrap',
|
|
92
|
+
gap: 8,
|
|
93
|
+
minHeight: 44,
|
|
94
|
+
paddingHorizontal: 12,
|
|
95
|
+
paddingVertical: 6,
|
|
96
|
+
},
|
|
97
|
+
label: {
|
|
98
|
+
color: tokens.color.foregroundNeutralPrimary,
|
|
99
|
+
fontSize: 14,
|
|
100
|
+
fontWeight: '600',
|
|
101
|
+
marginEnd: 4,
|
|
102
|
+
},
|
|
103
|
+
option: {
|
|
104
|
+
backgroundColor: tokens.color.backgroundNeutralLoud,
|
|
105
|
+
borderColor: tokens.color.strokeNeutralSoft,
|
|
106
|
+
borderRadius: 4,
|
|
107
|
+
borderWidth: 1,
|
|
108
|
+
paddingHorizontal: 10,
|
|
109
|
+
paddingVertical: 5,
|
|
110
|
+
},
|
|
111
|
+
selectedOption: {
|
|
112
|
+
backgroundColor: tokens.color.backgroundBrandHeavy,
|
|
113
|
+
borderColor: tokens.color.strokeBrandLoud,
|
|
114
|
+
},
|
|
115
|
+
pressedOption: {
|
|
116
|
+
opacity: 0.75,
|
|
117
|
+
},
|
|
118
|
+
optionText: {
|
|
119
|
+
color: tokens.color.foregroundNeutralPrimary,
|
|
120
|
+
fontSize: 12,
|
|
121
|
+
},
|
|
122
|
+
selectedOptionText: {
|
|
123
|
+
color: tokens.color.foregroundBrandOnloud,
|
|
124
|
+
fontWeight: '600',
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
@@ -14,7 +14,7 @@ import { Win32ResizeHandle } from './Win32ResizeHandle';
|
|
|
14
14
|
const emptyRefs = {};
|
|
15
15
|
const emptyStatus = {};
|
|
16
16
|
|
|
17
|
-
export const StorybookUIComponent: SBUI = ({ children, setStory, storage, story, storyHash, theme }) => {
|
|
17
|
+
export const StorybookUIComponent: SBUI = ({ children, setStory, storage, story, storyHash, storyBackgroundColor, theme }) => {
|
|
18
18
|
const storiesAnchorRef = React.useRef<View>(null);
|
|
19
19
|
const addonsAnchorRef = React.useRef<View>(null);
|
|
20
20
|
const [sidebarVisible, setSidebarVisible] = React.useState(true);
|
|
@@ -67,7 +67,7 @@ export const StorybookUIComponent: SBUI = ({ children, setStory, storage, story,
|
|
|
67
67
|
return (
|
|
68
68
|
<ThemeProvider theme={theme}>
|
|
69
69
|
<StorageProvider storage={storage}>
|
|
70
|
-
<View style={styles.root} testID={chromeTestID}>
|
|
70
|
+
<View style={[styles.root, { backgroundColor: theme.background.app }]} testID={chromeTestID}>
|
|
71
71
|
{sidebarVisible ? (
|
|
72
72
|
<>
|
|
73
73
|
<View
|
|
@@ -174,7 +174,7 @@ export const StorybookUIComponent: SBUI = ({ children, setStory, storage, story,
|
|
|
174
174
|
</Text>
|
|
175
175
|
</View>
|
|
176
176
|
) : null}
|
|
177
|
-
<View style={styles.preview} testID={previewTestID}>
|
|
177
|
+
<View style={[styles.preview, { backgroundColor: storyBackgroundColor ?? theme.background.preview }]} testID={previewTestID}>
|
|
178
178
|
{children}
|
|
179
179
|
</View>
|
|
180
180
|
{addonsVisible ? (
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SBUI } from '@storybook/react-native-ui-common';
|
|
2
|
+
|
|
3
|
+
import { StorybookUIComponent } from './StorybookUI';
|
|
4
|
+
import { useStorybookTheme } from './useStorybookTheme';
|
|
5
|
+
|
|
6
|
+
export const ThemedStorybookUI: SBUI = (props) => {
|
|
7
|
+
const theme = useStorybookTheme();
|
|
8
|
+
return <StorybookUIComponent {...props} theme={theme} />;
|
|
9
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { StyleSheet
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { Callout } from '@fluentui-react-native/callout';
|
|
5
5
|
import type { CalloutProps, DirectionalHint } from '@fluentui-react-native/callout';
|
|
6
|
+
import { RootInputBoundary } from '@fluentui-react-native/design';
|
|
6
7
|
import { useTheme } from '@storybook/react-native-theming';
|
|
7
8
|
|
|
8
9
|
type Win32CalloutPortalProps = React.PropsWithChildren<{
|
|
@@ -55,13 +56,13 @@ export function Win32CalloutPortal({
|
|
|
55
56
|
target={target}
|
|
56
57
|
testID={testID}
|
|
57
58
|
>
|
|
58
|
-
<
|
|
59
|
+
<RootInputBoundary
|
|
59
60
|
collapsable={false}
|
|
60
61
|
style={[styles.content, { backgroundColor: theme.background.content, height, width }]}
|
|
61
62
|
testID={`${testID}-content`}
|
|
62
63
|
>
|
|
63
64
|
{children}
|
|
64
|
-
</
|
|
65
|
+
</RootInputBoundary>
|
|
65
66
|
</Callout>
|
|
66
67
|
);
|
|
67
68
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import type * as React from 'react';
|
|
2
|
-
import { StyleSheet, View as NativeView } from 'react-native';
|
|
3
1
|
import type { View } from '@storybook/react-native';
|
|
4
2
|
|
|
5
3
|
import { DesktopDriverBridge } from './DesktopDriverBridge';
|
|
6
|
-
import { DesktopStorybookConfigProvider
|
|
4
|
+
import { DesktopStorybookConfigProvider } from './DesktopStorybookConfig';
|
|
7
5
|
import type { DesktopStorybookRuntimeInstance } from './DesktopStorybookConfig';
|
|
8
6
|
import { StorybookThemeHost } from './StorybookTheme';
|
|
9
|
-
import {
|
|
7
|
+
import { ThemedStorybookUI } from './ThemedStorybookUI';
|
|
10
8
|
|
|
11
9
|
export type DesktopStorybookOptions = {
|
|
12
10
|
enableWebsockets?: boolean;
|
|
@@ -35,7 +33,7 @@ export function createDesktopStorybookApp(
|
|
|
35
33
|
enableWebsockets,
|
|
36
34
|
host,
|
|
37
35
|
port: port ?? runtimeInstance?.storybookPort ?? 7007,
|
|
38
|
-
CustomUIComponent:
|
|
36
|
+
CustomUIComponent: ThemedStorybookUI,
|
|
39
37
|
storage,
|
|
40
38
|
});
|
|
41
39
|
|
|
@@ -43,30 +41,13 @@ export function createDesktopStorybookApp(
|
|
|
43
41
|
const resolvedTestIDPrefix = runtimeInstance?.testIDPrefix ?? testIDPrefix;
|
|
44
42
|
return (
|
|
45
43
|
<DesktopStorybookConfigProvider runtimeInstance={runtimeInstance} testIDPrefix={resolvedTestIDPrefix}>
|
|
46
|
-
<
|
|
44
|
+
<StorybookThemeHost>
|
|
47
45
|
<DesktopDriverBridge />
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
</StorybookThemeHost>
|
|
51
|
-
</DesktopStorybookAppRoot>
|
|
46
|
+
<StorybookUI />
|
|
47
|
+
</StorybookThemeHost>
|
|
52
48
|
</DesktopStorybookConfigProvider>
|
|
53
49
|
);
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
return DesktopStorybookApp;
|
|
57
53
|
}
|
|
58
|
-
|
|
59
|
-
function DesktopStorybookAppRoot({ children }: React.PropsWithChildren) {
|
|
60
|
-
const testID = useDesktopStorybookTestID('app-root');
|
|
61
|
-
return (
|
|
62
|
-
<NativeView style={styles.root} testID={testID}>
|
|
63
|
-
{children}
|
|
64
|
-
</NativeView>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const styles = StyleSheet.create({
|
|
69
|
-
root: {
|
|
70
|
-
flex: 1,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
@@ -2,19 +2,16 @@ import { StyleSheet, View } from 'react-native';
|
|
|
2
2
|
import type { Preview } from '@storybook/react-native';
|
|
3
3
|
|
|
4
4
|
import { DesktopStoryRoot } from './DesktopStoryRoot';
|
|
5
|
-
import { StorybookThemeProvider } from './StorybookTheme';
|
|
6
5
|
|
|
7
6
|
export function createDesktopStorybookPreview(): Preview {
|
|
8
7
|
return {
|
|
9
8
|
decorators: [
|
|
10
9
|
(Story, context) => (
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
</DesktopStoryRoot>
|
|
17
|
-
</StorybookThemeProvider>
|
|
10
|
+
<DesktopStoryRoot storyId={context.id}>
|
|
11
|
+
<View style={styles.story}>
|
|
12
|
+
<Story />
|
|
13
|
+
</View>
|
|
14
|
+
</DesktopStoryRoot>
|
|
18
15
|
),
|
|
19
16
|
],
|
|
20
17
|
parameters: {},
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { PlatformColor } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { act, render } from '@testing-library/react-native';
|
|
4
|
+
import { FlexThemeReference, ThemedRoot, useThemeState } from '@fluentui-react-native/design';
|
|
5
|
+
import type { ThemeState } from '@fluentui-react-native/design';
|
|
6
|
+
import type { Theme } from '@storybook/react-native-theming';
|
|
7
|
+
|
|
8
|
+
import { useStorybookTheme } from './useStorybookTheme';
|
|
9
|
+
|
|
10
|
+
function Probe({ themes, states }: { themes: Theme[]; states?: ThemeState[] }) {
|
|
11
|
+
themes.push(useStorybookTheme());
|
|
12
|
+
const state = useThemeState();
|
|
13
|
+
states?.push(state);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Storybook theme palette', () => {
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('derives colors from the current tokens and updates after source invalidation', async () => {
|
|
23
|
+
const themes: Theme[] = [];
|
|
24
|
+
const states: ThemeState[] = [];
|
|
25
|
+
const source = new FlexThemeReference({
|
|
26
|
+
base: { color: { surfaceNeutralFar: '#123456', foregroundNeutralPrimary: '#abcdef' } },
|
|
27
|
+
});
|
|
28
|
+
const scene = await render(
|
|
29
|
+
<ThemedRoot theme={source}>
|
|
30
|
+
<Probe themes={themes} states={states} />
|
|
31
|
+
</ThemedRoot>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(themes[0].background.app).toBe('#123456');
|
|
35
|
+
expect(themes[0].background.content).toBe('#123456');
|
|
36
|
+
expect(themes[0].background.preview).toBe('#123456');
|
|
37
|
+
expect(themes[0].color.defaultText).toBe('#abcdef');
|
|
38
|
+
await scene.rerender(
|
|
39
|
+
<ThemedRoot theme={source}>
|
|
40
|
+
<Probe themes={themes} states={states} />
|
|
41
|
+
</ThemedRoot>,
|
|
42
|
+
);
|
|
43
|
+
expect(themes[themes.length - 1]).toBe(themes[0]);
|
|
44
|
+
|
|
45
|
+
await act(() => source.update({ color: { surfaceNeutralFar: '#654321' } }));
|
|
46
|
+
expect(themes[themes.length - 1].background.app).toBe('#654321');
|
|
47
|
+
expect(themes[themes.length - 1].background.content).toBe('#654321');
|
|
48
|
+
expect(themes[themes.length - 1].background.preview).toBe('#654321');
|
|
49
|
+
expect(themes[themes.length - 1]).not.toBe(themes[0]);
|
|
50
|
+
expect(themes[themes.length - 1].appBorderColor).toBe(states[states.length - 1].tokens.color.strokeNeutralSubtle);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('warns and substitutes a contrasting theme color for native opaque colors', async () => {
|
|
54
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation();
|
|
55
|
+
const themes: Theme[] = [];
|
|
56
|
+
const states: ThemeState[] = [];
|
|
57
|
+
const source = new FlexThemeReference({ base: { color: { foregroundBrandPrimary: PlatformColor('labelColor') } } });
|
|
58
|
+
await render(
|
|
59
|
+
<ThemedRoot theme={source}>
|
|
60
|
+
<Probe themes={themes} states={states} />
|
|
61
|
+
</ThemedRoot>,
|
|
62
|
+
);
|
|
63
|
+
expect(themes[0].color.primary).toBe(states[0].tokens.color.fixedBlack);
|
|
64
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
65
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('foregroundBrandPrimary'));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it.each([
|
|
69
|
+
['light', 'standard', 'fixedWhite'],
|
|
70
|
+
['dark', 'standard', 'fixedBlack'],
|
|
71
|
+
['light', 'highContrast', 'fixedBlack'],
|
|
72
|
+
['dark', 'highContrast', 'fixedBlack'],
|
|
73
|
+
] as const)('retains the native canvas fallback for %s/%s', async (colorScheme, contrast, fallback) => {
|
|
74
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation();
|
|
75
|
+
const themes: Theme[] = [];
|
|
76
|
+
const states: ThemeState[] = [];
|
|
77
|
+
const source = new FlexThemeReference({
|
|
78
|
+
appearance: { colorScheme, contrast },
|
|
79
|
+
base: { color: { surfaceNeutralFar: PlatformColor('windowBackgroundColor') } },
|
|
80
|
+
});
|
|
81
|
+
await render(
|
|
82
|
+
<ThemedRoot theme={source}>
|
|
83
|
+
<Probe themes={themes} states={states} />
|
|
84
|
+
</ThemedRoot>,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(themes[0].background.content).toBe(states[0].tokens.color[fallback]);
|
|
88
|
+
expect(themes[0].background.preview).toBe(states[0].tokens.color[fallback]);
|
|
89
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('surfaceNeutralFar'));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { useThemeState } from '@fluentui-react-native/design';
|
|
4
|
+
import type { SemanticColorTokenValues, ThemeState } from '@fluentui-react-native/design';
|
|
5
|
+
import { darkTheme, theme as lightTheme } from '@storybook/react-native-theming';
|
|
6
|
+
import type { Theme } from '@storybook/react-native-theming';
|
|
7
|
+
|
|
8
|
+
export function useStorybookTheme(): Theme {
|
|
9
|
+
const state = useThemeState();
|
|
10
|
+
const { theme, nativeColors } = React.useMemo(() => storybookThemeFromThemeState(state), [state]);
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
if (nativeColors.length > 0) {
|
|
13
|
+
console.warn(
|
|
14
|
+
`Storybook chrome cannot resolve native system colors; using fixed contrasting theme colors for: ${nativeColors.join(', ')}.`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}, [nativeColors]);
|
|
18
|
+
return theme;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function storybookThemeFromThemeState({ appearance, highContrast, tokens }: ThemeState) {
|
|
22
|
+
const base = highContrast || appearance.colorScheme === 'dark' ? darkTheme : lightTheme;
|
|
23
|
+
const foreground = base.base === 'dark' ? 'fixedWhite' : 'fixedBlack';
|
|
24
|
+
const background = base.base === 'dark' ? 'fixedBlack' : 'fixedWhite';
|
|
25
|
+
const nativeColors = new Set<keyof SemanticColorTokenValues>();
|
|
26
|
+
const color = (name: keyof SemanticColorTokenValues, role: 'foreground' | 'background' = 'foreground'): string => {
|
|
27
|
+
const value = tokens.color[name];
|
|
28
|
+
// Storybook also passes these colors to JS color utilities, not just native styles.
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
nativeColors.add(name);
|
|
33
|
+
const fallbackName = role === 'foreground' ? foreground : background;
|
|
34
|
+
const fallback = tokens.color[fallbackName];
|
|
35
|
+
if (typeof fallback !== 'string') {
|
|
36
|
+
throw new Error(`Storybook chrome requires a literal "${fallbackName}" fallback for the "${name}" theme token.`);
|
|
37
|
+
}
|
|
38
|
+
return fallback;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const theme: Theme = {
|
|
42
|
+
...base,
|
|
43
|
+
color: {
|
|
44
|
+
...base.color,
|
|
45
|
+
primary: color('foregroundBrandPrimary'),
|
|
46
|
+
secondary: color('backgroundBrandHeavy'),
|
|
47
|
+
secondaryLighter: color('backgroundBrandSoft', 'background'),
|
|
48
|
+
tertiary: color('foregroundSuccessPrimary'),
|
|
49
|
+
ancillary: color('foregroundBrandPrimary'),
|
|
50
|
+
orange: color('foregroundWarningPrimary'),
|
|
51
|
+
gold: color('foregroundWarningPrimary'),
|
|
52
|
+
green: color('foregroundSuccessPrimary'),
|
|
53
|
+
seafoam: color('foregroundSuccessPrimary'),
|
|
54
|
+
purple: color('foregroundBrandPrimary'),
|
|
55
|
+
ultraviolet: color('foregroundBrandPrimary'),
|
|
56
|
+
lightest: color('foregroundBrandOnloud', 'background'),
|
|
57
|
+
lighter: color('surfaceNeutralNear', 'background'),
|
|
58
|
+
light: color('backgroundNeutralSubtle', 'background'),
|
|
59
|
+
mediumlight: color('strokeNeutralSubtle'),
|
|
60
|
+
medium: color('strokeNeutralSoft'),
|
|
61
|
+
mediumdark: color('foregroundNeutralSecondary'),
|
|
62
|
+
dark: color('foregroundNeutralPrimary'),
|
|
63
|
+
darker: color('foregroundNeutralPrimary'),
|
|
64
|
+
darkest: color('foregroundNeutralPrimary'),
|
|
65
|
+
border: color('strokeNeutralSoft'),
|
|
66
|
+
positive: color('foregroundSuccessPrimary'),
|
|
67
|
+
negative: color('foregroundDangerPrimary'),
|
|
68
|
+
warning: color('foregroundWarningPrimary'),
|
|
69
|
+
critical: color('foregroundDangerPrimary'),
|
|
70
|
+
defaultText: color('foregroundNeutralPrimary'),
|
|
71
|
+
inverseText: color('foregroundNeutralOnloud', 'background'),
|
|
72
|
+
positiveText: color('foregroundSuccessPrimary'),
|
|
73
|
+
negativeText: color('foregroundDangerPrimary'),
|
|
74
|
+
warningText: color('foregroundWarningPrimary'),
|
|
75
|
+
},
|
|
76
|
+
background: {
|
|
77
|
+
...base.background,
|
|
78
|
+
app: color('surfaceNeutralFar', 'background'),
|
|
79
|
+
bar: color('backgroundNeutralSubtle', 'background'),
|
|
80
|
+
// LiteUI and StoryView paint the canvas with content rather than preview.
|
|
81
|
+
content: color('surfaceNeutralFar', 'background'),
|
|
82
|
+
preview: color('surfaceNeutralFar', 'background'),
|
|
83
|
+
hoverable: color('backgroundNeutralSoft', 'background'),
|
|
84
|
+
positive: color('backgroundSuccessSubtle', 'background'),
|
|
85
|
+
negative: color('backgroundDangerSubtle', 'background'),
|
|
86
|
+
warning: color('backgroundWarningSubtle', 'background'),
|
|
87
|
+
critical: color('backgroundDangerSubtle', 'background'),
|
|
88
|
+
},
|
|
89
|
+
textMutedColor: color('foregroundNeutralTertiary'),
|
|
90
|
+
input: {
|
|
91
|
+
...base.input,
|
|
92
|
+
border: color('strokeNeutralSoft'),
|
|
93
|
+
background: color('backgroundNeutralLoud', 'background'),
|
|
94
|
+
color: color('foregroundNeutralPrimary'),
|
|
95
|
+
},
|
|
96
|
+
button: {
|
|
97
|
+
...base.button,
|
|
98
|
+
background: color('backgroundNeutralLoud', 'background'),
|
|
99
|
+
border: color('strokeNeutralSoft'),
|
|
100
|
+
},
|
|
101
|
+
boolean: {
|
|
102
|
+
...base.boolean,
|
|
103
|
+
background: color('backgroundNeutralSoft', 'background'),
|
|
104
|
+
selectedBackground: color('backgroundBrandHeavy'),
|
|
105
|
+
},
|
|
106
|
+
appBorderColor: color('strokeNeutralSubtle'),
|
|
107
|
+
barTextColor: color('foregroundNeutralPrimary'),
|
|
108
|
+
barHoverColor: color('foregroundBrandPrimary'),
|
|
109
|
+
barSelectedColor: color('foregroundBrandPrimary'),
|
|
110
|
+
barBg: color('backgroundNeutralSubtle', 'background'),
|
|
111
|
+
};
|
|
112
|
+
return { theme, nativeColors: [...nativeColors] };
|
|
113
|
+
}
|