diff --git a/src/editor/components/BBContrastChecker.js b/src/editor/components/BBContrastChecker.js new file mode 100644 index 00000000..d85a51df --- /dev/null +++ b/src/editor/components/BBContrastChecker.js @@ -0,0 +1,102 @@ +import { Notice } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +const BBContrastChecker = ( { background, foreground } ) => { + /** + * If either colour is not provided, don't render anything + */ + if ( ! background || ! foreground ) { + return; + } + + /** + * Get the luminance of a colour + * + * @param {string} colour The colour to convert to luminance value + * + * @return {number} The luminance value + */ + const getLuminance = ( colour ) => { + const rgb = colour + .match( /\w\w/g ) + .map( ( c ) => parseInt( c, 16 ) / 255 ); + const [ r, g, b ] = rgb.map( ( c ) => { + return c <= 0.03928 + ? c / 12.92 + : Math.pow( ( c + 0.055 ) / 1.055, 2.4 ); + } ); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + }; + + /** + * Compare two colours and return their contrast ratio. + * + * @param {string} colour1 This is an example function/method parameter description. + * @param {string} colour2 This is a second example. + * + * @return {number} The contrast ratio between the colours + */ + const getContrastRatio = ( colour1, colour2 ) => { + const luminance1 = getLuminance( colour1 ); + const luminance2 = getLuminance( colour2 ); + return ( + ( Math.max( luminance1, luminance2 ) + 0.05 ) / + ( Math.min( luminance1, luminance2 ) + 0.05 ) + ); + }; + + /** + * Determine the colour contrast ratio + */ + const contrastRatio = getContrastRatio( background, foreground ); + + /** + * Determine the message to output + */ + const displayMessage = + contrastRatio >= 4.5 + ? __( + 'The selected colours do not meet the colour contrast ratio for AAA (7:1) accessibility standards', + 'themer' + ) + : __( + 'The selected colours do not meet the colour contrast ratio for AA (4.5:1) accessibility standards', + 'themer' + ); + + /** + * Set the notice display level based on the contrast ratio + */ + const displayMessageImportance = contrastRatio >= 4.5 ? 'info' : 'warning'; + + return ( + <> +
+

+ { __( 'WCAG Check:', 'themer' ) } +

+

+ { contrastRatio.toFixed( 1 ) } : 1 +

+
+

= 4.5 ? 'pass' : ' fail' }> + ✓ { __( 'AA', 'themer' ) } +

+

= 7 ? 'pass' : ' fail' }> + ✓ { __( 'AAA ', 'themer' ) } +

+
+
+ { contrastRatio < 7 && ( + + { displayMessage } + + ) } + + ); +}; + +export default BBContrastChecker; diff --git a/src/editor/components/StylesColor.js b/src/editor/components/StylesColor.js index f47cefdd..83c0ac38 100644 --- a/src/editor/components/StylesColor.js +++ b/src/editor/components/StylesColor.js @@ -1,4 +1,4 @@ -import { set } from 'lodash'; +import { set, debounce } from 'lodash'; import { __ } from '@wordpress/i18n'; import { useContext } from '@wordpress/element'; import { ColorPalette } from '@wordpress/components'; @@ -8,6 +8,7 @@ import getThemeOption from '../../utils/get-theme-option'; import EditorContext from '../context/EditorContext'; import StylesContext from '../context/StylesContext'; import Gradient from './StylesGradient'; +import BBContrastChecker from './BBContrastChecker'; /** * Reusable color control style component @@ -24,7 +25,13 @@ const Color = ( { selector } ) => { themeConfig ); - const onChange = ( newValue, key ) => { + /** + * Function to handle the colour palette changes + * + * @param {string} newValue The value of the setting + * @param {string} key The key of the setting + */ + const onChange = debounce( ( newValue, key ) => { let config = structuredClone( userConfig ); config = set( config, @@ -32,14 +39,14 @@ const Color = ( { selector } ) => { hexToVar( newValue, themePalette ) ?? '' ); setUserConfig( config ); - }; + }, 50 ); const colorPalettes = [ 'background', 'text' ].map( ( key ) => (
{ key } onChange( value, key ) } value={ varToHex( colorStyles[ key ], themePalette ) } /> @@ -51,6 +58,10 @@ const Color = ( { selector } ) => { { __( 'Color', 'themer' ) } +
{ colorPalettes } diff --git a/src/editor/styles/components/bb-contrast-checker.scss b/src/editor/styles/components/bb-contrast-checker.scss new file mode 100644 index 00000000..e90c7e0a --- /dev/null +++ b/src/editor/styles/components/bb-contrast-checker.scss @@ -0,0 +1,82 @@ +.themer--styles__item:has(.contrast-checker) { + container-type: inline-size; + container-name: contrast-checker-outer; +} + +.contrast-checker { + align-items: center; + border: 1px solid green; + border-radius: 0.5rem; + display: flex; + gap: 1rem; + justify-content: flex-start; + padding: 0.5rem; + + transition: border-color 0.25s ease-in-out; +} + +.contrast-checker:has(p.fail) { + border-color: blue; +} + +.contrast-checker:has(p.fail + p.fail) { + border-color: red; +} + +.contrast-checker p { + margin-block: 0; +} + +.contrast-checker-title { + font-size: 0.75rem; + font-weight: bold; + text-transform: uppercase; +} + +.contrast-checker-ratio { + font-size: 1.25rem; + font-weight: bold; + margin-inline-start: auto; +} + +.contrast-checker-badges { + align-items: center; + display: flex; + gap: 0.5rem; +} + +.contrast-checker-badges > p { + background-color: green; + border-radius: 0.25rem; + color: #fff; + display: block; + padding: 0.25rem 0.5rem; + + transition: background-color 0.25s ease-in-out, + color 0.25s ease-in-out; +} + +.contrast-checker-badges > p.fail { + background-color: red; + color: #000; +} + + +@container contrast-checker-outer (max-width: 350px) { + + .contrast-checker { + align-items: center; + flex-wrap: wrap; + justify-content: space-between; + } + + .contrast-checker-title { + text-align: center; + width: 100%; + } + + .contrast-checker-ratio { + margin-inline-start: 0; + } + +} diff --git a/src/editor/styles/index.scss b/src/editor/styles/index.scss index 3b72351e..2b488a1f 100644 --- a/src/editor/styles/index.scss +++ b/src/editor/styles/index.scss @@ -10,3 +10,4 @@ @import "./components/breadcrumbs"; @import "./components/nav-list"; @import "./components/fontPicker.scss"; +@import "./components/bb-contrast-checker";