Skip to content

Commit

Permalink
Merge pull request #40773 from nextcloud/fix/contrast-maxcontrast-vs-…
Browse files Browse the repository at this point in the history
…hover

fix(theming): Ensure all text colors have enough contrast for accessibility
  • Loading branch information
susnux authored Oct 27, 2023
2 parents 66f7639 + 3378a73 commit 32eaf57
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 32 deletions.
128 changes: 128 additions & 0 deletions apps/theming/__tests__/accessibility.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved
import style from '!raw-loader!../css/default.css'

const testCases = {
'Main text': {
foregroundColors: [
'color-main-text',
// 'color-text-light', deprecated
// 'color-text-lighter', deprecated
'color-text-maxcontrast',
'color-text-maxcontrast-default',
],
backgroundColors: [
'color-background-main',
'color-background-hover',
'color-background-dark',
// 'color-background-darker', this should only be used for elements not for text
],
},
Primary: {
foregroundColors: [
'color-primary-text',
],
backgroundColors: [
// 'color-primary-default', this should only be used for elements not for text!
// 'color-primary-hover', this should only be used for elements and not for text!
'color-primary',
],
},
'Primary light': {
foregroundColors: [
'color-primary-light-text',
],
backgroundColors: [
'color-primary-light',
'color-primary-light-hover',
],
},
'Primary element': {
foregroundColors: [
'color-primary-element-text',
'color-primary-element-text-dark',
],
backgroundColors: [
'color-primary-element',
'color-primary-element-hover',
],
},
'Primary element light': {
foregroundColors: [
'color-primary-element-light-text',
],
backgroundColors: [
'color-primary-element-light',
'color-primary-element-light-hover',
],
},
'Servity information texts': {
foregroundColors: [
'color-error-text',
'color-warning-text',
'color-success-text',
'color-info-text',
],
backgroundColors: [
'color-background-main',
'color-background-hover',
],
},
}

/**
* Create a wrapper element with color and background set
*
* @param foreground The foreground color (css variable without leading --)
* @param background The background color
*/
function createTestCase(foreground: string, background: string) {
const wrapper = document.createElement('div')
wrapper.innerText = `${foreground} ${background}`
wrapper.style.color = `var(--${foreground})`
wrapper.style.backgroundColor = `var(--${background})`
wrapper.style.padding = '4px'
wrapper.setAttribute('data-cy-testcase', '')
return wrapper
}

