MDL-59817 atto_accessibilitychecker: Handle transparency properly

Some browsers, notably Firefox, do not return the computed style for
background colour in a computed RGB format. Instead they return the RGBA
where the alpha channel is set to fully transparent.

To solve this we need to work up the hierarchy and compute the
background colour for each parent node until we reach full alpha (1).

We can use a standard calculation to approximate the value for the
resultant element background by multiplying the alpha of the current
transparent (or semi-transparent) node with the R, G, or B channel in
question, and that of the parent node's background colour.

There are cases where this will not be 100% accurate - notably where
there is some additional content in addition to the parent background,
but this gives us a reasoable approximation for the majority of cases.
Additionally the code has never considered the full set of node content
when calculating this information.
This commit is contained in:
Andrew Nicols 2020-01-07 17:28:10 +08:00
parent 77d1c41502
commit 43aa3cbe44
4 changed files with 133 additions and 10 deletions

View File

@ -129,8 +129,11 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
// Check for non-empty text.
if (Y.Lang.trim(node.get('text')) !== '') {
foreground = node.getComputedStyle('color');
background = node.getComputedStyle('backgroundColor');
foreground = Y.Color.fromArray(
this._getComputedBackgroundColor(node, node.getComputedStyle('color')),
Y.Color.TYPES.RGBA
);
background = Y.Color.fromArray(this._getComputedBackgroundColor(node), Y.Color.TYPES.RGBA);
lum1 = this._getLuminanceFromCssColor(foreground);
lum2 = this._getLuminanceFromCssColor(background);
@ -239,7 +242,7 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
* Generate the HTML that lists the found warnings.
*
* @method _addWarnings
* @param {Node} A Node to append the html to.
* @param {Node} list Node to append the html to.
* @param {String} description Description of this failure.
* @param {array} nodes An array of failing nodes.
* @param {boolean} imagewarnings true if the warnings are related to images, false if text.
@ -309,6 +312,44 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
b1 = part1(color[2]);
return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1;
},
/**
* Get the computed RGB converted to full alpha value, considering the node hierarchy.
*
* @method _getComputedBackgroundColor
* @param {Node} node
* @param {String} color The initial colour. If not specified, fetches the backgroundColor from the node.
* @return {Array} Colour in Array form (RGBA)
* @private
*/
_getComputedBackgroundColor: function(node, color) {
color = color || node.getComputedStyle('backgroundColor');
if (color.toLowerCase() === 'transparent') {
// Y.Color doesn't handle 'transparent' properly.
color = 'rgba(1, 1, 1, 0)';
}
// Convert the colour to its constituent parts in RGBA format, then fetch the alpha.
var colorParts = Y.Color.toArray(color);
var alpha = colorParts[3];
if (alpha === 1) {
// If the alpha of the background is already 1, then the parent background colour does not change anything.
return colorParts;
}
// Fetch the computed background colour of the parent and use it to calculate the RGB of this item.
var parentColor = this._getComputedBackgroundColor(node.get('parentNode'));
return [
// RGB = (alpha * R|G|B) + (1 - alpha * solid parent colour).
(1 - alpha) * parentColor[0] + alpha * colorParts[0],
(1 - alpha) * parentColor[1] + alpha * colorParts[1],
(1 - alpha) * parentColor[2] + alpha * colorParts[2],
// We always return a colour with full alpha.
1
];
}
});

View File

