Ned Zimmerman
7 years ago
3 changed files with 284 additions and 0 deletions
@ -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. |
||||
|
||||
<img src="assets/wcag-color-contrast.gif" width="650" /> |
||||
|
||||
## 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/. |
@ -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"<br/>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<number>} 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 ) ); |
Loading…
Reference in new issue