495 lines
15 KiB
JavaScript
495 lines
15 KiB
JavaScript
import accordionSummaryClasses from "../AccordionSummary/accordionSummaryClasses.mjs";
|
|
import autocompleteClasses from "../Autocomplete/autocompleteClasses.mjs";
|
|
import checkboxClasses from "../Checkbox/checkboxClasses.mjs";
|
|
import filledInputClasses from "../FilledInput/filledInputClasses.mjs";
|
|
import formControlLabelClasses from "../FormControlLabel/formControlLabelClasses.mjs";
|
|
import formHelperTextClasses from "../FormHelperText/formHelperTextClasses.mjs";
|
|
import formLabelClasses from "../FormLabel/formLabelClasses.mjs";
|
|
import inputClasses from "../Input/inputClasses.mjs";
|
|
import listItemButtonClasses from "../ListItemButton/listItemButtonClasses.mjs";
|
|
import menuItemClasses from "../MenuItem/menuItemClasses.mjs";
|
|
import nativeSelectClasses from "../NativeSelect/nativeSelectClasses.mjs";
|
|
import outlinedInputClasses from "../OutlinedInput/outlinedInputClasses.mjs";
|
|
import radioClasses from "../Radio/radioClasses.mjs";
|
|
import sliderClasses from "../Slider/sliderClasses.mjs";
|
|
import toggleButtonClasses from "../ToggleButton/toggleButtonClasses.mjs";
|
|
// System color keywords used in forced-colors / high contrast mode.
|
|
const defaultHcTokens = {
|
|
disabled: 'GrayText',
|
|
error: 'ActiveText',
|
|
selectedBackground: 'SelectedItem',
|
|
selectedText: 'SelectedItemText',
|
|
activeBackground: 'Highlight',
|
|
activeText: 'HighlightText',
|
|
buttonBorder: 'ButtonBorder',
|
|
buttonText: 'ButtonText',
|
|
canvas: 'Canvas'
|
|
};
|
|
const HCM = '@media (forced-colors: active)';
|
|
|
|
/**
|
|
* Enhances a theme with styles for Windows High Contrast Mode (forced-colors).
|
|
*
|
|
* Accepts a fully-created theme, merges in HCM component overrides using arrays
|
|
* so that Emotion emits each entry as a separate CSS rule and the browser
|
|
* cascade (rather than JS object merging) resolves specificity.
|
|
*
|
|
* @param themeInput - The theme to enhance.
|
|
* @param tokens - Override any of the default system color tokens.
|
|
* @returns The enhanced theme (same type as the input).
|
|
*
|
|
* @example
|
|
* // Use defaults
|
|
* const theme = enhanceHighContrast(createTheme({ palette: { ... } }));
|
|
*
|
|
* @example
|
|
* // Override specific tokens
|
|
* const theme = enhanceHighContrast(createTheme(), { disabled: 'ButtonText' });
|
|
*/
|
|
export default function enhanceHighContrast(themeInput, tokens) {
|
|
const hcTokens = {
|
|
disabled: tokens?.disabled ?? defaultHcTokens.disabled,
|
|
error: tokens?.error ?? defaultHcTokens.error,
|
|
selectedBackground: tokens?.selectedBackground ?? defaultHcTokens.selectedBackground,
|
|
selectedText: tokens?.selectedText ?? defaultHcTokens.selectedText,
|
|
activeBackground: tokens?.activeBackground ?? defaultHcTokens.activeBackground,
|
|
activeText: tokens?.activeText ?? defaultHcTokens.activeText,
|
|
buttonBorder: tokens?.buttonBorder ?? defaultHcTokens.buttonBorder,
|
|
buttonText: tokens?.buttonText ?? defaultHcTokens.buttonText,
|
|
canvas: tokens?.canvas ?? defaultHcTokens.canvas
|
|
};
|
|
const theme = {
|
|
...themeInput
|
|
};
|
|
const c = theme.components;
|
|
theme.components = {
|
|
...c,
|
|
MuiAccordionSummary: {
|
|
...c?.MuiAccordionSummary,
|
|
styleOverrides: {
|
|
...c?.MuiAccordionSummary?.styleOverrides,
|
|
root: [c?.MuiAccordionSummary?.styleOverrides?.root, {
|
|
[`&.${accordionSummaryClasses.disabled}`]: {
|
|
[HCM]: {
|
|
opacity: 1
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiAutocomplete: {
|
|
...c?.MuiAutocomplete,
|
|
styleOverrides: {
|
|
...c?.MuiAutocomplete?.styleOverrides,
|
|
listbox: [c?.MuiAutocomplete?.styleOverrides?.listbox, {
|
|
[`& .${autocompleteClasses.option}`]: {
|
|
'&[aria-disabled="true"]': {
|
|
[HCM]: {
|
|
color: hcTokens.disabled,
|
|
opacity: 1
|
|
}
|
|
},
|
|
[`&.${autocompleteClasses.focused}, &.${autocompleteClasses.focusVisible}`]: {
|
|
[HCM]: {
|
|
forcedColorAdjust: 'none',
|
|
color: hcTokens.activeText,
|
|
backgroundColor: hcTokens.activeBackground
|
|
}
|
|
},
|
|
'&[aria-selected="true"]': {
|
|
[HCM]: {
|
|
forcedColorAdjust: 'none',
|
|
color: hcTokens.selectedText,
|
|
backgroundColor: hcTokens.selectedBackground
|
|
},
|
|
[`&.${autocompleteClasses.focused}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.activeText,
|
|
backgroundColor: hcTokens.activeBackground
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiCheckbox: {
|
|
...c?.MuiCheckbox,
|
|
styleOverrides: {
|
|
...c?.MuiCheckbox?.styleOverrides,
|
|
root: [c?.MuiCheckbox?.styleOverrides?.root, {
|
|
[`&.${checkboxClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiFilledInput: {
|
|
...c?.MuiFilledInput,
|
|
styleOverrides: {
|
|
...c?.MuiFilledInput?.styleOverrides,
|
|
root: [c?.MuiFilledInput?.styleOverrides?.root, {
|
|
[`&.${filledInputClasses.error}`]: {
|
|
'&::before, &::after': {
|
|
[HCM]: {
|
|
borderBottomColor: hcTokens.error
|
|
}
|
|
}
|
|
},
|
|
[`&.${filledInputClasses.disabled}:before`]: {
|
|
[HCM]: {
|
|
borderBottomStyle: 'solid',
|
|
borderBottomColor: hcTokens.disabled
|
|
}
|
|
},
|
|
[`&.${filledInputClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiFormControlLabel: {
|
|
...c?.MuiFormControlLabel,
|
|
styleOverrides: {
|
|
...c?.MuiFormControlLabel?.styleOverrides,
|
|
root: [c?.MuiFormControlLabel?.styleOverrides?.root, {
|
|
[`& .${formControlLabelClasses.label}.${formControlLabelClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiFormHelperText: {
|
|
...c?.MuiFormHelperText,
|
|
styleOverrides: {
|
|
...c?.MuiFormHelperText?.styleOverrides,
|
|
root: [c?.MuiFormHelperText?.styleOverrides?.root, {
|
|
[`&.${formHelperTextClasses.error}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.error
|
|
}
|
|
},
|
|
[`&.${formHelperTextClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiFormLabel: {
|
|
...c?.MuiFormLabel,
|
|
styleOverrides: {
|
|
...c?.MuiFormLabel?.styleOverrides,
|
|
root: [c?.MuiFormLabel?.styleOverrides?.root, {
|
|
[`&.${formLabelClasses.error}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.error
|
|
}
|
|
},
|
|
[`&.${formLabelClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiInput: {
|
|
...c?.MuiInput,
|
|
styleOverrides: {
|
|
...c?.MuiInput?.styleOverrides,
|
|
root: [c?.MuiInput?.styleOverrides?.root, {
|
|
[`&.${inputClasses.error}`]: {
|
|
'&::before, &::after': {
|
|
[HCM]: {
|
|
borderBottomColor: hcTokens.error
|
|
}
|
|
}
|
|
},
|
|
[`&.${inputClasses.disabled}:before`]: {
|
|
[HCM]: {
|
|
borderBottomStyle: 'solid',
|
|
borderBottomColor: hcTokens.disabled
|
|
}
|
|
},
|
|
[`&.${inputClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiLinearProgress: {
|
|
...c?.MuiLinearProgress,
|
|
styleOverrides: {
|
|
...c?.MuiLinearProgress?.styleOverrides,
|
|
root: [c?.MuiLinearProgress?.styleOverrides?.root, {
|
|
[HCM]: {
|
|
forcedColorAdjust: 'none',
|
|
outline: `1px solid ${hcTokens.buttonBorder}`,
|
|
backgroundColor: hcTokens.canvas
|
|
}
|
|
}],
|
|
bar: [c?.MuiLinearProgress?.styleOverrides?.bar, {
|
|
[HCM]: {
|
|
backgroundColor: hcTokens.buttonText
|
|
}
|
|
}],
|
|
bar2: [c?.MuiLinearProgress?.styleOverrides?.bar2, {
|
|
variants: [{
|
|
props: {
|
|
variant: 'buffer'
|
|
},
|
|
style: {
|
|
[HCM]: {
|
|
backgroundColor: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}]
|
|
}
|
|
},
|
|
MuiInputBase: {
|
|
...c?.MuiInputBase,
|
|
styleOverrides: {
|
|
...c?.MuiInputBase?.styleOverrides,
|
|
input: [c?.MuiInputBase?.styleOverrides?.input, {
|
|
[HCM]: {
|
|
'&::placeholder': {
|
|
opacity: 1
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiMenuItem: {
|
|
...c?.MuiMenuItem,
|
|
styleOverrides: {
|
|
...c?.MuiMenuItem?.styleOverrides,
|
|
root: [c?.MuiMenuItem?.styleOverrides?.root, {
|
|
[`&.${menuItemClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled,
|
|
opacity: 1
|
|
}
|
|
},
|
|
[`&.${menuItemClasses.focusVisible}, &:hover`]: {
|
|
[HCM]: {
|
|
forcedColorAdjust: 'none',
|
|
color: hcTokens.activeText,
|
|
backgroundColor: hcTokens.activeBackground,
|
|
outline: 'none'
|
|
}
|
|
},
|
|
[`&.${menuItemClasses.selected}`]: {
|
|
[HCM]: {
|
|
forcedColorAdjust: 'none',
|
|
color: hcTokens.selectedText,
|
|
backgroundColor: hcTokens.selectedBackground
|
|
}
|
|
},
|
|
[`&.${menuItemClasses.selected}.${menuItemClasses.focusVisible}, &.${menuItemClasses.selected}:hover`]: {
|
|
[HCM]: {
|
|
color: hcTokens.activeText,
|
|
backgroundColor: hcTokens.activeBackground
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiListItemIcon: {
|
|
...c?.MuiListItemIcon,
|
|
styleOverrides: {
|
|
...c?.MuiListItemIcon?.styleOverrides,
|
|
root: [c?.MuiListItemIcon?.styleOverrides?.root, {
|
|
[HCM]: {
|
|
color: 'inherit'
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiListItemButton: {
|
|
...c?.MuiListItemButton,
|
|
styleOverrides: {
|
|
...c?.MuiListItemButton?.styleOverrides,
|
|
root: [c?.MuiListItemButton?.styleOverrides?.root, {
|
|
[`&.${listItemButtonClasses.focusVisible}, &:hover`]: {
|
|
[HCM]: {
|
|
forcedColorAdjust: 'none',
|
|
color: hcTokens.activeText,
|
|
backgroundColor: hcTokens.activeBackground
|
|
}
|
|
},
|
|
[`&.${listItemButtonClasses.selected}`]: {
|
|
[HCM]: {
|
|
forcedColorAdjust: 'none',
|
|
color: hcTokens.selectedText,
|
|
backgroundColor: hcTokens.selectedBackground
|
|
}
|
|
},
|
|
[`&.${listItemButtonClasses.selected}.${listItemButtonClasses.focusVisible}, &.${listItemButtonClasses.selected}:hover`]: {
|
|
[HCM]: {
|
|
color: hcTokens.activeText,
|
|
backgroundColor: hcTokens.activeBackground
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiNativeSelect: {
|
|
...c?.MuiNativeSelect,
|
|
styleOverrides: {
|
|
...c?.MuiNativeSelect?.styleOverrides,
|
|
icon: [c?.MuiNativeSelect?.styleOverrides?.icon, {
|
|
[`&.${nativeSelectClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiOutlinedInput: {
|
|
...c?.MuiOutlinedInput,
|
|
styleOverrides: {
|
|
...c?.MuiOutlinedInput?.styleOverrides,
|
|
root: [c?.MuiOutlinedInput?.styleOverrides?.root, {
|
|
[`&.${outlinedInputClasses.error} .${outlinedInputClasses.notchedOutline}`]: {
|
|
[HCM]: {
|
|
borderColor: hcTokens.error
|
|
}
|
|
},
|
|
[`&.${outlinedInputClasses.disabled} .${outlinedInputClasses.notchedOutline}`]: {
|
|
[HCM]: {
|
|
borderColor: hcTokens.disabled
|
|
}
|
|
},
|
|
[`&.${outlinedInputClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiRadio: {
|
|
...c?.MuiRadio,
|
|
styleOverrides: {
|
|
...c?.MuiRadio?.styleOverrides,
|
|
root: [c?.MuiRadio?.styleOverrides?.root, {
|
|
[`&.${radioClasses.disabled}`]: {
|
|
[HCM]: {
|
|
color: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiSlider: {
|
|
...c?.MuiSlider,
|
|
styleOverrides: {
|
|
...c?.MuiSlider?.styleOverrides,
|
|
// track doesn't receive the disabled class — use ownerState
|
|
track: [c?.MuiSlider?.styleOverrides?.track, ({
|
|
ownerState
|
|
}) => ({
|
|
...(ownerState.disabled && {
|
|
[HCM]: {
|
|
borderColor: hcTokens.disabled
|
|
}
|
|
})
|
|
})],
|
|
// thumb receives the disabled class directly
|
|
thumb: [c?.MuiSlider?.styleOverrides?.thumb, {
|
|
[`&.${sliderClasses.disabled}`]: {
|
|
[HCM]: {
|
|
borderColor: hcTokens.disabled
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiSwitch: {
|
|
...c?.MuiSwitch,
|
|
styleOverrides: {
|
|
...c?.MuiSwitch?.styleOverrides,
|
|
// track and thumb don't receive the disabled class — use ownerState
|
|
track: [c?.MuiSwitch?.styleOverrides?.track, ({
|
|
ownerState
|
|
}) => ({
|
|
...(ownerState.disabled && {
|
|
[HCM]: {
|
|
borderColor: hcTokens.disabled
|
|
}
|
|
})
|
|
})],
|
|
thumb: [c?.MuiSwitch?.styleOverrides?.thumb, ({
|
|
ownerState
|
|
}) => ({
|
|
...(ownerState.disabled && {
|
|
[HCM]: {
|
|
borderColor: hcTokens.disabled
|
|
}
|
|
})
|
|
})]
|
|
}
|
|
},
|
|
MuiButtonBase: {
|
|
...c?.MuiButtonBase,
|
|
styleOverrides: {
|
|
...c?.MuiButtonBase?.styleOverrides,
|
|
root: [c?.MuiButtonBase?.styleOverrides?.root, {
|
|
// Restore the focus outline in HCM since the ripple is not visible.
|
|
// Also handle components where the focusable element is a hidden inner input (Checkbox, Radio, Switch).
|
|
[HCM]: {
|
|
'&:focus-visible, &:focus-within:has(input:focus-visible)': {
|
|
outline: `5px auto ${hcTokens.activeBackground}`
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiTooltip: {
|
|
...c?.MuiTooltip,
|
|
styleOverrides: {
|
|
...c?.MuiTooltip?.styleOverrides,
|
|
tooltip: [c?.MuiTooltip?.styleOverrides?.tooltip, {
|
|
[HCM]: {
|
|
border: `1px solid ${hcTokens.buttonText}`
|
|
}
|
|
}]
|
|
}
|
|
},
|
|
MuiToggleButton: {
|
|
...c?.MuiToggleButton,
|
|
styleOverrides: {
|
|
...c?.MuiToggleButton?.styleOverrides,
|
|
root: [c?.MuiToggleButton?.styleOverrides?.root, {
|
|
[`&.${toggleButtonClasses.selected}`]: {
|
|
[HCM]: {
|
|
forcedColorAdjust: 'none',
|
|
color: hcTokens.activeText,
|
|
backgroundColor: hcTokens.activeBackground,
|
|
borderColor: hcTokens.activeBackground
|
|
},
|
|
'&:hover': {
|
|
[HCM]: {
|
|
backgroundColor: hcTokens.activeBackground,
|
|
borderColor: hcTokens.buttonBorder
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
};
|
|
return theme;
|
|
} |