feat(themes): enable all palettes (#30874)

Issue number: internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

The new theming structure does not allow high contrast and high contrast
dark to render properly. This can be seen when running a test that uses
`set-content`.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- High contrast and high contrast dark have been enabled to the testing
environments
- The theme files for high contrast and high contrast dark have been
added under `themes`.

## Does this introduce a breaking change?

- [x] Yes
- [ ] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

[Colors
preview](https://ionic-framework-git-set-content-ionic1.vercel.app/src/themes/test/color/)

How to test:
1. Verify that tests related to palettes are passing especially those
that use `setContent`
2. Verify that the preview page renders the right colors for each
possible variation of palettes and themes

---------

Co-authored-by: ionitron <hi@ionicframework.com>
Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
This commit is contained in:
Maria Hutt
2026-01-16 13:20:08 -08:00
committed by GitHub
parent a61ab385dc
commit 52779dd46c
28 changed files with 931 additions and 15 deletions

View File

@@ -21,6 +21,7 @@
*/
const DEFAULT_THEME = 'md';
const DEFAULT_PALETTE = 'light';
(function() {
@@ -88,17 +89,33 @@ const DEFAULT_THEME = 'md';
* or `high-contrast-dark`. Default to `light` for tests.
*/
const validPalettes = ['light', '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;
/**
* Ensure window.Ionic.config is defined before importing 'testing/scripts'
* in the test HTML to properly initialize the palette configuration below.
*
* Example:
* <script>
* window.Ionic = { config: { customTheme: { palette: { ... } } } };
* </script>
* <script src="testing/scripts.js"></script>
*/
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 +136,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 palette
if (paletteName === 'dark' && theme.palette?.dark) {
theme.palette.dark.enabled = 'always';
// TODO(FW-4005): Implement high contrast palette
} else if (paletteName === 'high-contrast' && theme.palette?.highContrast) {
theme.palette.highContrast.enabled = 'always';
// TODO(FW-4005): Implement high contrast dark palette
} else if (paletteName === 'high-contrast-dark' && theme.palette?.highContrastDark) {
theme.palette.highContrastDark.enabled = 'always';
}
// Apply the theme tokens to Ionic config

View File

@@ -1,3 +1,5 @@
<!-- TODO(FW-4004): Remove page and test it through the basic page tests -->
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
@@ -10,6 +12,21 @@
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../css/palettes/dark.always.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script>
// Need to be called before loading Ionic else
// the scripts.js logic runs too early.
window.Ionic = {
config: {
customTheme: {
palette: {
dark: {
enabled: 'always',
},
},
},
},
};
</script>
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>

View File

@@ -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(
`

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -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: {

View File

@@ -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',
},
};

View File

@@ -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',
},
};

View File

@@ -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',

View File

@@ -0,0 +1,6 @@
import { highContrastDarkTheme as baseHighContrastDarkTheme } from '../base/high-contrast-dark.tokens';
import type { HighContrastDarkTheme } from '../themes.interfaces';
export const highContrastDarkTheme: HighContrastDarkTheme = {
...baseHighContrastDarkTheme,
};

View File

@@ -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',
},
};

View File

@@ -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',

View File

@@ -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: {

View File

@@ -0,0 +1,36 @@
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',
},
};

View File

@@ -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',
},
};

View File

@@ -974,7 +974,7 @@
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

View File

@@ -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;

View File

@@ -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 = `
<!DOCTYPE html>
<html dir="${direction}" lang="en">
@@ -77,14 +105,14 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
${ionicCSSImports}
<link href="${baseUrl}/scripts/testing/styles.css" rel="stylesheet" />
${palette !== 'light' ? `<link href="${baseUrl}/css/palettes/${palette}.always.css" rel="stylesheet" />` : ''}
<script src="${baseUrl}/scripts/testing/scripts.js"></script>
${ionicJSImports}
<script>
window.Ionic = {
config: {
mode: '${mode}',
theme: '${theme}'
theme: '${theme}',
customTheme: ${JSON.stringify(customTheme)}
}
}
</script>

View File

@@ -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', () => {

View File

@@ -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} {