@ -1 +1 @@
YUI.add("moodle-atto_accessibilitychecker-button",function(e,t){var n="atto_accessibilitychecker";e.namespace("M.atto_accessibilitychecker").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{initializer:function(){this.addButton({icon:"e/accessibility_checker",callback:this._displayDialogue})},_displayDialogue:function(){var e=this.getDialogue({headerContent:M.util.get_string("pluginname",n),width:"500px",focusAfterHide:!0});e.set("bodyContent",this._getDialogueContent()).show()},_getDialogueContent:function(){var t=e.Node.create('<div style="word-wrap: break-word;"></div>');return t.append(this._getWarnings()),t.delegate("click",function(e){e.preventDefault();var t=this.get("host"),n=e.currentTarget.getData("sourceNode"),r=this.getDialogue();n?(r.set("focusAfterHide",this.editor).hide(),t.setSelection(t.getSelectionFromNode(n))):r.hide()},"a",this),t},_getWarnings:function(){var t,r=e.Node.create("<div></div>");return t=[],this.editor.all("img").each(function(e){var n=e.getAttribute("alt");(typeof n=="undefined"||n==="")&&e.getAttribute("role")!=="presentation"&&t.push(e)},this),this._addWarnings(r,M.util.get_string("imagesmissingalt",n),t,!0),t=[],this.editor.all("*").each(function(n){var r,i,s,o,u;if(e.Lang.trim(n.get("text"))!==""){r=n.getComputedStyle("color"),i=n.getComputedStyle("backgroundColor"),o=this._getLuminanceFromCssColor(r),u=this._getLuminanceFromCssColor(i),o>u?s=(o+.05)/(u+.05):s=(u+.05)/(o+.05);if(s<=4.5){var a=0,f=!1;for(a=0;a<t.length;a++){if(n.ancestors("*").indexOf(t[a])!==-1){f=!0;break}if(t[a].ancestors("*").indexOf(n)!==-1){t[a]=n,f=!0;break}}f||t.push(n)}}},this),this._addWarnings(r,M.util.get_string("needsmorecontrast",n),t,!1),this.editor.get("text").length>1e3&&!this.editor.one("h3, h4, h5")&&this._addWarnings(r,M.util.get_string("needsmoreheadings",n),[this.editor],!1),t=[],this.editor.all("table").each(function(e){var n=e.one("caption");(n===null||n.get("text").trim()==="")&&t.push(e)},this),this._addWarnings(r,M.util.get_string("tablesmissingcaption",n),t,!1),t=[],this.editor.all("table").each(function(e){var n=e.one("[colspan],[rowspan]");n!==null&&t.push(e)},this),this._addWarnings(r,M.util.get_string("tableswithmergedcells",n),t,!1),t=[],this.editor.all("table").each(function(e){if(e.one("tr").one("td"))e.all("tr").some(function(n){var r=n.one("th");return!r||r.get("text").trim()===""?(t.push(e),!0):!1},this);else{var n=!1;e.one("tr").all("th").some(function(r){return n=!0,r.get("text").trim()===""?(t.push(e),!0):!1}),n||t.push(e)}},this),this._addWarnings(r,M.util.get_string("tablesmissingheaders",n),t,!1),r.hasChildNodes()||r.append("<p>"+M.util.get_string("nowarnings",n)+"</p>"),r},_addWarnings:function(t,r,i,s){var o,u,a,f,l,c,h,p;if(i.length>0){o=e.Node.create("<p>"+r+"</p>"),u=e.Node.create('<ol class="accessibilitywarnings"></ol>'),a=0;for(a=0;a<i.length;a++)c=e.Node.create("<li></li>"),s?(f=i[a].getAttribute("src"),h=e.Node.create('<a href="#"><img src="'+f+'" /> '+f+"</a>")):(l="innerText"in i[a]?"innerText":"textContent",p=i[a].get(l).trim(),p===""&&(p=M.util.get_string("emptytext",n)),i[a]===this.editor&&(p=M.util.get_string("entiredocument",n)),h=e.Node.create('<a href="#">'+p+"</a>")),h.setData("sourceNode",i[a]),c.append(h),u.append(c);o.append(u),t.append(o)}},_getLuminanceFromCssColor:function(t){var n;t==="transparent"&&(t="#ffffff"),n=e.Color.toArray(e.Color.toRGB(t));var r=function(e){return e=parseInt(e,10)/255,e<=.03928?e/=12.92:e=Math.pow((e+.055)/1.055,2.4),e},i=r(n[0]),s=r(n[1]),o=r(n[2]);return.2126*i+.7152*s+.0722*o}})},"@VERSION@",{requires:["color-base","moodle-editor_atto-plugin"]});
YUI.add("moodle-atto_accessibilitychecker-button",function(e,t){var n="atto_accessibilitychecker";e.namespace("M.atto_accessibilitychecker").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{initializer:function(){this.addButton({icon:"e/accessibility_checker",callback:this._displayDialogue})},_displayDialogue:function(){var e=this.getDialogue({headerContent:M.util.get_string("pluginname",n),width:"500px",focusAfterHide:!0});e.set("bodyContent",this._getDialogueContent()).show()},_getDialogueContent:function(){var t=e.Node.create('<div style="word-wrap: break-word;"></div>');return t.append(this._getWarnings()),t.delegate("click",function(e){e.preventDefault();var t=this.get("host"),n=e.currentTarget.getData("sourceNode"),r=this.getDialogue();n?(r.set("focusAfterHide",this.editor).hide(),t.setSelection(t.getSelectionFromNode(n))):r.hide()},"a",this),t},_getWarnings:function(){var t,r=e.Node.create("<div></div>");return t=[],this.editor.all("img").each(function(e){var n=e.getAttribute("alt");(typeof n=="undefined"||n==="")&&e.getAttribute("role")!=="presentation"&&t.push(e)},this),this._addWarnings(r,M.util.get_string("imagesmissingalt",n),t,!0),t=[],this.editor.all("*").each(function(n){var r,i,s,o,u;if(e.Lang.trim(n.get("text"))!==""){r=e.Color.fromArray(this._getComputedBackgroundColor(n,n.getComputedStyle("color")),e.Color.TYPES.RGBA),i=e.Color.fromArray(this._getComputedBackgroundColor(n),e.Color.TYPES.RGBA),o=this._getLuminanceFromCssColor(r),u=this._getLuminanceFromCssColor(i),o>u?s=(o+.05)/(u+.05):s=(u+.05)/(o+.05);if(s<=4.5){var a=0,f=!1;for(a=0;a<t.length;a++){if(n.ancestors("*").indexOf(t[a])!==-1){f=!0;break}if(t[a].ancestors("*").indexOf(n)!==-1){t[a]=n,f=!0;break}}f||t.push(n)}}},this),this._addWarnings(r,M.util.get_string("needsmorecontrast",n),t,!1),this.editor.get("text").length>1e3&&!this.editor.one("h3, h4, h5")&&this._addWarnings(r,M.util.get_string("needsmoreheadings",n),[this.editor],!1),t=[],this.editor.all("table").each(function(e){var n=e.one("caption");(n===null||n.get("text").trim()==="")&&t.push(e)},this),this._addWarnings(r,M.util.get_string("tablesmissingcaption",n),t,!1),t=[],this.editor.all("table").each(function(e){var n=e.one("[colspan],[rowspan]");n!==null&&t.push(e)},this),this._addWarnings(r,M.util.get_string("tableswithmergedcells",n),t,!1),t=[],this.editor.all("table").each(function(e){if(e.one("tr").one("td"))e.all("tr").some(function(n){var r=n.one("th");return!r||r.get("text").trim()===""?(t.push(e),!0):!1},this);else{var n=!1;e.one("tr").all("th").some(function(r){return n=!0,r.get("text").trim()===""?(t.push(e),!0):!1}),n||t.push(e)}},this),this._addWarnings(r,M.util.get_string("tablesmissingheaders",n),t,!1),r.hasChildNodes()||r.append("<p>"+M.util.get_string("nowarnings",n)+"</p>"),r},_addWarnings:function(t,r,i,s){var o,u,a,f,l,c,h,p;if(i.length>0){o=e.Node.create("<p>"+r+"</p>"),u=e.Node.create('<ol class="accessibilitywarnings"></ol>'),a=0;for(a=0;a<i.length;a++)c=e.Node.create("<li></li>"),s?(f=i[a].getAttribute("src"),h=e.Node.create('<a href="#"><img src="'+f+'" /> '+f+"</a>")):(l="innerText"in i[a]?"innerText":"textContent",p=i[a].get(l).trim(),p===""&&(p=M.util.get_string("emptytext",n)),i[a]===this.editor&&(p=M.util.get_string("entiredocument",n)),h=e.Node.create('<a href="#">'+p+"</a>")),h.setData("sourceNode",i[a]),c.append(h),u.append(c);o.append(u),t.append(o)}},_getLuminanceFromCssColor:function(t){var n;t==="transparent"&&(t="#ffffff"),n=e.Color.toArray(e.Color.toRGB(t));var r=function(e){return e=parseInt(e,10)/255,e<=.03928?e/=12.92:e=Math.pow((e+.055)/1.055,2.4),e},i=r(n[0]),s=r(n[1]),o=r(n[2]);return.2126*i+.7152*s+.0722*o},_getComputedBackgroundColor:function(t,n){n=n||t.getComputedStyle("backgroundColor"),n.toLowerCase()==="transparent"&&(n="rgba(1, 1, 1, 0)");var r=e.Color.toArray(n),i=r[3];if(i===1)return r;var s=this._getComputedBackgroundColor(t.get("parentNode"));return[(1-i)*s[0]+i*r[0],(1-i)*s[1]+i*r[1],(1-i)*s[2]+i*r[2],1]}})},"@VERSION@",{requires:["color-base","moodle-editor_atto-plugin"]});

