diff --git a/core/scripts/testing/scripts.js b/core/scripts/testing/scripts.js index 1fb67341b9d..9e3a15e318f 100644 --- a/core/scripts/testing/scripts.js +++ b/core/scripts/testing/scripts.js @@ -21,6 +21,7 @@ */ const DEFAULT_THEME = 'md'; +const DEFAULT_PALETTE = 'light'; (function() { @@ -87,18 +88,24 @@ const DEFAULT_THEME = 'md'; * Values can be `light`, `dark`, `high-contrast`, * or `high-contrast-dark`. Default to `light` for tests. */ - const validPalettes = ['light', 'dark', 'high-contrast', 'high-contrast-dark']; + const validPalettes = [DEFAULT_PALETTE, 'dark', 'high-contrast', 'high-contrast-dark']; + + const configDarkMode = window.Ionic?.config?.customTheme?.palette?.dark?.enabled === 'always' ? 'dark' : null; + const configHighContrastMode = window.Ionic?.config?.customTheme?.palette?.highContrast?.enabled === 'always' ? 'high-contrast' : null; + const configHighContrastDarkMode = window.Ionic?.config?.customTheme?.palette?.highContrastDark?.enabled === 'always' ? 'high-contrast-dark' : null; + const configPalette = configDarkMode || configHighContrastMode || configHighContrastDarkMode; const paletteQuery = window.location.search.match(/palette=([a-z-]+)/); const paletteHash = window.location.hash.match(/palette=([a-z-]+)/); const darkClass = document.body?.classList.contains('ion-palette-dark') ? 'dark' : null; const highContrastClass = document.body?.classList.contains('ion-palette-high-contrast') ? 'high-contrast' : null; const highContrastDarkClass = darkClass && highContrastClass ? 'high-contrast-dark' : null; + const paletteClass = highContrastDarkClass || highContrastClass || darkClass; - let paletteName = paletteQuery?.[1] || paletteHash?.[1] || highContrastDarkClass || darkClass || highContrastClass || 'light'; + let paletteName = configPalette || paletteQuery?.[1] || paletteHash?.[1] || paletteClass || DEFAULT_PALETTE; if (!validPalettes.includes(paletteName)) { console.warn(`Invalid palette name: '${paletteName}'. Falling back to 'light' palette.`); - paletteName = 'light'; + paletteName = DEFAULT_PALETTE; } // Load theme tokens if the theme is valid @@ -119,8 +126,15 @@ const DEFAULT_THEME = 'md'; // If a specific palette is requested, modify the palette structure // to set the enabled property to 'always' + // TODO(FW-4004): Implement dark mode if (paletteName === 'dark' && theme.palette?.dark) { theme.palette.dark.enabled = 'always'; + // TODO(FW-4005): Implement high contrast mode + } else if (paletteName === 'high-contrast' && theme.palette?.highContrast) { + theme.palette.highContrast.enabled = 'always'; + // TODO(FW-4005): Implement high contrast dark mode + } else if (paletteName === 'high-contrast-dark' && theme.palette?.highContrastDark) { + theme.palette.highContrastDark.enabled = 'always'; } // Apply the theme tokens to Ionic config diff --git a/core/src/components/modal/test/dark-mode/index.html b/core/src/components/modal/test/dark-mode/index.html index a104ad9ffbb..d6c2433c98f 100644 --- a/core/src/components/modal/test/dark-mode/index.html +++ b/core/src/components/modal/test/dark-mode/index.html @@ -10,6 +10,21 @@ + diff --git a/core/src/components/toast/test/a11y/toast.e2e.ts b/core/src/components/toast/test/a11y/toast.e2e.ts index bf1fbfd72c7..23cdd9501e2 100644 --- a/core/src/components/toast/test/a11y/toast.e2e.ts +++ b/core/src/components/toast/test/a11y/toast.e2e.ts @@ -236,7 +236,8 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { */ configs({ directions: ['ltr'], palettes: ['high-contrast-dark', 'high-contrast'] }).forEach( ({ title, config, screenshot }) => { - test.describe(title('toast: high contrast: buttons'), () => { + // TODO(FW-4005): Once high contrast themes are fully implemented in ionic modular, remove the skips from these tests + test.describe.skip(title('toast: high contrast: buttons'), () => { test.beforeEach(async ({ page }) => { await page.setContent( ` diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Chrome-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Chrome-linux.png index 4ad457bb0a3..bb69dce333a 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Chrome-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Firefox-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Firefox-linux.png index 9c106966636..f7b4765b747 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Firefox-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Safari-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Safari-linux.png index cda44e8be65..699111abbfe 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Safari-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-ltr-dark-Mobile-Safari-linux.png differ diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Chrome-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Chrome-linux.png index 6eb3e0d2b5c..1462fcde043 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Chrome-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Firefox-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Firefox-linux.png index 6494689ed27..f8838bab39b 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Firefox-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Safari-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Safari-linux.png index 28978fa112c..897bd3c991b 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Safari-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-buttons-inside-content-ionic-md-rtl-dark-Mobile-Safari-linux.png differ diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-ltr-dark-Mobile-Chrome-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-ltr-dark-Mobile-Chrome-linux.png index a1253d9355a..87aca6fe10f 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-ltr-dark-Mobile-Chrome-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-ltr-dark-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-ltr-dark-Mobile-Firefox-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-ltr-dark-Mobile-Firefox-linux.png index 303b43b5954..bb0b2b3f243 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-ltr-dark-Mobile-Firefox-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-ltr-dark-Mobile-Firefox-linux.png differ diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-rtl-dark-Mobile-Chrome-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-rtl-dark-Mobile-Chrome-linux.png index baee190f0c4..13e0b551dcd 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-rtl-dark-Mobile-Chrome-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-rtl-dark-Mobile-Chrome-linux.png differ diff --git a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-rtl-dark-Mobile-Firefox-linux.png b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-rtl-dark-Mobile-Firefox-linux.png index c01630f5f68..9bbaa0bfb91 100644 Binary files a/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-rtl-dark-Mobile-Firefox-linux.png and b/core/src/components/toolbar/test/basic/toolbar.e2e.ts-snapshots/toolbar-basic-text-icon-buttons-ionic-md-rtl-dark-Mobile-Firefox-linux.png differ diff --git a/core/src/themes/base/default.tokens.ts b/core/src/themes/base/default.tokens.ts index 6df6efb9e6f..26906072d12 100644 --- a/core/src/themes/base/default.tokens.ts +++ b/core/src/themes/base/default.tokens.ts @@ -1,6 +1,8 @@ import type { DefaultTheme } from '../themes.interfaces'; import { darkTheme } from './dark.tokens'; +import { highContrastDarkTheme } from './high-contrast-dark.tokens'; +import { highContrastTheme } from './high-contrast.tokens'; import { lightTheme } from './light.tokens'; export const defaultTheme: DefaultTheme = { @@ -9,6 +11,8 @@ export const defaultTheme: DefaultTheme = { palette: { light: lightTheme, dark: darkTheme, + highContrast: highContrastTheme, + highContrastDark: highContrastDarkTheme, }, config: { diff --git a/core/src/themes/base/high-contrast-dark.tokens.ts b/core/src/themes/base/high-contrast-dark.tokens.ts new file mode 100644 index 00000000000..168866a5f8c --- /dev/null +++ b/core/src/themes/base/high-contrast-dark.tokens.ts @@ -0,0 +1,213 @@ +import { mix } from '../../utils/theme'; +import type { HighContrastDarkTheme } from '../themes.interfaces'; + +const colors = { + primary: '#7cabff', + secondary: '#62bdff', + tertiary: '#b6b9f9', + success: '#4ada71', + warning: '#ffce31', + danger: '#fc9aa2', + light: '#222428', + medium: '#a8aab3', + dark: '#f4f5f8', +}; + +export const highContrastDarkTheme: HighContrastDarkTheme = { + enabled: 'never', + color: { + primary: { + bold: { + base: colors.primary, + contrast: '#000', + foreground: colors.primary, + shade: mix(colors.primary, '#000', '12%'), + tint: mix(colors.primary, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.primary, '8%'), + contrast: colors.primary, + foreground: mix(colors.primary, '#000', '12%'), + shade: mix('#fff', colors.primary, '12%'), + tint: mix('#fff', colors.primary, '4%'), + }, + }, + secondary: { + bold: { + base: colors.secondary, + contrast: '#000', + foreground: colors.secondary, + shade: mix(colors.secondary, '#000', '12%'), + tint: mix(colors.secondary, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.secondary, '8%'), + contrast: colors.secondary, + foreground: mix(colors.secondary, '#000', '12%'), + shade: mix('#fff', colors.secondary, '12%'), + tint: mix('#fff', colors.secondary, '4%'), + }, + }, + tertiary: { + bold: { + base: colors.tertiary, + contrast: '#000', + foreground: colors.tertiary, + shade: mix(colors.tertiary, '#000', '12%'), + tint: mix(colors.tertiary, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.tertiary, '8%'), + contrast: colors.tertiary, + foreground: mix(colors.tertiary, '#000', '12%'), + shade: mix('#fff', colors.tertiary, '12%'), + tint: mix('#fff', colors.tertiary, '4%'), + }, + }, + success: { + bold: { + base: colors.success, + contrast: '#000', + foreground: colors.success, + shade: mix(colors.success, '#000', '12%'), + tint: mix(colors.success, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.success, '8%'), + contrast: colors.success, + foreground: mix(colors.success, '#000', '12%'), + shade: mix('#fff', colors.success, '12%'), + tint: mix('#fff', colors.success, '4%'), + }, + }, + warning: { + bold: { + base: colors.warning, + contrast: '#000', + foreground: colors.warning, + shade: mix(colors.warning, '#000', '12%'), + tint: mix(colors.warning, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.warning, '8%'), + contrast: colors.warning, + foreground: mix(colors.warning, '#000', '12%'), + shade: mix('#fff', colors.warning, '12%'), + tint: mix('#fff', colors.warning, '4%'), + }, + }, + danger: { + bold: { + base: colors.danger, + contrast: '#000', + foreground: colors.danger, + shade: mix(colors.danger, '#000', '12%'), + tint: mix(colors.danger, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.danger, '8%'), + contrast: colors.danger, + foreground: mix(colors.danger, '#000', '12%'), + shade: mix('#fff', colors.danger, '12%'), + tint: mix('#fff', colors.danger, '4%'), + }, + }, + light: { + bold: { + base: colors.light, + contrast: '#fff', + foreground: colors.light, + shade: mix(colors.light, '#000', '12%'), + tint: mix(colors.light, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.light, '8%'), + contrast: colors.light, + foreground: mix(colors.light, '#000', '12%'), + shade: mix('#fff', colors.light, '12%'), + tint: mix('#fff', colors.light, '4%'), + }, + }, + medium: { + bold: { + base: colors.medium, + contrast: '#000', + foreground: colors.medium, + shade: mix(colors.medium, '#000', '12%'), + tint: mix(colors.medium, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.medium, '8%'), + contrast: colors.medium, + foreground: mix(colors.medium, '#000', '12%'), + shade: mix('#fff', colors.medium, '12%'), + tint: mix('#fff', colors.medium, '4%'), + }, + }, + dark: { + bold: { + base: colors.dark, + contrast: '#000', + foreground: colors.dark, + shade: mix(colors.dark, '#000', '12%'), + tint: mix(colors.dark, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.dark, '8%'), + contrast: colors.dark, + foreground: mix(colors.dark, '#000', '12%'), + shade: mix('#fff', colors.dark, '12%'), + tint: mix('#fff', colors.dark, '4%'), + }, + }, + }, + + backgroundColor: '#000000', + backgroundColorRgb: '0, 0, 0', + textColor: '#ffffff', + textColorRgb: '255, 255, 255', + + backgroundColorStep: { + 50: '#0d0d0d', + 100: '#1a1a1a', + 150: '#262626', + 200: '#333333', + 250: '#404040', + 300: '#4d4d4d', + 350: '#595959', + 400: '#666666', + 450: '#737373', + 500: '#808080', + 550: '#8c8c8c', + 600: '#999999', + 650: '#a6a6a6', + 700: '#b3b3b3', + 750: '#bfbfbf', + 800: '#cccccc', + 850: '#d9d9d9', + 900: '#e6e6e6', + 950: '#f2f2f2', + }, + + textColorStep: { + 50: '#f9f9f9', + 100: '#f3f3f3', + 150: '#ededed', + 200: '#e7e7e7', + 250: '#e1e1e1', + 300: '#dbdbdb', + 350: '#d5d5d5', + 400: '#cfcfcf', + 450: '#c9c9c9', + 500: '#c4c4c4', + 550: '#bebebe', + 600: '#b8b8b8', + 650: '#b2b2b2', + 700: '#acacac', + 750: '#a6a6a6', + 800: '#a0a0a0', + 850: '#9a9a9a', + 900: '#949494', + 950: '#8e8e8e', + }, +}; diff --git a/core/src/themes/base/high-contrast.tokens.ts b/core/src/themes/base/high-contrast.tokens.ts new file mode 100644 index 00000000000..f56daef372d --- /dev/null +++ b/core/src/themes/base/high-contrast.tokens.ts @@ -0,0 +1,213 @@ +import { mix } from '../../utils/theme'; +import type { HighContrastTheme } from '../themes.interfaces'; + +const colors = { + primary: '#003fae', + secondary: '#01487b', + tertiary: '#3400e6', + success: '#004314', + warning: '#5f4100', + danger: '#9c000c', + light: '#f4f5f8', + medium: '#444446', + dark: '#222428', +}; + +export const highContrastTheme: HighContrastTheme = { + enabled: 'never', + color: { + primary: { + bold: { + base: colors.primary, + contrast: '#fff', + foreground: colors.primary, + shade: mix(colors.primary, '#000', '12%'), + tint: mix(colors.primary, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.primary, '8%'), + contrast: colors.primary, + foreground: mix(colors.primary, '#000', '12%'), + shade: mix('#fff', colors.primary, '12%'), + tint: mix('#fff', colors.primary, '4%'), + }, + }, + secondary: { + bold: { + base: colors.secondary, + contrast: '#fff', + foreground: colors.secondary, + shade: mix(colors.secondary, '#000', '12%'), + tint: mix(colors.secondary, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.secondary, '8%'), + contrast: colors.secondary, + foreground: mix(colors.secondary, '#000', '12%'), + shade: mix('#fff', colors.secondary, '12%'), + tint: mix('#fff', colors.secondary, '4%'), + }, + }, + tertiary: { + bold: { + base: colors.tertiary, + contrast: '#fff', + foreground: colors.tertiary, + shade: mix(colors.tertiary, '#000', '12%'), + tint: mix(colors.tertiary, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.tertiary, '8%'), + contrast: colors.tertiary, + foreground: mix(colors.tertiary, '#000', '12%'), + shade: mix('#fff', colors.tertiary, '12%'), + tint: mix('#fff', colors.tertiary, '4%'), + }, + }, + success: { + bold: { + base: colors.success, + contrast: '#fff', + foreground: colors.success, + shade: mix(colors.success, '#000', '12%'), + tint: mix(colors.success, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.success, '8%'), + contrast: colors.success, + foreground: mix(colors.success, '#000', '12%'), + shade: mix('#fff', colors.success, '12%'), + tint: mix('#fff', colors.success, '4%'), + }, + }, + warning: { + bold: { + base: colors.warning, + contrast: '#fff', + foreground: colors.warning, + shade: mix(colors.warning, '#000', '12%'), + tint: mix(colors.warning, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.warning, '8%'), + contrast: colors.warning, + foreground: mix(colors.warning, '#000', '12%'), + shade: mix('#fff', colors.warning, '12%'), + tint: mix('#fff', colors.warning, '4%'), + }, + }, + danger: { + bold: { + base: colors.danger, + contrast: '#fff', + foreground: colors.danger, + shade: mix(colors.danger, '#000', '12%'), + tint: mix(colors.danger, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.danger, '8%'), + contrast: colors.danger, + foreground: mix(colors.danger, '#000', '12%'), + shade: mix('#fff', colors.danger, '12%'), + tint: mix('#fff', colors.danger, '4%'), + }, + }, + light: { + bold: { + base: colors.light, + contrast: '#000', + foreground: colors.light, + shade: mix(colors.light, '#000', '12%'), + tint: mix(colors.light, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.light, '8%'), + contrast: colors.light, + foreground: mix(colors.light, '#000', '12%'), + shade: mix('#fff', colors.light, '12%'), + tint: mix('#fff', colors.light, '4%'), + }, + }, + medium: { + bold: { + base: colors.medium, + contrast: '#fff', + foreground: colors.medium, + shade: mix(colors.medium, '#000', '12%'), + tint: mix(colors.medium, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.medium, '8%'), + contrast: colors.medium, + foreground: mix(colors.medium, '#000', '12%'), + shade: mix('#fff', colors.medium, '12%'), + tint: mix('#fff', colors.medium, '4%'), + }, + }, + dark: { + bold: { + base: colors.dark, + contrast: '#fff', + foreground: colors.dark, + shade: mix(colors.dark, '#000', '12%'), + tint: mix(colors.dark, '#fff', '10%'), + }, + subtle: { + base: mix('#fff', colors.dark, '8%'), + contrast: colors.dark, + foreground: mix(colors.dark, '#000', '12%'), + shade: mix('#fff', colors.dark, '12%'), + tint: mix('#fff', colors.dark, '4%'), + }, + }, + }, + + backgroundColor: '#ffffff', + backgroundColorRgb: '255, 255, 255', + textColor: '#000000', + textColorRgb: '0, 0, 0', + + backgroundColorStep: { + 50: '#818181', + 100: '#7a7a7a', + 150: '#747474', + 200: '#6d6d6d', + 250: '#666666', + 300: '#5f5f5f', + 350: '#585858', + 400: '#525252', + 450: '#4b4b4b', + 500: '#444444', + 550: '#3d3d3d', + 600: '#363636', + 650: '#303030', + 700: '#292929', + 750: '#222222', + 800: '#1b1b1b', + 850: '#141414', + 900: '#0e0e0e', + 950: '#070707', + }, + + textColorStep: { + 50: '#070707', + 100: '#0e0e0e', + 150: '#141414', + 200: '#1b1b1b', + 250: '#222222', + 300: '#292929', + 350: '#303030', + 400: '#363636', + 450: '#3d3d3d', + 500: '#444444', + 550: '#4b4b4b', + 600: '#525252', + 650: '#585858', + 700: '#5f5f5f', + 750: '#666666', + 800: '#6d6d6d', + 850: '#747474', + 900: '#7a7a7a', + 950: '#818181', + }, +}; diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index 266191a1cb8..236039af13f 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -2,6 +2,8 @@ import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import type { DefaultTheme } from '../themes.interfaces'; import { darkTheme } from './dark.tokens'; +import { highContrastDarkTheme } from './high-contrast-dark.tokens'; +import { highContrastTheme } from './high-contrast.tokens'; import { lightTheme } from './light.tokens'; export const defaultTheme: DefaultTheme = { @@ -12,6 +14,8 @@ export const defaultTheme: DefaultTheme = { palette: { light: lightTheme, dark: darkTheme, + highContrast: highContrastTheme, + highContrastDark: highContrastDarkTheme, }, fontFamily: '-apple-system, BlinkMacSystemFont, "Helvetica Neue", "Roboto", sans-serif', diff --git a/core/src/themes/ios/high-contrast-dark.tokens.ts b/core/src/themes/ios/high-contrast-dark.tokens.ts new file mode 100644 index 00000000000..eaf97dcb791 --- /dev/null +++ b/core/src/themes/ios/high-contrast-dark.tokens.ts @@ -0,0 +1,53 @@ +import { highContrastDarkTheme as baseHighContrastDarkTheme } from '../base/high-contrast-dark.tokens'; +import type { HighContrastDarkTheme } from '../themes.interfaces'; + +export const highContrastDarkTheme: HighContrastDarkTheme = { + ...baseHighContrastDarkTheme, + + backgroundColor: '#000000', + textColor: '#ffffff', + + backgroundColorStep: { + 50: '#0d0d0d', + 100: '#1a1a1a', + 150: '#262626', + 200: '#333333', + 250: '#404040', + 300: '#4d4d4d', + 350: '#595959', + 400: '#666666', + 450: '#737373', + 500: '#808080', + 550: '#8c8c8c', + 600: '#999999', + 650: '#a6a6a6', + 700: '#b3b3b3', + 750: '#bfbfbf', + 800: '#cccccc', + 850: '#d9d9d9', + 900: '#e6e6e6', + 950: '#f2f2f2', + }, + + textColorStep: { + 50: '#f9f9f9', + 100: '#f3f3f3', + 150: '#ededed', + 200: '#e7e7e7', + 250: '#e1e1e1', + 300: '#dbdbdb', + 350: '#d5d5d5', + 400: '#cfcfcf', + 450: '#c9c9c9', + 500: '#c4c4c4', + 550: '#bebebe', + 600: '#b8b8b8', + 650: '#b2b2b2', + 700: '#acacac', + 750: '#a6a6a6', + 800: '#a0a0a0', + 850: '#9a9a9a', + 900: '#949494', + 950: '#8e8e8e', + }, +}; diff --git a/core/src/themes/ios/high-contrast.tokens.ts b/core/src/themes/ios/high-contrast.tokens.ts new file mode 100644 index 00000000000..96e90d79b83 --- /dev/null +++ b/core/src/themes/ios/high-contrast.tokens.ts @@ -0,0 +1,53 @@ +import { highContrastTheme as baseHighContrastTheme } from '../base/high-contrast.tokens'; +import type { HighContrastTheme } from '../themes.interfaces'; + +export const highContrastTheme: HighContrastTheme = { + ...baseHighContrastTheme, + + backgroundColor: '#ffffff', + textColor: '#000000', + + backgroundColorStep: { + 50: '#818181', + 100: '#7a7a7a', + 150: '#747474', + 200: '#6d6d6d', + 250: '#666666', + 300: '#5f5f5f', + 350: '#585858', + 400: '#525252', + 450: '#4b4b4b', + 500: '#444444', + 550: '#3d3d3d', + 600: '#363636', + 650: '#303030', + 700: '#292929', + 750: '#222222', + 800: '#1b1b1b', + 850: '#141414', + 900: '#0e0e0e', + 950: '#070707', + }, + + textColorStep: { + 50: '#070707', + 100: '#0e0e0e', + 150: '#141414', + 200: '#1b1b1b', + 250: '#222222', + 300: '#292929', + 350: '#303030', + 400: '#363636', + 450: '#3d3d3d', + 500: '#444444', + 550: '#4b4b4b', + 600: '#525252', + 650: '#585858', + 700: '#5f5f5f', + 750: '#666666', + 800: '#6d6d6d', + 850: '#747474', + 900: '#7a7a7a', + 950: '#818181', + }, +}; diff --git a/core/src/themes/md/dark.tokens.ts b/core/src/themes/md/dark.tokens.ts index 80b9d9116e1..ef863643d5c 100644 --- a/core/src/themes/md/dark.tokens.ts +++ b/core/src/themes/md/dark.tokens.ts @@ -9,6 +9,11 @@ export const darkTheme: DarkTheme = { textColor: '#ffffff', textColorRgb: '255, 255, 255', + // TODO(FW-6864): Remove once IonToolbar themes are added + toolbar: { + background: '#1f1f1f', + }, + backgroundColorStep: { 50: '#1e1e1e', 100: '#2a2a2a', diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index 7dcff7c0445..15f7570e05a 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -2,6 +2,8 @@ import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import type { DefaultTheme } from '../themes.interfaces'; import { darkTheme } from './dark.tokens'; +import { highContrastDarkTheme } from './high-contrast-dark.tokens'; +import { highContrastTheme } from './high-contrast.tokens'; import { lightTheme } from './light.tokens'; export const defaultTheme: DefaultTheme = { @@ -12,6 +14,8 @@ export const defaultTheme: DefaultTheme = { palette: { light: lightTheme, dark: darkTheme, + highContrast: highContrastTheme, + highContrastDark: highContrastDarkTheme, }, config: { diff --git a/core/src/themes/md/high-contrast-dark.tokens.ts b/core/src/themes/md/high-contrast-dark.tokens.ts new file mode 100644 index 00000000000..a1e2cbb2d51 --- /dev/null +++ b/core/src/themes/md/high-contrast-dark.tokens.ts @@ -0,0 +1,58 @@ +import { highContrastDarkTheme as baseHighContrastDarkTheme } from '../base/high-contrast-dark.tokens'; +import type { HighContrastDarkTheme } from '../themes.interfaces'; + +export const highContrastDarkTheme: HighContrastDarkTheme = { + ...baseHighContrastDarkTheme, + + backgroundColor: '#121212', + textColor: '#000000', + + // TODO(FW-6864): Remove once IonToolbar themes are added + toolbar: { + background: '#1f1f1f', + }, + + backgroundColorStep: { + 50: '#1e1e1e', + 100: '#2a2a2a', + 150: '#363636', + 200: '#414141', + 250: '#4d4d4d', + 300: '#595959', + 350: '#656565', + 400: '#717171', + 450: '#7d7d7d', + 500: '#898989', + 550: '#949494', + 600: '#a0a0a0', + 650: '#acacac', + 700: '#b8b8b8', + 750: '#c4c4c4', + 800: '#d0d0d0', + 850: '#dbdbdb', + 900: '#e7e7e7', + 950: '#f3f3f3', + }, + + textColorStep: { + 50: '#f9f9f9', + 100: '#f3f3f3', + 150: '#ededed', + 200: '#e7e7e7', + 250: '#e1e1e1', + 300: '#dbdbdb', + 350: '#d5d5d5', + 400: '#cfcfcf', + 450: '#c9c9c9', + 500: '#c4c4c4', + 550: '#bebebe', + 600: '#b8b8b8', + 650: '#b2b2b2', + 700: '#acacac', + 750: '#a6a6a6', + 800: '#a0a0a0', + 850: '#9a9a9a', + 900: '#949494', + 950: '#8e8e8e', + }, +}; diff --git a/core/src/themes/md/high-contrast.tokens.ts b/core/src/themes/md/high-contrast.tokens.ts new file mode 100644 index 00000000000..96e90d79b83 --- /dev/null +++ b/core/src/themes/md/high-contrast.tokens.ts @@ -0,0 +1,53 @@ +import { highContrastTheme as baseHighContrastTheme } from '../base/high-contrast.tokens'; +import type { HighContrastTheme } from '../themes.interfaces'; + +export const highContrastTheme: HighContrastTheme = { + ...baseHighContrastTheme, + + backgroundColor: '#ffffff', + textColor: '#000000', + + backgroundColorStep: { + 50: '#818181', + 100: '#7a7a7a', + 150: '#747474', + 200: '#6d6d6d', + 250: '#666666', + 300: '#5f5f5f', + 350: '#585858', + 400: '#525252', + 450: '#4b4b4b', + 500: '#444444', + 550: '#3d3d3d', + 600: '#363636', + 650: '#303030', + 700: '#292929', + 750: '#222222', + 800: '#1b1b1b', + 850: '#141414', + 900: '#0e0e0e', + 950: '#070707', + }, + + textColorStep: { + 50: '#070707', + 100: '#0e0e0e', + 150: '#141414', + 200: '#1b1b1b', + 250: '#222222', + 300: '#292929', + 350: '#303030', + 400: '#363636', + 450: '#3d3d3d', + 500: '#444444', + 550: '#4b4b4b', + 600: '#525252', + 650: '#585858', + 700: '#5f5f5f', + 750: '#666666', + 800: '#6d6d6d', + 850: '#747474', + 900: '#7a7a7a', + 950: '#818181', + }, +}; diff --git a/core/src/themes/native/test/css-variables/index.html b/core/src/themes/native/test/css-variables/index.html index 169370d12f2..b7c2b9990de 100644 --- a/core/src/themes/native/test/css-variables/index.html +++ b/core/src/themes/native/test/css-variables/index.html @@ -974,7 +974,7 @@

Battle Royale Open World Pac-Man

function togglePalette(palette) { // The path to the directory containing the // custom palette files for this test - var paletteFilesDir = '/src/themes/test/css-variables/css'; + var paletteFilesDir = '/src/themes/native/test/css-variables/css'; let modifier = ''; // The default and dark palettes are official Ionic diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index 6e5e8157e29..6f9f62beb97 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -17,6 +17,9 @@ export type BaseTheme = { [key: string]: string; }; + // TODO(FW-6864): Remove once IonToolbar themes are added + toolbar?: any; + // SPACE TOKENS spacing?: { 0?: string; @@ -247,6 +250,16 @@ export type DarkTheme = BaseTheme & { enabled: 'system' | 'always' | 'never' | 'class'; }; +// High Contrast theme interface +export type HighContrastTheme = BaseTheme & { + enabled: 'system' | 'always' | 'never' | 'class'; +}; + +// High Contrast Dark theme interface +export type HighContrastDarkTheme = BaseTheme & { + enabled: 'system' | 'always' | 'never' | 'class'; +}; + // Light theme interface export type LightTheme = BaseTheme; @@ -257,6 +270,8 @@ export type DefaultTheme = BaseTheme & { palette?: { light?: LightTheme; dark?: DarkTheme; + highContrast?: HighContrastTheme; + highContrastDark?: HighContrastDarkTheme; }; config?: IonicConfig; diff --git a/core/src/utils/test/playwright/page/utils/set-content.ts b/core/src/utils/test/playwright/page/utils/set-content.ts index 7ee403d2e4e..185bbaca5da 100644 --- a/core/src/utils/test/playwright/page/utils/set-content.ts +++ b/core/src/utils/test/playwright/page/utils/set-content.ts @@ -68,6 +68,34 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o `; } + /** + * This object is CRITICAL for Playwright stability. + * + * WHY IT'S NEEDED: + * 1. Bypasses Dynamic Loading: It avoids the consistent import + * failure 'await import(...)' when the global theme needed to be + * re-applied after the initial Ionic framework load. + * 2. Prevents Incorrect Palettes: It directly initializes with the + * required 'enabled: "always"' palette before any scripts run. This guarantees that correct CSS variables are loaded from the start. + * Otherwise, it would load the default light palette. + * + * These issues were only happening in Playwright Firefox tests + * that use `setContent`. + */ + const customTheme = { + palette: { + dark: { + enabled: palette === 'dark' ? 'always' : 'never', + }, + highContrast: { + enabled: palette === 'high-contrast' ? 'always' : 'never', + }, + highContrastDark: { + enabled: palette === 'high-contrast-dark' ? 'always' : 'never', + }, + }, + }; + const output = ` @@ -77,14 +105,14 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o ${ionicCSSImports} - ${palette !== 'light' ? `` : ''} ${ionicJSImports} diff --git a/core/src/utils/test/theme.spec.ts b/core/src/utils/test/theme.spec.ts index 9c68d95510d..c4c76774980 100644 --- a/core/src/utils/test/theme.spec.ts +++ b/core/src/utils/test/theme.spec.ts @@ -205,6 +205,12 @@ describe('generateCSSVars', () => { dark: { enabled: 'system', }, + highContrast: { + enabled: 'never', + }, + highContrastDark: { + enabled: 'never', + }, }, config: { rippleEffect: true, @@ -319,6 +325,12 @@ describe('generateGlobalThemeCSS', () => { dark: { enabled: 'never', }, + highContrast: { + enabled: 'never', + }, + highContrastDark: { + enabled: 'never', + }, }, borderWidth: { sm: '4px', @@ -372,6 +384,12 @@ describe('generateGlobalThemeCSS', () => { dark: { enabled: 'never', }, + highContrast: { + enabled: 'never', + }, + highContrastDark: { + enabled: 'never', + }, }, borderWidth: { sm: '4px', @@ -437,6 +455,12 @@ describe('generateGlobalThemeCSS', () => { dark: { enabled: 'never', }, + highContrast: { + enabled: 'never', + }, + highContrastDark: { + enabled: 'never', + }, }, borderWidth: { sm: '4px', @@ -513,6 +537,12 @@ describe('generateGlobalThemeCSS', () => { }, }, }, + highContrast: { + enabled: 'never', + }, + highContrastDark: { + enabled: 'never', + }, }, borderWidth: { sm: '4px', @@ -555,6 +585,162 @@ describe('generateGlobalThemeCSS', () => { expect(css).toBe(expectedCSS); }); + + it('should generate global CSS for a given theme with high contrast palette enabled for system preference', () => { + const theme = { + name: 'test', + palette: { + light: {}, + dark: { + enabled: 'never', + }, + highContrast: { + enabled: 'system', + color: { + primary: { + bold: { + base: '#0054e9', + contrast: '#ffffff', + shade: '#0041c4', + tint: '#0065ff', + }, + subtle: { + base: '#0054e9', + contrast: '#ffffff', + shade: '#0041c4', + tint: '#0065ff', + }, + }, + red: { + 50: '#ffebee', + 100: '#ffcdd2', + 200: '#ef9a9a', + }, + }, + }, + highContrastDark: { + enabled: 'never', + }, + }, + borderWidth: { + sm: '4px', + }, + spacing: { + md: '12px', + }, + dynamicFont: '-apple-system-body', + }; + + const css = generateGlobalThemeCSS(theme).replace(/\s/g, ''); + + const expectedCSS = ` + :root { + --ion-border-width-sm: 4px; + --ion-spacing-md: 12px; + --ion-dynamic-font: -apple-system-body; + } + + @media(prefers-contrast: more) { + :root { + --ion-color-primary-bold: #0054e9; + --ion-color-primary-bold-rgb: 0, 84, 233; + --ion-color-primary-bold-contrast: #ffffff; + --ion-color-primary-bold-contrast-rgb: 255, 255, 255; + --ion-color-primary-bold-shade: #0041c4; + --ion-color-primary-bold-tint: #0065ff; + --ion-color-primary-subtle: #0054e9; + --ion-color-primary-subtle-rgb: 0, 84, 233; + --ion-color-primary-subtle-contrast: #ffffff; + --ion-color-primary-subtle-contrast-rgb: 255, 255, 255; + --ion-color-primary-subtle-shade: #0041c4; + --ion-color-primary-subtle-tint: #0065ff; + --ion-color-red-50: #ffebee; + --ion-color-red-100: #ffcdd2; + --ion-color-red-200: #ef9a9a; + } + } + `.replace(/\s/g, ''); + + expect(css).toBe(expectedCSS); + }); + + it('should generate global CSS for a given theme with high contrast dark palette enabled for system preference', () => { + const theme = { + name: 'test', + palette: { + light: {}, + dark: { + enabled: 'never', + }, + highContrast: { + enabled: 'never', + }, + highContrastDark: { + enabled: 'system', + color: { + primary: { + bold: { + base: '#0054e9', + contrast: '#ffffff', + shade: '#0041c4', + tint: '#0065ff', + }, + subtle: { + base: '#0054e9', + contrast: '#ffffff', + shade: '#0041c4', + tint: '#0065ff', + }, + }, + red: { + 50: '#ffebee', + 100: '#ffcdd2', + 200: '#ef9a9a', + }, + }, + }, + }, + borderWidth: { + sm: '4px', + }, + spacing: { + md: '12px', + }, + dynamicFont: '-apple-system-body', + }; + + const css = generateGlobalThemeCSS(theme).replace(/\s/g, ''); + + const expectedCSS = ` + :root { + --ion-border-width-sm: 4px; + --ion-spacing-md: 12px; + --ion-dynamic-font: -apple-system-body; + } + + @media(prefers-contrast: more) and (prefers-color-scheme: dark) { + :root { + --ion-color-primary-bold: #0054e9; + --ion-color-primary-bold-rgb: 0, 84, 233; + --ion-color-primary-bold-contrast: #ffffff; + --ion-color-primary-bold-contrast-rgb: 255, 255, 255; + --ion-color-primary-bold-shade: #0041c4; + --ion-color-primary-bold-tint: #0065ff; + --ion-color-primary-subtle: #0054e9; + --ion-color-primary-subtle-rgb: 0, 84, 233; + --ion-color-primary-subtle-contrast: #ffffff; + --ion-color-primary-subtle-contrast-rgb: 255, 255, 255; + --ion-color-primary-subtle-shade: #0041c4; + --ion-color-primary-subtle-tint: #0065ff; + --ion-color-red-50: #ffebee; + --ion-color-red-100: #ffcdd2; + --ion-color-red-200: #ef9a9a; + } + } + `.replace(/\s/g, ''); + + expect(css).toBe(expectedCSS); + }); }); describe('generateComponentThemeCSS', () => { diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index aeab9823ec2..94318125bfe 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -287,9 +287,27 @@ export const generateGlobalThemeCSS = (theme: any): string => { // Generate CSS variables for the dark color palette const darkTokensCSS = generateCSSVars(palette.dark); - // Include CSS variables for the dark color palette instead of - // the light palette if dark palette enabled is 'always' - const paletteTokensCSS = palette.dark.enabled === 'always' ? darkTokensCSS : lightTokensCSS; + // Generate CSS variable for the high contrast color palette + const highContrastTokensCSS = generateCSSVars(palette.highContrast); + + // Generate CSS variable for the high contrast dark color palette + const highContrastDarkTokensCSS = generateCSSVars(palette.highContrastDark); + + let paletteTokensCSS = lightTokensCSS; + + if (palette.highContrastDark?.enabled === 'always') { + // Include CSS variables for the high contrast dark color palette instead of + // the light palette if high contrast dark palette enabled is 'always' + paletteTokensCSS = highContrastDarkTokensCSS; + } else if (palette.highContrast?.enabled === 'always') { + // Include CSS variables for the dark color palette instead of + // the light palette if dark palette enabled is 'always' + paletteTokensCSS = highContrastTokensCSS; + } else if (palette.dark.enabled === 'always') { + // Include CSS variables for the dark color palette instead of + // the light palette if dark palette enabled is 'always' + paletteTokensCSS = darkTokensCSS; + } let css = ` ${CSS_ROOT_SELECTOR} { @@ -298,9 +316,25 @@ export const generateGlobalThemeCSS = (theme: any): string => { } `; - // Include CSS variables for the dark color palette inside of a - // class if dark palette enabled is 'class' - if (palette.dark.enabled === 'class') { + if (palette.highContrastDark.enabled === 'class') { + // Include CSS variables for the high contrast dark color palette inside of a + // class if high contrast dark palette enabled is 'class' + css += ` + .ion-palette-high-contrast.ion-palette-dark { + ${highContrastDarkTokensCSS} + } + `; + } else if (palette.highContrast.enabled === 'class') { + // Include CSS variables for the high contrast color palette inside of a + // class if high contrast palette enabled is 'class' + css += ` + .ion-palette-high-contrast { + ${highContrastTokensCSS} + } + `; + } else if (palette.dark.enabled === 'class') { + // Include CSS variables for the dark color palette inside of a + // class if dark palette enabled is 'class' css += ` .ion-palette-dark { ${darkTokensCSS} @@ -308,9 +342,29 @@ export const generateGlobalThemeCSS = (theme: any): string => { `; } - // Include CSS variables for the dark color palette inside of the - // dark color scheme media query if dark palette enabled is 'system' - if (palette.dark.enabled === 'system') { + if (palette.highContrastDark.enabled === 'system') { + // Include CSS variables for the high contrast dark color palette inside of the + // high contrast dark media query if high contrast dark palette enabled is 'system' + css += ` + @media (prefers-contrast: more) and (prefers-color-scheme: dark) { + ${CSS_ROOT_SELECTOR} { + ${highContrastDarkTokensCSS} + } + } + `; + } else if (palette.highContrast.enabled === 'system') { + // Include CSS variables for the high contrast color palette inside of the + // high contrast media query if high contrast palette enabled is 'system' + css += ` + @media (prefers-contrast: more) { + ${CSS_ROOT_SELECTOR} { + ${highContrastTokensCSS} + } + } + `; + } else if (palette.dark.enabled === 'system') { + // Include CSS variables for the dark color palette inside of the + // dark color scheme media query if dark palette enabled is 'system' css += ` @media (prefers-color-scheme: dark) { ${CSS_ROOT_SELECTOR} {