Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/eui/changelogs/upcoming/8036.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -102,6 +112,7 @@ export const GuideThemeSelector = () => {
label={item.label}
checked={item.checked}
onChange={item.onChange}
disabled={item.disabled}
/>
</div>
) : null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ export const HighContrastModeExample = {
<p>
Since this is done at a level that EUI can do nothing about, if
forced colors mode is detected by <strong>EuiProvider</strong>, EUI
will ignore any passed <EuiCode>highContrastMode</EuiCode> prop, as
this user choice and system setting takes precedence.
will ignore any passed <EuiCode>highContrastMode</EuiCode> or{' '}
<EuiCode>colorMode</EuiCode> props, as this user choice and system
setting takes precedence.
</p>
<EuiCallOut>
To quickly test your application in forced colors mode without
Expand Down
19 changes: 19 additions & 0 deletions packages/eui/src/services/theme/provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<EuiSystemDefaultsProvider>
<EuiThemeProvider colorMode="light">
<div css={({ euiTheme }) => ({ color: euiTheme.colors.fullShade })}>
Forced dark mode
</div>
</EuiThemeProvider>
</EuiSystemDefaultsProvider>
);

expect(getByText('Forced dark mode')).toHaveStyleRule('color', '#FFF');
});
});

describe('highContrastMode', () => {
Expand Down
18 changes: 11 additions & 7 deletions packages/eui/src/services/theme/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ export const EuiThemeProvider = <T extends {} = {}>({
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
Expand All @@ -99,22 +100,25 @@ export const EuiThemeProvider = <T extends {} = {}>({
: Fragment;
}, [isGlobalTheme, _modifications]);

const [system, setSystem] = useState(_system || parentSystem);
const prevSystemKey = useRef(system.key);

const [modifications, setModifications] = useState<EuiThemeModifications>(
mergeDeep(parentModifications, _modifications)
);
const prevModifications = useRef(modifications);

const [colorMode, setColorMode] = useState<EuiThemeColorModeStandard>(
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({
Expand Down Expand Up @@ -165,13 +169,13 @@ export const EuiThemeProvider = <T extends {} = {}>({
}, [_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) {
Expand Down
3 changes: 3 additions & 0 deletions packages/eui/src/services/theme/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
5 changes: 3 additions & 2 deletions packages/eui/src/services/theme/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand Down