From bff87e3e0817cd331f588d96370dd7bbf86b77a6 Mon Sep 17 00:00:00 2001 From: Ned Zimmerman Date: Tue, 14 Nov 2017 23:52:14 -0400 Subject: [PATCH] Add Per Soderlind's WCAG validator. --- app/admin.php | 15 ++ .../README.md | 59 +++++ ...customizer-validate-wcag-color-contrast.js | 210 ++++++++++++++++++ 3 files changed, 284 insertions(+) create mode 100644 lib/customizer-validate-wcag-color-contrast/README.md create mode 100644 lib/customizer-validate-wcag-color-contrast/customizer-validate-wcag-color-contrast.js diff --git a/app/admin.php b/app/admin.php index 8faf4c1..09a84f4 100644 --- a/app/admin.php +++ b/app/admin.php @@ -136,3 +136,18 @@ add_action('customize_preview_init', function () { wp_enqueue_script('aldine/customizer.js', asset_path('scripts/customizer.js'), ['customize-preview'], null, true); wp_localize_script('aldine/customizer.js', 'SAGE_DIST_PATH', get_theme_file_uri() . '/dist/'); }); + +add_action('customize_controls_enqueue_scripts', function () { + $handle = 'wcag-validate-customizer-color-contrast'; + $src = get_theme_file_uri() . '/lib/customizer-validate-wcag-color-contrast/customizer-validate-wcag-color-contrast.js'; + $deps = [ 'customize-controls' ]; + wp_enqueue_script($handle, $src, $deps); + + $exports = [ + 'validate_color_contrast' => [ + 'pb_network_color_primary_fg' => [ 'pb_network_color_primary' ], + 'pb_network_color_accent_fg' => [ 'pb_network_color_accent' ], + ], + ]; + wp_scripts()->add_data($handle, 'data', sprintf('var _validateWCAGColorContrastExports = %s;', wp_json_encode($exports))); +}); diff --git a/lib/customizer-validate-wcag-color-contrast/README.md b/lib/customizer-validate-wcag-color-contrast/README.md new file mode 100644 index 0000000..00cd0dd --- /dev/null +++ b/lib/customizer-validate-wcag-color-contrast/README.md @@ -0,0 +1,59 @@ +# Validate WCAG Color Contrast for Customizer Color Control + +The validator measures the color contrast between 2 or more color controls. It will post a warning if the contrast is less than 4.5 + +BTW, if the contrast >= 7, the score is a WCAG AAA. If the contrast is between 7 and 4.5 the score is a WCAG AA. + + + +## Demo + +I've added this validator to my [customizer demo theme](https://github.com/soderlind/2016-customizer-demo). + +## Installing the validator + +Clone this repository and include the [javascript code](customizer-validate-wcag-color-contrast.js): + +```php +/** + * Enqueue customizer control scripts. + */ +add_action( 'customize_controls_enqueue_scripts', 'on_customize_controls_enqueue_scripts' ); + +function on_customize_controls_enqueue_scripts() { + $handle = 'wcag-validate-customizer-color-contrast'; + $src = get_stylesheet_directory_uri() . '/js/customizer-validate-wcag-color-contrast.js'; + $deps = [ 'customize-controls' ]; + wp_enqueue_script( $handle, $src, $deps ); + + $exports = [ + 'validate_color_contrast' => [ + // key = current color control , values = array with color controls to check color contrast against + 'page_background_color' => [ 'main_text_color', 'secondary_text_color' ], + 'main_text_color' => [ 'page_background_color' ], + 'secondary_text_color' => [ 'page_background_color' ], + ], + ]; + wp_scripts()->add_data( $handle, 'data', sprintf( 'var _validateWCAGColorContrastExports = %s;', wp_json_encode( $exports ) ) ); +} +``` + +**Note:** You have to add color control setting ids to the `validate_color_contrast` above. See inline comment. + +## Credits ## + +- [WCAG contrast ratio measurement and scoring](https://github.com/tmcw/wcag-contrast) - Copyright (c) 2017, Tom MacWright. All rights reserved. +- [hex-rgb](https://github.com/sindresorhus/hex-rgb) - Copyright (c) Sindre Sorhus +- [relative-luminance](https://github.com/tmcw/relative-luminance) + + + +## Copyright and License + +The Validate WCAG Color Contrast for Customizer Color Control is copyright 2017 Per Soderlind + +The Validate WCAG Color Contrast for Customizer Color Control is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. + +The Validate WCAG Color Contrast for Customizer Color Control is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with the Extension. If not, see http://www.gnu.org/licenses/. diff --git a/lib/customizer-validate-wcag-color-contrast/customizer-validate-wcag-color-contrast.js b/lib/customizer-validate-wcag-color-contrast/customizer-validate-wcag-color-contrast.js new file mode 100644 index 0000000..1c32db3 --- /dev/null +++ b/lib/customizer-validate-wcag-color-contrast/customizer-validate-wcag-color-contrast.js @@ -0,0 +1,210 @@ +/* global wp, _validateWCAGColorContrastExports */ +/* exported validateWCAGColorContrast */ +var validateWCAGColorContrast = ( function( $, api, exports ) { + var self = { + validate_color_contrast: [] + }; + if ( exports ) { + $.extend( self, exports ); + } + /** + * Add contrast validation to a control if it is entitled (is a valid color control). + * + * @param {wp.customize.Control} setting - Control. + * @param {wp.customize.Value} setting.validationMessage - Validation message. + * @return {boolean} Whether validation was added. + */ + self.addWCAGColorContrastValidation = function( setting ) { + var initialValidate; + + if ( ! self.isColorControl( setting ) ) { + return false; + } + initialValidate = setting.validate; + + /** + * Wrap the setting's validate() method to do validation on the value to be sent to the server. + * + * @param {mixed} value - New value being assigned to the setting. + * @returns {*} + */ + setting.validate = function( value ) { + var setting = this, title, validationError; + var current_color = value; + var current_id = this.id; + + var all_color_controls = _.union( _.flatten( _.values( self.validate_color_contrast ) ) ); + + // remove other (old) notifications + _.each ( _.without ( all_color_controls , current_id ), function( other_color_control_id ) { + var other_control = api.control.instance( other_color_control_id ); + notice = other_control.container.find('.notice'); + notice.hide(); + } ); + + // find other color controls and check contrast with current color control + var other_color_controls = self.validate_color_contrast[ current_id ]; + + _.each ( other_color_controls, function( other_color_control_id ) { + var other_control = api.control.instance( other_color_control_id); + var other_color = other_control.container.find('.color-picker-hex').val(); + var name = $( '#customize-control-' + other_color_control_id + ' .customize-control-title').text(); + var contrast = self.hex( current_color, other_color ); + var score = self.score( contrast ); + + // contrast >= 7 ? "AAA" : contrast >= 4.5 ? "AA" : "" + if ( contrast < 4.5 ) { + setting.notifications.remove( other_color_control_id ); + validationWarning = new api.Notification( other_color_control_id, { message: self.sprintf( 'WCAG conflict with "%s"
contrast: %s' ,name, contrast), type: 'warning' } ); + setting.notifications.add( validationWarning.code, validationWarning ); + // console.log( color_control_id + ' ' + color + ' ' + contrast + ' ' + score ); + } else { + setting.notifications.remove( other_color_control_id ); + } + } ); + + return value; + }; + + return true; + }; + + /** + * Return whether the setting is entitled (i.e. if it is a title or has a title). + * + * @param {wp.customize.Setting} setting - Setting. + * @returns {boolean} + */ + self.isColorControl = function( setting ) { + return _.findKey( self.validate_color_contrast, function( key, value ) { + return value == setting.id; + } ); + }; + + api.bind( 'add', function( setting ) { + self.addWCAGColorContrastValidation( setting ); + } ); + + + self.sprintf = function( format ) { + for( var i=1; i < arguments.length; i++ ) { + format = format.replace( /%s/, arguments[i] ); + } + return format; + }; + + /** + * Methods used to calculate WCAG Color Contrast + */ + + // from https://github.com/sindresorhus/hex-rgb + self.hexRgb = function (hex) { + if (typeof hex !== 'string') { + throw new TypeError('Expected a string'); + } + + hex = hex.replace(/^#/, ''); + + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + + var num = parseInt(hex, 16); + + return [num >> 16, num >> 8 & 255, num & 255]; + }; + + // from https://github.com/tmcw/relative-luminance + // # Relative luminance + // + // http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + // https://en.wikipedia.org/wiki/Luminance_(relative) + // https://en.wikipedia.org/wiki/Luminosity_function + // https://en.wikipedia.org/wiki/Rec._709#Luma_coefficients + + // red, green, and blue coefficients + var rc = 0.2126, + gc = 0.7152, + bc = 0.0722, + // low-gamma adjust coefficient + lowc = 1 / 12.92; + + self.adjustGamma = function( g ) { + return Math.pow((g + 0.055) / 1.055, 2.4); + }; + + /** + * Given a 3-element array of R, G, B varying from 0 to 255, return the luminance + * as a number from 0 to 1. + * @param {Array} rgb 3-element array of a color + * @returns {number} luminance, between 0 and 1 + * @example + * var luminance = require('relative-luminance'); + * var black_lum = luminance([0, 0, 0]); // 0 + */ + self.relativeLuminance = function (rgb) { + var rsrgb = rgb[0] / 255; + var gsrgb = rgb[1] / 255; + var bsrgb = rgb[2] / 255; + + var r = rsrgb <= 0.03928 ? rsrgb * lowc : self.adjustGamma(rsrgb), + g = gsrgb <= 0.03928 ? gsrgb * lowc : self.adjustGamma(gsrgb), + b = bsrgb <= 0.03928 ? bsrgb * lowc : self.adjustGamma(bsrgb); + + return r * rc + g * gc + b * bc; + }; + + + // from https://github.com/tmcw/wcag-contrast + /** + * Get the contrast ratio between two relative luminance values + * @param {number} a luminance value + * @param {number} b luminance value + * @returns {number} contrast ratio + * @example + * luminance(1, 1); // = 1 + */ + self.luminance = function(a, b) { + var l1 = Math.max(a, b); + var l2 = Math.min(a, b); + return (l1 + 0.05) / (l2 + 0.05); + }; + + /** + * Get a score for the contrast between two colors as rgb triplets + * @param {array} a + * @param {array} b + * @returns {number} contrast ratio + * @example + * rgb([0, 0, 0], [255, 255, 255]); // = 21 + */ + self.rgb = function(a, b) { + return self.luminance(self.relativeLuminance(a), self.relativeLuminance(b)); + }; + + /** + * Get a score for the contrast between two colors as hex strings + * @param {string} a hex value + * @param {string} b hex value + * @returns {number} contrast ratio + * @example + * hex('#000', '#fff'); // = 21 + */ + self.hex = function(a, b) { + return self.rgb(self.hexRgb(a), self.hexRgb(b)); + }; + + /** + * Get a textual score from a numeric contrast value + * @param {number} contrast + * @returns {string} score + * @example + * score(10); // = 'AAA' + */ + self.score = function(contrast) { + return contrast >= 7 ? "AAA" : contrast >= 4.5 ? "AA" : ""; + }; + + return self; + +}( jQuery, wp.customize, _validateWCAGColorContrastExports ) );