diff --git a/packages/eui/changelogs/upcoming/8036.md b/packages/eui/changelogs/upcoming/8036.md index 3e47054f021..517c80e45ac 100644 --- a/packages/eui/changelogs/upcoming/8036.md +++ b/packages/eui/changelogs/upcoming/8036.md @@ -1,3 +1,4 @@ - Updated `EuiProvider` `and `EuiThemeProvider` with a new `highContrastMode` - This prop allows toggling a higher contrast visual style that primarily affects borders and shadows - - On `EuiProvider`, if the `highContrastMode` prop is not passed, this setting will inherit from the user's OS/system light/dark mode setting + - On `EuiProvider`, if the `highContrastMode` prop is not passed, this setting will inherit from the user's OS/system settings + - If the user is using a forced colors mode (e.g. Windows' high contrast themes), this system setting will take precedence over any `highContrastMode` or `colorMode` props passed diff --git a/packages/eui/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx b/packages/eui/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx index 2851a4941c5..e108f33ba07 100644 --- a/packages/eui/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx +++ b/packages/eui/src-docs/src/components/guide_theme_selector/guide_theme_selector.tsx @@ -18,9 +18,17 @@ import { export const GuideThemeSelector = () => { const context = useContext(ThemeContext); const euiThemeContext = useEuiTheme(); - const colorMode = context.colorMode ?? euiThemeContext.colorMode; + + const isForced = euiThemeContext.highContrastMode === 'forced'; + const colorMode = + context.colorMode && !isForced + ? context.colorMode + : euiThemeContext.colorMode; const highContrastMode = - context.highContrastMode ?? euiThemeContext.highContrastMode; + context.colorMode && !isForced + ? context.highContrastMode + : euiThemeContext.highContrastMode; + const currentTheme: EUI_THEME = EUI_THEMES.find((theme) => theme.value === context.theme) || EUI_THEMES[0]; @@ -51,12 +59,14 @@ export const GuideThemeSelector = () => { context.setContext({ colorMode: e.target.checked ? 'DARK' : 'LIGHT', }), + disabled: isForced, }, { label: 'High contrast', checked: !!highContrastMode, onChange: (e: EuiSwitchEvent) => context.setContext({ highContrastMode: e.target.checked }), + disabled: isForced, }, location.host.includes('803') && { label: 'i18n testing', @@ -102,6 +112,7 @@ export const GuideThemeSelector = () => { label={item.label} checked={item.checked} onChange={item.onChange} + disabled={item.disabled} /> ) : null diff --git a/packages/eui/src-docs/src/views/theme/high_contrast_mode/high_contrast_mode_example.js b/packages/eui/src-docs/src/views/theme/high_contrast_mode/high_contrast_mode_example.js index d779aaee562..13feda32737 100644 --- a/packages/eui/src-docs/src/views/theme/high_contrast_mode/high_contrast_mode_example.js +++ b/packages/eui/src-docs/src/views/theme/high_contrast_mode/high_contrast_mode_example.js @@ -78,8 +78,9 @@ export const HighContrastModeExample = {

Since this is done at a level that EUI can do nothing about, if forced colors mode is detected by EuiProvider, EUI - will ignore any passed highContrastMode prop, as - this user choice and system setting takes precedence. + will ignore any passed highContrastMode or{' '} + colorMode props, as this user choice and system + setting takes precedence.

To quickly test your application in forced colors mode without diff --git a/packages/eui/src/services/theme/provider.test.tsx b/packages/eui/src/services/theme/provider.test.tsx index 6637ec645b0..7200f8b8f11 100644 --- a/packages/eui/src/services/theme/provider.test.tsx +++ b/packages/eui/src/services/theme/provider.test.tsx @@ -67,6 +67,25 @@ describe('EuiThemeProvider', () => { '#000' ); }); + + it('detects if color mode is forced from the system and overrides any props', () => { + (useWindowMediaMatcher as jest.Mock).mockImplementation((media) => { + if (media === '(prefers-color-scheme: dark)') return true; + if (media === '(forced-colors: active)') return true; + }); + + const { getByText } = render( + + +
({ color: euiTheme.colors.fullShade })}> + Forced dark mode +
+
+
+ ); + + expect(getByText('Forced dark mode')).toHaveStyleRule('color', '#FFF'); + }); }); describe('highContrastMode', () => { diff --git a/packages/eui/src/services/theme/provider.tsx b/packages/eui/src/services/theme/provider.tsx index 2cf12cb62c3..25a182d32e9 100644 --- a/packages/eui/src/services/theme/provider.tsx +++ b/packages/eui/src/services/theme/provider.tsx @@ -87,8 +87,9 @@ export const EuiThemeProvider = ({ const parentHighContrastMode = useContext(EuiHighContrastModeContext); const parentTheme = useContext(EuiThemeContext); - const [system, setSystem] = useState(_system || parentSystem); - const prevSystemKey = useRef(system.key); + // If the user has an OS-wide high contrast theme applied, it will ignore EUI's + // colors and light/dark mode. We should respect the user's system setting + const isForced = parentHighContrastMode === 'forced'; // To reduce the number of window resize listeners, only render a // CurrentEuiBreakpointProvider for the top level parent theme, or for @@ -99,22 +100,25 @@ export const EuiThemeProvider = ({ : Fragment; }, [isGlobalTheme, _modifications]); + const [system, setSystem] = useState(_system || parentSystem); + const prevSystemKey = useRef(system.key); + const [modifications, setModifications] = useState( mergeDeep(parentModifications, _modifications) ); const prevModifications = useRef(modifications); const [colorMode, setColorMode] = useState( - getColorMode(_colorMode, parentColorMode) + getColorMode(_colorMode, parentColorMode, isForced) ); const prevColorMode = useRef(colorMode); const highContrastMode: EuiThemeHighContrastMode = useMemo(() => { - if (parentHighContrastMode === 'forced') return 'forced'; // System forced high contrast mode will always supercede application settings + if (isForced) return 'forced'; // System forced high contrast mode will always supercede application settings if (_highContrastMode === true) return 'preferred'; // Convert the boolean prop to our internal enum if (_highContrastMode === false) return false; // Allow `false` prop to override user/system preference return parentHighContrastMode; // Fall back to the parent/system setting - }, [_highContrastMode, parentHighContrastMode]); + }, [_highContrastMode, parentHighContrastMode, isForced]); const prevHighContrastMode = useRef(highContrastMode); const modificationsWithHighContrast = useHighContrastModifications({ @@ -165,13 +169,13 @@ export const EuiThemeProvider = ({ }, [_modifications, parentModifications]); useEffect(() => { - const newColorMode = getColorMode(_colorMode, parentColorMode); + const newColorMode = getColorMode(_colorMode, parentColorMode, isForced); if (!isEqual(newColorMode, prevColorMode.current)) { setColorMode(newColorMode); prevColorMode.current = newColorMode; isParentTheme.current = false; } - }, [_colorMode, parentColorMode]); + }, [_colorMode, parentColorMode, isForced]); useEffect(() => { if (prevHighContrastMode.current !== highContrastMode) { diff --git a/packages/eui/src/services/theme/utils.test.ts b/packages/eui/src/services/theme/utils.test.ts index c654e4d3c22..b7a4e51ce08 100644 --- a/packages/eui/src/services/theme/utils.test.ts +++ b/packages/eui/src/services/theme/utils.test.ts @@ -35,6 +35,9 @@ describe('getColorMode', () => { it('uses `parentMode` as fallback', () => { expect(getColorMode(undefined, 'DARK')).toEqual('DARK'); }); + it('uses `parentMode` (the system OS setting) if isForced is true', () => { + expect(getColorMode('LIGHT', 'DARK', true)).toEqual('DARK'); + }); it("understands 'INVERSE'", () => { expect(getColorMode('INVERSE', 'DARK')).toEqual('LIGHT'); expect(getColorMode('INVERSE', 'LIGHT')).toEqual('DARK'); diff --git a/packages/eui/src/services/theme/utils.ts b/packages/eui/src/services/theme/utils.ts index c7c9c5216d0..196686340b0 100644 --- a/packages/eui/src/services/theme/utils.ts +++ b/packages/eui/src/services/theme/utils.ts @@ -44,9 +44,10 @@ export const isInverseColorMode = ( */ export const getColorMode = ( colorMode?: EuiThemeColorMode, - parentColorMode?: EuiThemeColorModeStandard + parentColorMode?: EuiThemeColorModeStandard, + isForced?: boolean ): EuiThemeColorModeStandard => { - if (colorMode == null) { + if (isForced || colorMode == null) { return parentColorMode || DEFAULT_COLOR_MODE; } const mode = colorMode.toUpperCase() as diff --git a/packages/website/docs/components/theming/high_contrast_mode.mdx b/packages/website/docs/components/theming/high_contrast_mode.mdx index 1251beeaad7..11eb0e60538 100644 --- a/packages/website/docs/components/theming/high_contrast_mode.mdx +++ b/packages/website/docs/components/theming/high_contrast_mode.mdx @@ -72,7 +72,7 @@ export default () => { Please note that some OSes and browsers have something called [forced colors mode](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors), which overrides **all** colors, backgrounds, borders, and shadows. An example of this is Windows High Contrast modes. -Since this is done at a level that EUI can do nothing about, if forced colors mode is detected by **EuiProvider**, EUI will ignore *any* passed `highContrastMode` prop, as this user choice and system setting takes precedence. +Since this is done at a level that EUI can do nothing about, if forced colors mode is detected by **EuiProvider**, EUI will ignore *any* passed `highContrastMode` or `colorMode` prop, as this user choice and system setting takes precedence. :::tip To quickly test your application in forced colors mode without switching OS themes, you can [use Chrome or Edge's devtools to emulate forced-colors mode](https://devtoolstips.org/tips/en/emulate-forced-colors/).