summaryrefslogtreecommitdiffstats
path: root/devtools/shared/accessibility.js
blob: 51f67172a0165add288afb64f2c31af4055d577a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
const {
  accessibility: {
    SCORES: { FAIL, AA, AAA },
  },
} = require("devtools/shared/constants");

/**
 * Mapping of text size to contrast ratio score levels
 */
const LEVELS = {
  LARGE_TEXT: { AA: 3, AAA: 4.5 },
  REGULAR_TEXT: { AA: 4.5, AAA: 7 },
};

/**
 * Mapping of large text size to CSS pixel value
 */
const LARGE_TEXT = {
  // CSS pixel value (constant) that corresponds to 14 point text size which defines large
  // text when font text is bold (font weight is greater than or equal to 600).
  BOLD_LARGE_TEXT_MIN_PIXELS: 18.66,
  // CSS pixel value (constant) that corresponds to 18 point text size which defines large
  // text for normal text (e.g. not bold).
  LARGE_TEXT_MIN_PIXELS: 24,
};

/**
 * Get contrast ratio score based on WCAG criteria.
 * @param  {Number} ratio
 *         Value of the contrast ratio for a given accessible object.
 * @param  {Boolean} isLargeText
 *         True if the accessible object contains large text.
 * @return {String}
 *         Value that represents calculated contrast ratio score.
 */
function getContrastRatioScore(ratio, isLargeText) {
  const levels = isLargeText ? LEVELS.LARGE_TEXT : LEVELS.REGULAR_TEXT;

  let score = FAIL;
  if (ratio >= levels.AAA) {
    score = AAA;
  } else if (ratio >= levels.AA) {
    score = AA;
  }

  return score;
}

/**
 * Get calculated text style properties from a node's computed style, if possible.
 * @param  {Object} computedStyle
 *         Computed style using which text styling information is to be calculated.
 *         - fontSize   {String}
 *                      Font size of the text
 *         - fontWeight {String}
 *                      Font weight of the text
 *         - color      {String}
 *                      Rgb color of the text
 *         - opacity    {String} Optional
 *                      Opacity of the text
 * @return {Object}
 *         Color and text size information for a given DOM node.
 */
function getTextProperties(computedStyle) {
  const { color, fontSize, fontWeight } = computedStyle;
  let { r, g, b, a } = colorUtils.colorToRGBA(color, true);

  // If the element has opacity in addition to background alpha value, take it
  // into account. TODO: this does not handle opacity set on ancestor elements
  // (see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1544721).
  const opacity = computedStyle.opacity
    ? parseFloat(computedStyle.opacity)
    : null;
  if (opacity) {
    a = opacity * a;
  }

  const textRgbaColor = new colorUtils.CssColor(
    `rgba(${r}, ${g}, ${b}, ${a})`,
    true
  );
  // TODO: For cases where text color is transparent, it likely comes from the color of
  // the background that is underneath it (commonly from background-clip: text
  // property). With some additional investigation it might be possible to calculate the
  // color contrast where the color of the background is used as text color and the
  // color of the ancestor's background is used as its background.
  if (textRgbaColor.isTransparent()) {
    return null;
  }

  const isBoldText = parseInt(fontWeight, 10) >= 600;
  const size = parseFloat(fontSize);
  const isLargeText =
    size >=
    (isBoldText
      ? LARGE_TEXT.BOLD_LARGE_TEXT_MIN_PIXELS
      : LARGE_TEXT.LARGE_TEXT_MIN_PIXELS);

  return {
    color: [r, g, b, a],
    isLargeText,
    isBoldText,
    size,
    opacity,
  };
}

/**
 * Calculates contrast ratio or range of contrast ratios of the referenced DOM node
 * against the given background color data. If background is multi-colored, return a
 * range, otherwise a single contrast ratio.
 *
 * @param  {Object} backgroundColorData
 *         Object with one or more of the following properties:
 *         - value              {Array}
 *                              rgba array for single color background
 *         - min                {Array}
 *                              min luminance rgba array for multi color background
 *         - max                {Array}
 *                              max luminance rgba array for multi color background
 * @param  {Object}  textData
 *         - color              {Array}
 *                              rgba array for text of referenced DOM node
 *         - isLargeText        {Boolean}
 *                              True if text of referenced DOM node is large
 * @return {Object}
 *         An object that may contain one or more of the following fields: error,
 *         isLargeText, value, min, max values for contrast.
 */
function getContrastRatioAgainstBackground(
  backgroundColorData,
  { color, isLargeText }
) {
  if (backgroundColorData.value) {
    const value = colorUtils.calculateContrastRatio(
      backgroundColorData.value,
      color
    );
    return {
      value,
      color,
      backgroundColor: backgroundColorData.value,
      isLargeText,
      score: getContrastRatioScore(value, isLargeText),
    };
  }

  let {
    min: backgroundColorMin,
    max: backgroundColorMax,
  } = backgroundColorData;
  let min = colorUtils.calculateContrastRatio(backgroundColorMin, color);
  let max = colorUtils.calculateContrastRatio(backgroundColorMax, color);

  // Flip minimum and maximum contrast ratios if necessary.
  if (min > max) {
    [min, max] = [max, min];
    [backgroundColorMin, backgroundColorMax] = [
      backgroundColorMax,
      backgroundColorMin,
    ];
  }

  const score = getContrastRatioScore(min, isLargeText);

  return {
    min,
    max,
    color,
    backgroundColorMin,
    backgroundColorMax,
    isLargeText,
    score,
    scoreMin: score,
    scoreMax: getContrastRatioScore(max, isLargeText),
  };
}

exports.getContrastRatioScore = getContrastRatioScore;
exports.getTextProperties = getTextProperties;
exports.getContrastRatioAgainstBackground = getContrastRatioAgainstBackground;
exports.LARGE_TEXT = LARGE_TEXT;