Skip to content

Commit 62ea8dc

Browse files
committed
feat: add dynamicColor prop and DynamicTheme refactor
1 parent b4a5696 commit 62ea8dc

7 files changed

Lines changed: 518 additions & 496 deletions

File tree

src/core/PaperProvider.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { defaultThemes, ThemeProvider } from './theming';
77
import MaterialCommunityIcon from '../components/MaterialCommunityIcon';
88
import PortalHost from '../components/Portal/PortalHost';
99
import { useAccessibleTheme } from '../theme/accessibility';
10+
import {
11+
isDynamicColorSupported,
12+
lightDynamicColors,
13+
darkDynamicColors,
14+
} from '../theme/schemes/DynamicTheme';
1015
import type { Theme, ThemeProp } from '../types';
1116

1217
export type Props = {
@@ -19,10 +24,17 @@ export type Props = {
1924
* accessibility in your own code.
2025
*/
2126
accessibilityAdapters?: boolean;
27+
/**
28+
* Whether to use Android Material You (dynamic) colors from the system wallpaper seed.
29+
* When `true`, dynamic colors override `theme.colors` on Android API 31+.
30+
* Falls back silently on unsupported platforms and API levels.
31+
* Set to `false` (default) when providing a fully custom color theme.
32+
*/
33+
dynamicColor?: boolean;
2234
};
2335

2436
const PaperProvider = (props: Props) => {
25-
const { accessibilityAdapters = true } = props;
37+
const { accessibilityAdapters = true, dynamicColor = false } = props;
2638

2739
const colorSchemeName =
2840
(!props.theme && Appearance?.getColorScheme()) || 'light';
@@ -59,17 +71,28 @@ const PaperProvider = (props: Props) => {
5971
}, [props.theme]);
6072

6173
const rawTheme = React.useMemo(() => {
62-
const scheme = colorScheme === 'dark' ? 'dark' : 'light';
74+
const effectiveDark = props.theme?.dark ?? colorScheme === 'dark';
75+
const scheme = effectiveDark ? 'dark' : 'light';
6376
const base = defaultThemes[scheme];
77+
const isDynamic = dynamicColor && isDynamicColorSupported;
78+
const dynamicColors = isDynamic
79+
? scheme === 'dark'
80+
? darkDynamicColors
81+
: lightDynamicColors
82+
: null;
6483
return {
6584
...base,
6685
...props.theme,
86+
...(dynamicColors
87+
? { colors: { ...base.colors, ...dynamicColors } }
88+
: {}),
6789
animation: {
6890
...props.theme?.animation,
6991
scale: props.theme?.animation?.scale ?? 1,
7092
},
71-
} as Theme;
72-
}, [colorScheme, props.theme]);
93+
dynamic: isDynamic,
94+
} as unknown as Theme;
95+
}, [colorScheme, props.theme, dynamicColor]);
7396

7497
const theme = useAccessibleTheme(rawTheme, accessibilityAdapters !== false);
7598

src/core/__tests__/PaperProvider.test.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ const createProvider = (theme?: ThemeProp) => {
103103
);
104104
};
105105

106-
const ExtendedLightTheme = { ...LightTheme } as ThemeProp;
107-
const ExtendedDarkTheme = { ...DarkTheme } as ThemeProp;
106+
const ExtendedLightTheme = { ...LightTheme, dynamic: false } as ThemeProp;
107+
const ExtendedDarkTheme = { ...DarkTheme, dynamic: false } as ThemeProp;
108108

109109
describe('PaperProvider', () => {
110110
beforeEach(() => {
@@ -134,6 +134,7 @@ describe('PaperProvider', () => {
134134
expect(getByTestId('provider-child-view').props.theme).toStrictEqual({
135135
...LightTheme,
136136
animation: { scale: 1, defaultAnimationDuration: 250 },
137+
dynamic: false,
137138
});
138139
});
139140

@@ -229,8 +230,9 @@ describe('PaperProvider', () => {
229230
},
230231
} as ThemeProp;
231232
const { getByTestId } = render(createProvider(customTheme));
232-
expect(getByTestId('provider-child-view').props.theme).toStrictEqual(
233-
customTheme
234-
);
233+
expect(getByTestId('provider-child-view').props.theme).toStrictEqual({
234+
...customTheme,
235+
dynamic: false,
236+
});
235237
});
236238
});

src/theme/provider.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ComponentType } from 'react';
33
import { $DeepPartial, createTheming } from '@callstack/react-theme-provider';
44

55
import { DarkTheme, LightTheme } from './schemes';
6-
import type { InternalTheme, Theme, NavigationTheme } from '../types';
6+
import type { Theme, NavigationTheme } from '../types';
77

88
export const DefaultTheme = LightTheme;
99

@@ -18,11 +18,11 @@ export function useTheme<T = Theme>(overrides?: $DeepPartial<T>) {
1818
}
1919

2020
export const useInternalTheme = (
21-
themeOverrides: $DeepPartial<InternalTheme> | undefined
22-
) => useAppTheme<InternalTheme>(themeOverrides);
21+
themeOverrides: $DeepPartial<Theme> | undefined
22+
) => useAppTheme<Theme>(themeOverrides);
2323

24-
export const withInternalTheme = <Props extends { theme: InternalTheme }, C>(
25-
WrappedComponent: ComponentType<Props & { theme: InternalTheme }> & C
24+
export const withInternalTheme = <Props extends { theme: Theme }, C>(
25+
WrappedComponent: ComponentType<Props & { theme: Theme }> & C
2626
) => withTheme<Props, C>(WrappedComponent);
2727

2828
export const defaultThemes = {

0 commit comments

Comments
 (0)