Compare commits

...

5 Commits

Author SHA1 Message Date
Sean Perkins
ecd03a2dba chore: lint 2023-12-05 16:25:30 -05:00
Sean Perkins
bf18d51256 chore: detect setContent within goto 2023-12-05 15:02:40 -05:00
Sean Perkins
fefaa8b57c chore(playwright): throw error when using themes config with page.goto 2023-12-05 13:46:15 -05:00
Sean Perkins
dc696695ce Merge remote-tracking branch 'origin/feature-8.0' into FW-5584 2023-12-05 13:45:18 -05:00
Sean Perkins
242dee551e feat: WCAG AA compliant dark theme colors (#28609)
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. -->

Ionic's proposed color palette for dark theme is not WCAG AA compliant. 

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

- Updates the dark theme (`dark.css`) file with the proposed color
palette for Level AA color contrast compliance.
- Adds tests for verifying color contrast requirements for the following
cases:
- Base color on dark theme background (`#00000` on iOS an `#121212` on
MD)
  - Contrast color on base color
  - Base color on base color with 0.08 opacity
  - Base color on base color with 0.12 opacity
  - Base color on base color with 0.16 opacity

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

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

---------

Co-authored-by: Brandy Carney <brandy@ionic.io>
2023-12-01 16:12:13 -05:00
7 changed files with 366 additions and 47 deletions

View File

@@ -4,68 +4,68 @@
*/
:root {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66, 140, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
--ion-color-primary: #4d8dff;
--ion-color-primary-rgb: 77, 141, 255;
--ion-color-primary-contrast: #000000;
--ion-color-primary-contrast-rgb: 0, 0, 0;
--ion-color-primary-shade: #447ce0;
--ion-color-primary-tint: #5f98ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80, 200, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-secondary: #62bdff;
--ion-color-secondary-rgb: 98, 189, 255;
--ion-color-secondary-contrast: #000000;
--ion-color-secondary-contrast-rgb: 0, 0, 0;
--ion-color-secondary-shade: #56a6e0;
--ion-color-secondary-tint: #72c4ff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106, 100, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-tertiary: #8482fb;
--ion-color-tertiary-rgb: 132, 130, 251;
--ion-color-tertiary-contrast: #000000;
--ion-color-tertiary-contrast-rgb: 0, 0, 0;
--ion-color-tertiary-shade: #7472dd;
--ion-color-tertiary-tint: #908ffb;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47, 223, 117;
--ion-color-success: #2dd55b;
--ion-color-success-rgb: 45, 213, 91;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0, 0, 0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-success-shade: #28bb50;
--ion-color-success-tint: #42d96b;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255, 213, 52;
--ion-color-warning: #ffce31;
--ion-color-warning-rgb: 255, 206, 49;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-warning-shade: #e0b52b;
--ion-color-warning-tint: #ffd346;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255, 73, 97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-danger: #f56570;
--ion-color-danger-rgb: 245, 101, 112;
--ion-color-danger-contrast: #000000;
--ion-color-danger-contrast-rgb: 0, 0, 0;
--ion-color-danger-shade: #d85963;
--ion-color-danger-tint: #f6747e;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244, 245, 248;
--ion-color-dark: #f3f3f3;
--ion-color-dark-rgb: 243, 243, 243;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0, 0, 0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-dark-shade: #d6d6d6;
--ion-color-dark-tint: #f4f4f4;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152, 154, 162;
--ion-color-medium: #959595;
--ion-color-medium-rgb: 149, 149, 149;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0, 0, 0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-medium-shade: #838383;
--ion-color-medium-tint: #a0a0a0;
--ion-color-light: #222428;
--ion-color-light-rgb: 34, 36, 40;
--ion-color-light: #2f2f2f;
--ion-color-light-rgb: 47, 47, 47;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255, 255, 255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
--ion-color-light-shade: #292929;
--ion-color-light-tint: #444444;
}
/*

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Theme - Dark</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../scripts/testing/styles.css" rel="stylesheet" />
<link href="../../../../scripts/testing/themes/dark.css" rel="stylesheet" />
<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>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Theme - Dark</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding ion-text-center" id="content" no-bounce>
<p>
<ion-button id="default">Default</ion-button>
<ion-button class="ion-focused">Default.focused</ion-button>
<ion-button class="ion-activated">Default.activated</ion-button>
</p>
<p>
<ion-button color="primary">Primary</ion-button>
<ion-button class="ion-focused" color="primary">Primary.focused</ion-button>
<ion-button class="ion-activated" color="primary">Primary.activated</ion-button>
</p>
<p>
<ion-button color="secondary">Secondary</ion-button>
<ion-button class="ion-focused" color="secondary">Secondary.focused</ion-button>
<ion-button class="ion-activated" color="secondary">Secondary.activated</ion-button>
</p>
<p>
<ion-button color="tertiary">Tertiary</ion-button>
<ion-button class="ion-focused" color="tertiary">Tertiary.focused</ion-button>
<ion-button class="ion-activated" color="tertiary">Tertiary.activated</ion-button>
</p>
<p>
<ion-button color="success">Success</ion-button>
<ion-button class="ion-focused" color="success">Success.focused</ion-button>
<ion-button class="ion-activated" color="success">Success.activated</ion-button>
</p>
<p>
<ion-button color="warning">Warning</ion-button>
<ion-button class="ion-focused" color="warning">Warning.focused</ion-button>
<ion-button class="ion-activated" color="warning">Warning.activated</ion-button>
</p>
<p>
<ion-button color="danger">Danger</ion-button>
<ion-button class="ion-focused" color="danger">Danger.focused</ion-button>
<ion-button class="ion-activated" color="danger">Danger.activated</ion-button>
</p>
<p>
<ion-button color="light">Light</ion-button>
<ion-button class="ion-focused" color="light">Light.focused</ion-button>
<ion-button class="ion-activated" color="light">Light.activated</ion-button>
</p>
<p>
<ion-button color="medium">Medium</ion-button>
<ion-button class="ion-focused" color="medium">Medium.focused</ion-button>
<ion-button class="ion-activated" color="medium">Medium.activated</ion-button>
</p>
<p>
<ion-button color="dark">Dark</ion-button>
<ion-button class="ion-focused" color="dark">Dark.focused</ion-button>
<ion-button class="ion-activated" color="dark">Dark.activated</ion-button>
</p>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,214 @@
import AxeBuilder from '@axe-core/playwright';
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
const styleTestHelpers = `
<style>
.ion-background {
background: var(--ion-color-base);
}
.ion-background-opacity-08 {
background: rgba(var(--ion-color-base-rgb), 0.08);
}
.ion-background-opacity-12 {
background: rgba(var(--ion-color-base-rgb), 0.12);
}
.ion-background-opacity-16 {
background: rgba(var(--ion-color-base-rgb), 0.16);
}
.ion-color {
color: var(--ion-color-base);
}
.ion-color-contrast {
color: var(--ion-color-contrast);
}
.ion-color-shade {
color: var(--ion-color-shade);
}
.ion-color-tint {
color: var(--ion-color-tint);
}
</style>
`;
/**
* All colors besides `light` should be tested against a dark background on dark theme.
*/
configs({ modes: ['md', 'ios'], directions: ['ltr'], themes: ['dark'] }).forEach(({ config, title }) => {
const colors = ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger', 'medium', 'dark'];
test.describe(title('theme'), () => {
test.beforeEach(({ skip }) => {
skip.browser('firefox', 'Color contrast ratio is consistent across browsers');
skip.browser('webkit', 'Color contrast ratio is consistent across browsers');
});
for (const color of colors) {
test(`color "${color}" should pass AA guidelines`, async ({ page }) => {
await page.setContent(
`${styleTestHelpers}
<main>
<p class="ion-color ion-color-${color}">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test(`contrast color on "${color}" background should pass AA guidelines`, async ({ page }) => {
await page.setContent(
`${styleTestHelpers}
<main>
<p class="ion-color-contrast ion-background ion-color-${color}">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test(`color "${color}" on 0.08 opacity background should pass AA guidelines`, async ({ page }) => {
await page.setContent(
`${styleTestHelpers}
<main>
<p class="ion-color ion-color-${color} ion-background-opacity-08">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test(`color "${color}" on 0.12 opacity background should pass AA guidelines`, async ({ page }) => {
await page.setContent(
`${styleTestHelpers}
<main>
<p class="ion-color ion-color-${color} ion-background-opacity-12">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test(`color "${color}" on 0.16 opacity background should pass AA guidelines`, async ({ page }) => {
await page.setContent(
`${styleTestHelpers}
<main>
<p class="ion-color ion-color-${color} ion-background-opacity-16">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
}
});
});
/**
* `light` color should be tested against a white background on dark theme. The mode doesn't matter here
* since we are testing against a consistent background color.
*/
configs({ modes: ['md'], directions: ['ltr'], themes: ['dark'] }).forEach(({ config, title }) => {
test.describe(title('theme'), () => {
test(`color light should pass AA guidelines on a white background`, async ({ page, skip }) => {
skip.browser('firefox', 'Color contrast ratio is consistent across browsers');
skip.browser('webkit', 'Color contrast ratio is consistent across browsers');
await page.setContent(
`${styleTestHelpers}
<style>
.md body {
--ion-background-color: #ffffff;
}
</style>
<main>
<p class="ion-color ion-color-light">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test(`contrast color on "light" background should pass AA guidelines`, async ({ page }) => {
await page.setContent(
`${styleTestHelpers}
<style>
.md body {
--ion-background-color: #ffffff;
}
</style>
<main>
<p class="ion-color-contrast ion-background ion-color-light">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test(`color "light" on 0.08 opacity background should pass AA guidelines`, async ({ page }) => {
await page.setContent(
`${styleTestHelpers}
<style>
.md body {
--ion-background-color: #ffffff;
}
</style>
<main>
<p class="ion-color ion-color-light ion-background-opacity-08">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test(`color "light" on 0.12 opacity background should pass AA guidelines`, async ({ page }) => {
await page.setContent(
`${styleTestHelpers}
<style>
.md body {
--ion-background-color: #ffffff;
}
</style>
<main>
<p class="ion-color ion-color-light ion-background-opacity-12">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test(`color "light" on 0.16 opacity background should pass AA guidelines`, async ({ page }) => {
await page.setContent(
`${styleTestHelpers}
<style>
.md body {
--ion-background-color: #ffffff;
}
</style>
<main>
<p class="ion-color ion-color-light ion-background-opacity-16">Hello World</p>
</main>`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
});
});

View File

@@ -111,4 +111,5 @@ const DEFAULT_THEMES: Theme[] = ['light'];
const DEFAULT_TEST_CONFIG_OPTION = {
modes: DEFAULT_MODES,
directions: DEFAULT_DIRECTIONS,
themes: DEFAULT_THEMES,
};

View File

@@ -1,5 +1,5 @@
import type { Page, TestInfo } from '@playwright/test';
import type { E2EPageOptions, Mode, Direction } from '@utils/test/playwright';
import type { E2EPageOptions, Mode, Direction, Theme } from '@utils/test/playwright';
/**
* This is an extended version of Playwright's
@@ -28,13 +28,22 @@ configs().forEach(({ config, title }) => {
let mode: Mode;
let direction: Direction;
let theme: Theme;
if (options == undefined) {
mode = testInfo.project.metadata.mode;
direction = testInfo.project.metadata.rtl ? 'rtl' : 'ltr';
theme = testInfo.project.metadata.theme;
} else {
mode = options.mode;
direction = options.direction;
theme = options.theme;
}
if (theme !== 'light' && options?.inline === undefined) {
throw new Error(
'The "themes" config option is only supported when using "page.setContent". Remove the theme from the config and manually set the theme in the test template.'
);
}
const rtlString = direction === 'rtl' ? 'true' : undefined;

View File

@@ -82,6 +82,6 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o
}
});
await page.goto(`${baseUrl}#`, options);
await page.goto(`${baseUrl}#`, { ...options, inline: true } as E2EPageOptions);
}
};

View File

@@ -4,7 +4,13 @@ import type { TestConfig } from './generator';
import type { EventSpy } from './page/event-spy';
import type { LocatorOptions, E2ELocator } from './page/utils/locator';
export interface E2EPageOptions extends PageOptions, TestConfig {}
export interface E2EPageOptions extends PageOptions, TestConfig {
/**
* Indicates if the page content is set inline with `.setContent`. This setting can
* only be true or not set.
*/
inline?: true;
}
interface PageOptions {
/**