View File

@ -129,8 +129,11 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
// Check for non-empty text.
if (Y.Lang.trim(node.get('text')) !== '') {
foreground = node.getComputedStyle('color');
background = node.getComputedStyle('backgroundColor');
foreground = Y.Color.fromArray(
this._getComputedBackgroundColor(node, node.getComputedStyle('color')),
Y.Color.TYPES.RGBA
);
background = Y.Color.fromArray(this._getComputedBackgroundColor(node), Y.Color.TYPES.RGBA);
lum1 = this._getLuminanceFromCssColor(foreground);
lum2 = this._getLuminanceFromCssColor(background);
@ -234,7 +237,7 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
* Generate the HTML that lists the found warnings.
*
* @method _addWarnings
* @param {Node} A Node to append the html to.
* @param {Node} list Node to append the html to.
* @param {String} description Description of this failure.
* @param {array} nodes An array of failing nodes.
* @param {boolean} imagewarnings true if the warnings are related to images, false if text.
@ -304,6 +307,44 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
b1 = part1(color[2]);
return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1;
},
/**
* Get the computed RGB converted to full alpha value, considering the node hierarchy.
*
* @method _getComputedBackgroundColor
* @param {Node} node
* @param {String} color The initial colour. If not specified, fetches the backgroundColor from the node.
* @return {Array} Colour in Array form (RGBA)
* @private
*/
_getComputedBackgroundColor: function(node, color) {
color = color || node.getComputedStyle('backgroundColor');
if (color.toLowerCase() === 'transparent') {
// Y.Color doesn't handle 'transparent' properly.
color = 'rgba(1, 1, 1, 0)';
}
// Convert the colour to its constituent parts in RGBA format, then fetch the alpha.
var colorParts = Y.Color.toArray(color);
var alpha = colorParts[3];
if (alpha === 1) {
// If the alpha of the background is already 1, then the parent background colour does not change anything.
return colorParts;
}
// Fetch the computed background colour of the parent and use it to calculate the RGB of this item.
var parentColor = this._getComputedBackgroundColor(node.get('parentNode'));
return [
// RGB = (alpha * R|G|B) + (1 - alpha * solid parent colour).
(1 - alpha) * parentColor[0] + alpha * colorParts[0],
(1 - alpha) * parentColor[1] + alpha * colorParts[1],
(1 - alpha) * parentColor[2] + alpha * colorParts[2],
// We always return a colour with full alpha.
1
];
}
});