describe('Accessibility of Nextcloud theming', () => {
before(() => {
cy.injectAxe()

const el = document.createElement('style')
el.innerText = style
document.head.appendChild(el)
})

beforeEach(() => {
cy.document().then(doc => {
const root = doc.querySelector('[data-cy-root]')
if (root === null) {
throw new Error('No test root found')
}
for (const child of root.children) {
root.removeChild(child)
}
})
})

for (const [name, { backgroundColors, foregroundColors }] of Object.entries(testCases)) {
context(`Accessibility of CSS color variables for ${name}`, () => {
for (const foreground of foregroundColors) {
for (const background of backgroundColors) {
it(`color contrast of ${foreground} on ${background}`, () => {
const element = createTestCase(foreground, background)
cy.document().then(doc => {
const root = doc.querySelector('[data-cy-root]')
// eslint-disable-next-line no-unused-expressions
expect(root).not.to.be.undefined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
root!.appendChild(element)
cy.checkA11y('[data-cy-testcase]')
})
})
}
}
})
}
})
38 changes: 21 additions & 17 deletions apps/theming/css/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@
--filter-background-blur: blur(25px);
--gradient-main-background: var(--color-main-background) 0%, var(--color-main-background-translucent) 85%, transparent 100%;
--color-background-hover: #f5f5f5;
/** Can be used e.g. to colorize selected table rows */
--color-background-dark: #ededed;
/** This should only be used for elements, not as a text background! Otherwise it will not work for accessibility. */
--color-background-darker: #dbdbdb;
--color-placeholder-light: #e6e6e6;
--color-placeholder-dark: #cccccc;
--color-main-text: #222222;
--color-text-maxcontrast: #767676;
--color-text-maxcontrast-default: #767676;
--color-text-maxcontrast-background-blur: #646464;
--color-text-light: #222222;
--color-text-lighter: #767676;
--color-text-maxcontrast: #6b6b6b;
--color-text-maxcontrast-default: #6b6b6b;
--color-text-maxcontrast-background-blur: #595959;
/** @deprecated use ` --color-main-text` instead */
--color-text-light: var(--color-main-text);
/** @deprecated use `--color-text-maxcontrast` instead */
--color-text-lighter: var(--color-text-maxcontrast);
--color-scrollbar: rgba(34,34,34, .15);
--color-error: #d91812;
--color-error-rgb: 217,24,18;
Expand All @@ -24,7 +28,7 @@
--color-warning: #c28900;
--color-warning-rgb: 194,137,0;
--color-warning-hover: #cea032;
--color-warning-text: #996c00;
--color-warning-text: #8f6500;
--color-success: #2d7b41;
--color-success-rgb: 45,123,65;
--color-success-hover: #448955;
Expand Down Expand Up @@ -64,20 +68,20 @@
--background-invert-if-bright: invert(100%);
--background-image-invert-if-bright: no;
--primary-invert-if-bright: no;
--color-primary: #006aa3;
--color-primary: #00679e;
--color-primary-default: #0082c9;
--color-primary-text: #ffffff;
--color-primary-hover: #3287b5;
--color-primary-light: #e5f0f5;
--color-primary-light-text: #002a41;
--color-primary-light-hover: #dbe5ea;
--color-primary-element: #006aa3;
--color-primary-element-hover: #1f7cae;
--color-primary-hover: #3285b1;
--color-primary-light: #e5eff5;
--color-primary-light-text: #00293f;
--color-primary-light-hover: #dbe4ea;
--color-primary-element: #00679e;
--color-primary-element-hover: #1674a6;
--color-primary-element-text: #ffffff;
--color-primary-element-light: #e5f0f5;
--color-primary-element-light-hover: #dbe5ea;
--color-primary-element-light-text: #002a41;
--color-primary-element-text-dark: #ededed;
--color-primary-element-text-dark: #f0f0f0;
--color-primary-element-light: #e5eff5;
--color-primary-element-light-hover: #dbe4ea;
--color-primary-element-light-text: #00293f;
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
--image-background-default: url('/apps/theming/img/background/kamil-porembinski-clouds.jpg');
--color-background-plain: #0082c9;
Expand Down
2 changes: 1 addition & 1 deletion apps/theming/lib/Service/BackgroundService.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class BackgroundService {
// true when the background is bright and need dark icons
public const THEMING_MODE_DARK = 'dark';
public const DEFAULT_COLOR = '#0082c9';
public const DEFAULT_ACCESSIBLE_COLOR = '#006aa3';
public const DEFAULT_ACCESSIBLE_COLOR = '#00679e';

public const BACKGROUND_SHIPPED = 'shipped';
public const BACKGROUND_CUSTOM = 'custom';
Expand Down
6 changes: 3 additions & 3 deletions apps/theming/lib/Themes/CommonThemeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ protected function generatePrimaryVariables(string $colorMainBackground, string

// used for buttons, inputs...
'--color-primary-element' => $colorPrimaryElement,
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 75),
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 82),
'--color-primary-element-text' => $this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff',
// mostly used for disabled states
'--color-primary-element-text-dark' => $this->util->darken($this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff', 6),

// used for hover/focus states
'--color-primary-element-light' => $colorPrimaryElementLight,
'--color-primary-element-light-hover' => $this->util->mix($colorPrimaryElementLight, $colorMainText, 90),
'--color-primary-element-light-text' => $this->util->mix($colorPrimaryElement, $this->util->invertTextColor($colorPrimaryElementLight) ? '#000000' : '#ffffff', -20),
// mostly used for disabled states
'--color-primary-element-text-dark' => $this->util->darken($this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff', 7),

// to use like this: background-image: var(--gradient-primary-background);
'--gradient-primary-background' => 'linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
Expand Down
4 changes: 2 additions & 2 deletions apps/theming/lib/Themes/DarkTheme.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ public function getCSSVariables(): array {
'--color-text-maxcontrast' => $colorTextMaxcontrast,
'--color-text-maxcontrast-default' => $colorTextMaxcontrast,
'--color-text-maxcontrast-background-blur' => $this->util->lighten($colorTextMaxcontrast, 2),
'--color-text-light' => $this->util->darken($colorMainText, 10),
'--color-text-lighter' => $this->util->darken($colorMainText, 20),
'--color-text-light' => 'var(--color-main-text)', // deprecated
'--color-text-lighter' => 'var(--color-text-maxcontrast)', // deprecated

'--color-error' => $colorError,
'--color-error-rgb' => join(',', $this->util->hexToRGB($colorError)),
Expand Down
9 changes: 5 additions & 4 deletions apps/theming/lib/Themes/DefaultTheme.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ public function getMediaQuery(): string {
public function getCSSVariables(): array {
$colorMainText = '#222222';
$colorMainTextRgb = join(',', $this->util->hexToRGB($colorMainText));
$colorTextMaxcontrast = $this->util->lighten($colorMainText, 33);
// Color that still provides enough contrast for text, so we need a ratio of 4.5:1 on main background AND hover
$colorTextMaxcontrast = '#6b6b6b'; // 4.5 : 1 for hover background and background dark
$colorMainBackground = '#ffffff';
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
$colorBoxShadow = $this->util->darken($colorMainBackground, 70);
Expand Down Expand Up @@ -137,8 +138,8 @@ public function getCSSVariables(): array {
'--color-text-maxcontrast' => $colorTextMaxcontrast,
'--color-text-maxcontrast-default' => $colorTextMaxcontrast,
'--color-text-maxcontrast-background-blur' => $this->util->darken($colorTextMaxcontrast, 7),
'--color-text-light' => $colorMainText,
'--color-text-lighter' => $this->util->lighten($colorMainText, 33),
'--color-text-light' => 'var(--color-main-text)', // deprecated
'--color-text-lighter' => 'var(--color-text-maxcontrast)', // deprecated

'--color-scrollbar' => 'rgba(' . $colorMainTextRgb . ', .15)',

Expand All @@ -150,7 +151,7 @@ public function getCSSVariables(): array {
'--color-warning' => $colorWarning,
'--color-warning-rgb' => join(',', $this->util->hexToRGB($colorWarning)),
'--color-warning-hover' => $this->util->mix($colorWarning, $colorMainBackground, 60),
'--color-warning-text' => $this->util->darken($colorWarning, 8),
'--color-warning-text' => $this->util->darken($colorWarning, 10),
'--color-success' => $colorSuccess,
'--color-success-rgb' => join(',', $this->util->hexToRGB($colorSuccess)),
'--color-success-hover' => $this->util->mix($colorSuccess, $colorMainBackground, 78),
Expand Down
2 changes: 2 additions & 0 deletions apps/theming/tests/Themes/DefaultThemeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ public function testThemindDisabledFallbackCss() {

$css = ":root {" . PHP_EOL . "$variables}" . PHP_EOL;
$fallbackCss = file_get_contents(__DIR__ . '/../../css/default.css');
// Remove comments
$fallbackCss = preg_replace('/\s*\/\*[\s\S]*?\*\//m', '', $fallbackCss);

$this->assertEquals($css, $fallbackCss);
}
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/theming/themingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import { colord } from 'colord'

export const defaultPrimary = '#0082c9'
export const defaultAccessiblePrimary = '#006aa3'
export const defaultAccessiblePrimary = '#00679e'
export const defaultBackground = 'kamil-porembinski-clouds.jpg'

/**
Expand Down
2 changes: 2 additions & 0 deletions cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import 'cypress-axe'

/* eslint-disable */
import { mount } from '@cypress/vue2'

Expand Down
Loading

0 comments on commit 32eaf57

Please sign in to comment.