Skip to content

Commit

Permalink
fix(color-contrast): ignore zero width characters (#4103)
Browse files Browse the repository at this point in the history
* fix(color-contrast): ignore zero width characters

* fix

* replace affirm font with purpose built zero width 0 char font

* Update lib/commons/text/is-icon-ligature.js

Co-authored-by: Dan Bjorge <[email protected]>

---------

Co-authored-by: Dan Bjorge <[email protected]>
  • Loading branch information
straker and dbjorge authored Aug 7, 2023
1 parent 080cc1b commit 4deb0a0
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 57 deletions.
16 changes: 10 additions & 6 deletions lib/commons/text/is-icon-ligature.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,7 @@ export default function isIconLigature(

// keep track of each font encountered and the number of times it shows up
// as a ligature.
if (!cache.get('fonts')) {
cache.set('fonts', {});
}
const fonts = cache.get('fonts');

const fonts = cache.get('fonts', () => ({}));
const style = window.getComputedStyle(textVNode.parent.actualNode);
const fontFamily = style.getPropertyValue('font-family');

Expand All @@ -109,7 +105,7 @@ export default function isIconLigature(
}
const font = fonts[fontFamily];

// improve the performance by only comparing the image data of a fon a certain number of times
// improve the performance by only comparing the image data of a font a certain number of times
// NOTE: This MIGHT cause an issue if someone uses an icon font to render actual text.
// We're leaving this as-is, unless someone reports a false positive over it.
if (font.occurrences >= occurrenceThreshold) {
Expand Down Expand Up @@ -143,6 +139,14 @@ export default function isIconLigature(
const firstChar = nodeValue.charAt(0);
let width = canvasContext.measureText(firstChar).width;

// we already checked for typical zero-width unicode formatting characters further up,
// so we assume that any remaining zero-width characters are part of an icon ligature
// @see https://github.com/dequelabs/axe-core/issues/3918
if (width === 0) {
font.numLigatures++;
return true;
}

// ensure font meets the 30px width requirement (30px font-size doesn't
// necessarily mean its 30px wide when drawn)
if (width < 30) {
Expand Down
Binary file added test/assets/ZeroWidth0Char.woff
Binary file not shown.
118 changes: 67 additions & 51 deletions test/commons/text/is-icon-ligature.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,73 @@
describe('text.isIconLigature', function () {
describe('text.isIconLigature', () => {
'use strict';

var isIconLigature = axe.commons.text.isIconLigature;
var queryFixture = axe.testUtils.queryFixture;
var fontApiSupport = !!document.fonts;
const isIconLigature = axe.commons.text.isIconLigature;
const queryFixture = axe.testUtils.queryFixture;
const fontApiSupport = !!document.fonts;

before(function (done) {
before(done => {
if (!fontApiSupport) {
done();
}

var firaFont = new FontFace(
const firaFont = new FontFace(
'Fira Code',
'url(/test/assets/FiraCode-Regular.woff)'
);
var ligatureFont = new FontFace(
const ligatureFont = new FontFace(
'LigatureSymbols',
'url(/test/assets/LigatureSymbols.woff)'
);
var materialFont = new FontFace(
const materialFont = new FontFace(
'Material Icons',
'url(/test/assets/MaterialIcons.woff2)'
);
var robotoFont = new FontFace('Roboto', 'url(/test/assets/Roboto.woff2)');
const robotoFont = new FontFace('Roboto', 'url(/test/assets/Roboto.woff2)');
const zeroWidth0CharFont = new FontFace(
'ZeroWidth0Char',
'url(/test/assets/ZeroWidth0Char.woff)'
);

window.Promise.all([
firaFont.load(),
ligatureFont.load(),
materialFont.load(),
robotoFont.load()
]).then(function () {
robotoFont.load(),
zeroWidth0CharFont.load()
]).then(() => {
document.fonts.add(firaFont);
document.fonts.add(ligatureFont);
document.fonts.add(materialFont);
document.fonts.add(robotoFont);
document.fonts.add(zeroWidth0CharFont);
done();
});
});

it('should return false for normal text', function () {
var target = queryFixture('<div id="target">Normal text</div>');
it('should return false for normal text', () => {
const target = queryFixture('<div id="target">Normal text</div>');
assert.isFalse(isIconLigature(target.children[0]));
});

it('should return false for emoji', function () {
var target = queryFixture('<div id="target">🌎</div>');
it('should return false for emoji', () => {
const target = queryFixture('<div id="target">🌎</div>');
assert.isFalse(isIconLigature(target.children[0]));
});

it('should return false for non-bmp unicode', function () {
var target = queryFixture('<div id="target">◓</div>');
it('should return false for non-bmp unicode', () => {
const target = queryFixture('<div id="target">◓</div>');
assert.isFalse(isIconLigature(target.children[0]));
});

it('should return false for whitespace strings', function () {
var target = queryFixture('<div id="target"> </div>');
it('should return false for whitespace strings', () => {
const target = queryFixture('<div id="target"> </div>');
assert.isFalse(isIconLigature(target.children[0]));
});

(fontApiSupport ? it : it.skip)(
'should return false for common ligatures (fi)',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: Roboto">figure</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -70,8 +76,8 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should return false for common ligatures (ff)',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: Roboto">ffugative</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -80,8 +86,8 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should return false for common ligatures (fl)',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: Roboto">flu shot</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -90,8 +96,8 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should return false for common ligatures (ffi)',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: Roboto">ffigure</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -100,8 +106,8 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should return false for common ligatures (ffl)',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: Roboto">fflu shot</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -110,35 +116,45 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should return true for an icon ligature',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'Material Icons\'">delete</div>'
);
assert.isTrue(isIconLigature(target.children[0]));
}
);

(fontApiSupport ? it : it.skip)('should trim the string', function () {
var target = queryFixture(
(fontApiSupport ? it : it.skip)('should trim the string', () => {
const target = queryFixture(
'<div id="target" style="font-family: Roboto"> fflu shot </div>'
);
assert.isFalse(isIconLigature(target.children[0]));
});

(fontApiSupport ? it : it.skip)(
'should return true for a font that has no character data',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'Material Icons\'">f</div>'
);
assert.isTrue(isIconLigature(target.children[0]));
}
);

(fontApiSupport ? it : it.skip)(
'should return true for a font that has zero width characters',
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'ZeroWidth0Char\'">0</div>'
);
assert.isTrue(isIconLigature(target.children[0]));
}
);

(fontApiSupport ? it : it.skip)(
'should return false for a programming text ligature',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: Fira Code">!==</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -147,8 +163,8 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should return true for an icon ligature with low pixel difference',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'Material Icons\'">keyboard_arrow_left</div>'
);
assert.isTrue(isIconLigature(target.children[0]));
Expand All @@ -157,8 +173,8 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should return true after the 3rd time the font is an icon',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'LigatureSymbols\'">delete</div>'
);

Expand All @@ -174,8 +190,8 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should return false after the 3rd time the font is not an icon',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'Roboto\'">__non-icon text__</div>'
);

Expand All @@ -189,11 +205,11 @@ describe('text.isIconLigature', function () {
}
);

describe('pixelThreshold', function () {
describe('pixelThreshold', () => {
(fontApiSupport ? it : it.skip)(
'should allow higher percent (will not flag icon ligatures)',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'LigatureSymbols\'">delete</div>'
);

Expand All @@ -204,20 +220,20 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should allow lower percent (will flag text ligatures)',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: Roboto">figure</div>'
);
assert.isTrue(isIconLigature(target.children[0], 0));
}
);
});

describe('occurrenceThreshold', function () {
describe('occurrenceThreshold', () => {
(fontApiSupport ? it : it.skip)(
'should change the number of times a font is seen before returning',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'LigatureSymbols\'">delete</div>'
);

Expand Down

0 comments on commit 4deb0a0

Please sign in to comment.