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/).