From 15cbe77848f720061d8a286f5496e2e1aac27c78 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 7 May 2024 11:59:07 +0200 Subject: [PATCH] Improved placeholders and font color grids in high contrast mode (#16284) Feature (utils): Implemented the `env#isMediaForcedColors` property for forced colors detection (e.g. high contrast mode on Windows). See #14907. Feature (ui): Implemented `ck-media-forced-colors` and `ck-media-default-colors` mixins for detecting forced colors (e.g. high contrast mode on Windows). See #14907. Fix (theme-lark): The caret should be visible in a placeholder while in forced colors mode (e.g. high contrast mode on Windows). Improved the look of the placeholders in the forced colors mode. Closes #14907. Fix (theme-lark): The color grid component should render as a grid of labels in the forced colors mode (e.g. high contrast mode on Windows). Closes #14907. --- .../ckeditor5-image/theme/imagecaption.css | 12 +++- packages/ckeditor5-table/package.json | 2 +- .../ckeditor5-table/theme/tablecaption.css | 14 +++- .../theme/ckeditor5-engine/placeholder.css | 29 +++++++- .../components/colorgrid/colorgrid.css | 70 ++++++++++++------- .../theme/ckeditor5-widget/widget.css | 6 +- .../src/colorgrid/colortileview.ts | 5 +- .../tests/colorgrid/colortileview.js | 28 +++++++- .../theme/mixins/_mediacolors.css | 20 ++++++ packages/ckeditor5-utils/src/env.ts | 14 ++++ packages/ckeditor5-utils/tests/env.js | 26 ++++++- 11 files changed, 189 insertions(+), 37 deletions(-) create mode 100644 packages/ckeditor5-ui/theme/mixins/_mediacolors.css diff --git a/packages/ckeditor5-image/theme/imagecaption.css b/packages/ckeditor5-image/theme/imagecaption.css index 1bb6d94b69a..846b1663dc8 100644 --- a/packages/ckeditor5-image/theme/imagecaption.css +++ b/packages/ckeditor5-image/theme/imagecaption.css @@ -3,6 +3,8 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +@import "@ckeditor/ckeditor5-ui/theme/mixins/_mediacolors.css"; + :root { --ck-color-image-caption-background: hsl(0, 0%, 97%); --ck-color-image-caption-text: hsl(0, 0%, 20%); @@ -19,11 +21,19 @@ padding: .6em; font-size: .75em; outline-offset: -1px; + + /* Improve placeholder rendering in high-constrast mode (https://github.com/ckeditor/ckeditor5/issues/14907). */ + @media (forced-colors: active) { + background-color: unset; + color: unset; + } } /* Editing styles */ .ck.ck-editor__editable .image > figcaption.image__caption_highlighted { - animation: ck-image-caption-highlight .6s ease-out; + @mixin ck-media-default-colors { + animation: ck-image-caption-highlight .6s ease-out; + } @media (prefers-reduced-motion: reduce) { animation: none; diff --git a/packages/ckeditor5-table/package.json b/packages/ckeditor5-table/package.json index a449d791cfd..15f5f314fc5 100644 --- a/packages/ckeditor5-table/package.json +++ b/packages/ckeditor5-table/package.json @@ -14,6 +14,7 @@ "main": "src/index.ts", "dependencies": { "ckeditor5": "41.3.1", + "@ckeditor/ckeditor5-ui": "41.3.1", "lodash-es": "4.17.21" }, "devDependencies": { @@ -36,7 +37,6 @@ "@ckeditor/ckeditor5-paragraph": "41.3.1", "@ckeditor/ckeditor5-theme-lark": "41.3.1", "@ckeditor/ckeditor5-typing": "41.3.1", - "@ckeditor/ckeditor5-ui": "41.3.1", "@ckeditor/ckeditor5-undo": "41.3.1", "@ckeditor/ckeditor5-utils": "41.3.1", "@ckeditor/ckeditor5-widget": "41.3.1", diff --git a/packages/ckeditor5-table/theme/tablecaption.css b/packages/ckeditor5-table/theme/tablecaption.css index dcdc690fff2..342afeb7365 100644 --- a/packages/ckeditor5-table/theme/tablecaption.css +++ b/packages/ckeditor5-table/theme/tablecaption.css @@ -3,6 +3,8 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +@import "@ckeditor/ckeditor5-ui/theme/mixins/_mediacolors.css"; + :root { --ck-color-selector-caption-background: hsl(0, 0%, 97%); --ck-color-selector-caption-text: hsl(0, 0%, 20%); @@ -20,12 +22,20 @@ padding: .6em; font-size: .75em; outline-offset: -1px; + + /* Improve placeholder rendering in high-constrast mode (https://github.com/ckeditor/ckeditor5/issues/14907). */ + @mixin ck-media-forced-colors { + background-color: unset; + color: unset; + } } /* Editing styles */ .ck.ck-editor__editable .table > figcaption { - &.table__caption_highlighted { - animation: ck-table-caption-highlight .6s ease-out; + @mixin ck-media-default-colors { + &.table__caption_highlighted { + animation: ck-table-caption-highlight .6s ease-out; + } } &.ck-placeholder::before { diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-engine/placeholder.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-engine/placeholder.css index db8a16e3611..e5eb5f0a5e9 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-engine/placeholder.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-engine/placeholder.css @@ -3,10 +3,37 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +@import "@ckeditor/ckeditor5-ui/theme/mixins/_mediacolors.css"; + /* See ckeditor/ckeditor5#936. */ .ck.ck-placeholder, .ck .ck-placeholder { + @mixin ck-media-forced-colors { + /* + * This is needed for Edge on Windows to use the right color for the placeholder content (::before). + * See https://github.com/ckeditor/ckeditor5/issues/14907. + */ + forced-color-adjust: preserve-parent-color; + } + &::before { cursor: text; - color: var(--ck-color-engine-placeholder-text); + + @mixin ck-media-default-colors { + color: var(--ck-color-engine-placeholder-text); + } + + @mixin ck-media-forced-colors { + /* + * In the high contrast mode there is no telling between regular and placeholder text. Using + * italic text to address that issue. See https://github.com/ckeditor/ckeditor5/issues/14907. + */ + font-style: italic; + + /* + * Without this margin, the caret will not show up and blink when the user puts the selection + * in the placeholder (Edge on Windows). See https://github.com/ckeditor/ckeditor5/issues/14907. + */ + margin-left: 1px; + } } } diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/colorgrid/colorgrid.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/colorgrid/colorgrid.css index cec25ce4f0f..443d4bb7f46 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/colorgrid/colorgrid.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/colorgrid/colorgrid.css @@ -4,6 +4,7 @@ */ @import "../../../mixins/_rounded.css"; +@import "@ckeditor/ckeditor5-ui/theme/mixins/_mediacolors.css"; :root { --ck-color-grid-tile-size: 24px; @@ -20,13 +21,52 @@ } .ck.ck-color-grid__tile { - width: var(--ck-color-grid-tile-size); - height: var(--ck-color-grid-tile-size); - min-width: var(--ck-color-grid-tile-size); - min-height: var(--ck-color-grid-tile-size); - padding: 0; transition: .2s ease box-shadow; - border: 0; + + @mixin ck-media-default-colors { + width: var(--ck-color-grid-tile-size); + height: var(--ck-color-grid-tile-size); + min-width: var(--ck-color-grid-tile-size); + min-height: var(--ck-color-grid-tile-size); + padding: 0; + border: 0; + + &.ck-on, + &:focus:not( .ck-disabled ), + &:hover:not( .ck-disabled ) { + /* Disable the default .ck-button's border ring. */ + border: 0; + } + + &.ck-color-selector__color-tile_bordered { + box-shadow: 0 0 0 1px var(--ck-color-base-border); + } + + &.ck-on { + box-shadow: inset 0 0 0 1px var(--ck-color-base-background), 0 0 0 2px var(--ck-color-base-text); + } + + &:focus:not( .ck-disabled ), + &:hover:not( .ck-disabled ) { + box-shadow: inset 0 0 0 1px var(--ck-color-base-background), 0 0 0 2px var(--ck-color-focus-border); + } + } + + /* + * In high contrast mode, the colors are replaced with text labels. + * See https://github.com/ckeditor/ckeditor5/issues/14907. + */ + @mixin ck-media-forced-colors { + width: unset; + height: unset; + min-width: unset; + min-height: unset; + padding: 0 var(--ck-spacing-small); + + & .ck-button__label { + display: inline-block; + } + } @media (prefers-reduced-motion: reduce) { transition: none; @@ -37,34 +77,16 @@ transition: unset; } - &.ck-color-selector__color-tile_bordered { - box-shadow: 0 0 0 1px var(--ck-color-base-border); - } - & .ck.ck-icon { display: none; color: var(--ck-color-color-grid-check-icon); } &.ck-on { - box-shadow: inset 0 0 0 1px var(--ck-color-base-background), 0 0 0 2px var(--ck-color-base-text); - & .ck.ck-icon { display: block; } } - - &.ck-on, - &:focus:not( .ck-disabled ), - &:hover:not( .ck-disabled ) { - /* Disable the default .ck-button's border ring. */ - border: 0; - } - - &:focus:not( .ck-disabled ), - &:hover:not( .ck-disabled ) { - box-shadow: inset 0 0 0 1px var(--ck-color-base-background), 0 0 0 2px var(--ck-color-focus-border); - } } .ck.ck-color-grid__label { diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widget.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widget.css index e1e08c425cf..cb71b166187 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widget.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widget.css @@ -5,6 +5,7 @@ @import "../mixins/_focus.css"; @import "../mixins/_shadow.css"; +@import "@ckeditor/ckeditor5-ui/theme/mixins/_mediacolors.css"; :root { --ck-widget-outline-thickness: 3px; @@ -47,8 +48,9 @@ &:focus { @mixin ck-focus-ring; @mixin ck-box-shadow var(--ck-inner-shadow); - - background-color: var(--ck-color-widget-editable-focus-background); + @mixin ck-media-default-colors { + background-color: var(--ck-color-widget-editable-focus-background); + } } } diff --git a/packages/ckeditor5-ui/src/colorgrid/colortileview.ts b/packages/ckeditor5-ui/src/colorgrid/colortileview.ts index 8f4fa13c163..91208234595 100644 --- a/packages/ckeditor5-ui/src/colorgrid/colortileview.ts +++ b/packages/ckeditor5-ui/src/colorgrid/colortileview.ts @@ -9,7 +9,7 @@ import ButtonView from '../button/buttonview.js'; -import type { Locale } from '@ckeditor/ckeditor5-utils'; +import { env, type Locale } from '@ckeditor/ckeditor5-utils'; import checkIcon from '../../theme/icons/color-tile-check.svg'; @@ -41,7 +41,8 @@ export default class ColorTileView extends ButtonView { this.extendTemplate( { attributes: { style: { - backgroundColor: bind.to( 'color' ) + // https://github.com/ckeditor/ckeditor5/issues/14907 + backgroundColor: bind.to( 'color', color => env.isMediaForcedColors ? null : color ) }, class: [ 'ck', diff --git a/packages/ckeditor5-ui/tests/colorgrid/colortileview.js b/packages/ckeditor5-ui/tests/colorgrid/colortileview.js index cb8d99e0fe9..836991fa757 100644 --- a/packages/ckeditor5-ui/tests/colorgrid/colortileview.js +++ b/packages/ckeditor5-ui/tests/colorgrid/colortileview.js @@ -6,14 +6,28 @@ import ColorTileView from '../../src/colorgrid/colortileview.js'; import ButtonView from '../../src/button/buttonview.js'; import checkIcon from '../../theme/icons/color-tile-check.svg'; +import { env } from '@ckeditor/ckeditor5-utils'; + +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; describe( 'ColorTileView', () => { + let colorTile; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + colorTile = new ColorTileView(); + } ); + + afterEach( () => { + colorTile.destroy(); + } ); + it( 'inherits from ButtonView', () => { - expect( new ColorTileView() ).to.be.instanceOf( ButtonView ); + expect( colorTile ).to.be.instanceOf( ButtonView ); } ); it( 'has proper attributes and classes', () => { - const colorTile = new ColorTileView(); colorTile.render(); expect( colorTile.color ).to.be.undefined; @@ -29,8 +43,16 @@ describe( 'ColorTileView', () => { expect( colorTile.element.classList.contains( 'ck-color-selector__color-tile_bordered' ) ).to.be.true; } ); + // https://github.com/ckeditor/ckeditor5/issues/14907 + it( 'should not set the background-color in the forced-colors mode for a better UX (displaying a label instead)', () => { + testUtils.sinon.stub( env, 'isMediaForcedColors' ).value( true ); + + colorTile.render(); + + expect( colorTile.element.style.backgroundColor ).to.equal( '' ); + } ); + it( 'has a check icon', () => { - const colorTile = new ColorTileView(); colorTile.render(); expect( colorTile.icon ).to.equal( checkIcon ); diff --git a/packages/ckeditor5-ui/theme/mixins/_mediacolors.css b/packages/ckeditor5-ui/theme/mixins/_mediacolors.css new file mode 100644 index 00000000000..e678502feeb --- /dev/null +++ b/packages/ckeditor5-ui/theme/mixins/_mediacolors.css @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +@define-mixin ck-media-forced-colors { + @media (forced-colors: active) { + & { + @mixin-content; + } + } +} + +@define-mixin ck-media-default-colors { + @media (forced-colors: none) { + & { + @mixin-content; + } + } +} diff --git a/packages/ckeditor5-utils/src/env.ts b/packages/ckeditor5-utils/src/env.ts index 29a80723d3f..f77fc0a8961 100644 --- a/packages/ckeditor5-utils/src/env.ts +++ b/packages/ckeditor5-utils/src/env.ts @@ -65,6 +65,11 @@ export interface EnvType { */ readonly isBlink: boolean; + /** + * Indicates that the the user agent has enabled a forced colors mode (e.g. Windows High Contrast mode). + */ + readonly isMediaForcedColors: boolean; + /** * Indicates that "prefer reduced motion" browser setting is active. */ @@ -104,6 +109,8 @@ const env: EnvType = { isBlink: isBlink( userAgent ), + isMediaForcedColors: isMediaForcedColors(), + get isMotionReduced() { return isMotionReduced(); }, @@ -209,6 +216,13 @@ export function isRegExpUnicodePropertySupported(): boolean { return isSupported; } +/** + * Checks if the user agent has enabled a forced colors mode (e.g. Windows High Contrast mode). + */ +export function isMediaForcedColors(): boolean { + return window.matchMedia( '(forced-colors: active)' ).matches; +} + /** * Checks if user enabled "prefers reduced motion" setting in browser. */ diff --git a/packages/ckeditor5-utils/tests/env.js b/packages/ckeditor5-utils/tests/env.js index 16638be7208..b9e1a426098 100644 --- a/packages/ckeditor5-utils/tests/env.js +++ b/packages/ckeditor5-utils/tests/env.js @@ -4,7 +4,7 @@ */ import env, { - isMac, isWindows, isGecko, isSafari, isiOS, isAndroid, isRegExpUnicodePropertySupported, isBlink, getUserAgent + isMac, isWindows, isGecko, isSafari, isiOS, isAndroid, isRegExpUnicodePropertySupported, isBlink, getUserAgent, isMediaForcedColors } from '../src/env.js'; import global from '../src/dom/global.js'; @@ -63,6 +63,12 @@ describe( 'Env', () => { } ); } ); + describe( 'isMediaForcedColors', () => { + it( 'is a boolean', () => { + expect( env.isMediaForcedColors ).to.be.a( 'boolean' ); + } ); + } ); + describe( 'isMotionReduced', () => { let matchMediaStub; @@ -304,6 +310,24 @@ describe( 'Env', () => { /* eslint-enable max-len */ } ); + describe( 'isMediaForcedColors()', () => { + it( 'returns true if the document media query matches forced-colors', () => { + testUtils.sinon.stub( global.window, 'matchMedia' ) + .withArgs( '(forced-colors: active)' ) + .returns( { matches: true } ); + + expect( isMediaForcedColors() ).to.be.true; + } ); + + it( 'returns false if the document media query does not match forced-colors', () => { + testUtils.sinon.stub( global.window, 'matchMedia' ) + .withArgs( '(forced-colors: active)' ) + .returns( { matches: false } ); + + expect( isMediaForcedColors() ).to.be.false; + } ); + } ); + describe( 'isRegExpUnicodePropertySupported()', () => { it( 'should detect accessibility of unicode properties', () => { // Usage of regular expression literal cause error during build (ckeditor/ckeditor5-dev#534)