View File

@ -127,8 +127,11 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
// Check for non-empty text.
if (Y.Lang.trim(node.get('text')) !== '') {
foreground = node.getComputedStyle('color');
background = node.getComputedStyle('backgroundColor');
foreground = Y.Color.fromArray(
this._getComputedBackgroundColor(node, node.getComputedStyle('color')),
Y.Color.TYPES.RGBA
);
background = Y.Color.fromArray(this._getComputedBackgroundColor(node), Y.Color.TYPES.RGBA);
lum1 = this._getLuminanceFromCssColor(foreground);
lum2 = this._getLuminanceFromCssColor(background);
@ -237,7 +240,7 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
* Generate the HTML that lists the found warnings.
*
* @method _addWarnings
* @param {Node} A Node to append the html to.
* @param {Node} list Node to append the html to.
* @param {String} description Description of this failure.
* @param {array} nodes An array of failing nodes.
* @param {boolean} imagewarnings true if the warnings are related to images, false if text.
@ -307,5 +310,43 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
b1 = part1(color[2]);
return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1;
},
/**
* Get the computed RGB converted to full alpha value, considering the node hierarchy.
*
* @method _getComputedBackgroundColor
* @param {Node} node
* @param {String} color The initial colour. If not specified, fetches the backgroundColor from the node.
* @return {Array} Colour in Array form (RGBA)
* @private
*/
_getComputedBackgroundColor: function(node, color) {
color = color || node.getComputedStyle('backgroundColor');
if (color.toLowerCase() === 'transparent') {
// Y.Color doesn't handle 'transparent' properly.
color = 'rgba(1, 1, 1, 0)';
}
// Convert the colour to its constituent parts in RGBA format, then fetch the alpha.
var colorParts = Y.Color.toArray(color);
var alpha = colorParts[3];
if (alpha === 1) {
// If the alpha of the background is already 1, then the parent background colour does not change anything.
return colorParts;
}
// Fetch the computed background colour of the parent and use it to calculate the RGB of this item.
var parentColor = this._getComputedBackgroundColor(node.get('parentNode'));
return [
// RGB = (alpha * R|G|B) + (1 - alpha * solid parent colour).
(1 - alpha) * parentColor[0] + alpha * colorParts[0],
(1 - alpha) * parentColor[1] + alpha * colorParts[1],
(1 - alpha) * parentColor[2] + alpha * colorParts[2],
// We always return a colour with full alpha.
1
];
}
});