govuk_publishing_components 21.22.1 → 21.22.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/app/assets/stylesheets/govuk_publishing_components/components/_breadcrumbs.scss +34 -0
  4. data/app/assets/stylesheets/govuk_publishing_components/components/_document-list.scss +2 -2
  5. data/app/views/govuk_publishing_components/components/_breadcrumbs.html.erb +0 -3
  6. data/app/views/govuk_publishing_components/components/_contextual_breadcrumbs.html.erb +0 -2
  7. data/lib/govuk_publishing_components/version.rb +1 -1
  8. data/node_modules/axe-core/CHANGELOG.md +70 -10
  9. data/node_modules/axe-core/axe.js +1295 -632
  10. data/node_modules/axe-core/axe.min.js +2 -2
  11. data/node_modules/axe-core/bower.json +1 -1
  12. data/node_modules/axe-core/doc/API.md +1 -0
  13. data/node_modules/axe-core/doc/aria-supported.md +0 -1
  14. data/node_modules/axe-core/doc/developer-guide.md +2 -2
  15. data/node_modules/axe-core/doc/rule-descriptions.md +91 -87
  16. data/node_modules/axe-core/lib/checks/aria/abstractrole.js +10 -1
  17. data/node_modules/axe-core/lib/checks/aria/abstractrole.json +4 -1
  18. data/node_modules/axe-core/lib/checks/aria/fallbackrole.js +1 -0
  19. data/node_modules/axe-core/lib/checks/aria/fallbackrole.json +11 -0
  20. data/node_modules/axe-core/lib/checks/aria/invalidrole.js +14 -3
  21. data/node_modules/axe-core/lib/checks/aria/invalidrole.json +4 -1
  22. data/node_modules/axe-core/lib/checks/aria/required-children.js +41 -30
  23. data/node_modules/axe-core/lib/checks/aria/valid-attr-value.js +24 -9
  24. data/node_modules/axe-core/lib/checks/aria/valid-attr-value.json +2 -2
  25. data/node_modules/axe-core/lib/checks/color/color-contrast.js +23 -7
  26. data/node_modules/axe-core/lib/checks/color/color-contrast.json +7 -1
  27. data/node_modules/axe-core/lib/checks/keyboard/focusable-disabled.js +5 -0
  28. data/node_modules/axe-core/lib/checks/keyboard/focusable-modal-open.js +14 -0
  29. data/node_modules/axe-core/lib/checks/keyboard/focusable-modal-open.json +11 -0
  30. data/node_modules/axe-core/lib/checks/keyboard/focusable-not-tabbable.js +5 -0
  31. data/node_modules/axe-core/lib/checks/keyboard/page-has-elm.js +5 -1
  32. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate-after.js +2 -0
  33. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate-banner.json +1 -0
  34. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate-contentinfo.json +1 -0
  35. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate-main.json +1 -0
  36. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate.js +14 -2
  37. data/node_modules/axe-core/lib/checks/lists/only-listitems.js +43 -49
  38. data/node_modules/axe-core/lib/checks/lists/only-listitems.json +4 -1
  39. data/node_modules/axe-core/lib/checks/media/no-autoplay-audio.js +93 -0
  40. data/node_modules/axe-core/lib/checks/media/no-autoplay-audio.json +15 -0
  41. data/node_modules/axe-core/lib/checks/navigation/identical-links-same-purpose-after.js +100 -0
  42. data/node_modules/axe-core/lib/checks/navigation/identical-links-same-purpose.js +31 -0
  43. data/node_modules/axe-core/lib/checks/navigation/identical-links-same-purpose.json +12 -0
  44. data/node_modules/axe-core/lib/checks/navigation/region.js +42 -8
  45. data/node_modules/axe-core/lib/checks/navigation/region.json +0 -1
  46. data/node_modules/axe-core/lib/checks/shared/svg-non-empty-title.js +4 -0
  47. data/node_modules/axe-core/lib/checks/shared/svg-non-empty-title.json +11 -0
  48. data/node_modules/axe-core/lib/commons/aria/index.js +12 -4
  49. data/node_modules/axe-core/lib/commons/color/get-background-color.js +5 -157
  50. data/node_modules/axe-core/lib/commons/dom/get-element-stack.js +272 -174
  51. data/node_modules/axe-core/lib/commons/dom/is-focusable.js +3 -1
  52. data/node_modules/axe-core/lib/commons/dom/is-hidden-with-css.js +2 -2
  53. data/node_modules/axe-core/lib/commons/dom/is-modal-open.js +98 -0
  54. data/node_modules/axe-core/lib/commons/dom/is-visible.js +57 -2
  55. data/node_modules/axe-core/lib/commons/dom/url-props-from-attribute.js +143 -0
  56. data/node_modules/axe-core/lib/commons/dom/visually-contains.js +62 -12
  57. data/node_modules/axe-core/lib/commons/matches/attributes.js +12 -8
  58. data/node_modules/axe-core/lib/commons/matches/from-definition.js +15 -10
  59. data/node_modules/axe-core/lib/commons/matches/index.js +11 -9
  60. data/node_modules/axe-core/lib/commons/matches/node-name.js +11 -21
  61. data/node_modules/axe-core/lib/commons/matches/properties.js +12 -9
  62. data/node_modules/axe-core/lib/commons/text/unicode.js +27 -24
  63. data/node_modules/axe-core/lib/core/base/virtual-node/virtual-node.js +11 -3
  64. data/node_modules/axe-core/lib/core/constants.js +1 -1
  65. data/node_modules/axe-core/lib/core/reporters/v1.js +6 -3
  66. data/node_modules/axe-core/lib/core/utils/contains.js +1 -1
  67. data/node_modules/axe-core/lib/core/utils/is-hidden.js +6 -6
  68. data/node_modules/axe-core/lib/core/utils/matches.js +263 -0
  69. data/node_modules/axe-core/lib/core/utils/preload-media.js +65 -0
  70. data/node_modules/axe-core/lib/core/utils/preload.js +33 -24
  71. data/node_modules/axe-core/lib/core/utils/qsa.js +7 -208
  72. data/node_modules/axe-core/lib/rules/aria-hidden-focus.json +5 -1
  73. data/node_modules/axe-core/lib/rules/aria-roles.json +1 -1
  74. data/node_modules/axe-core/lib/rules/button-name.json +1 -1
  75. data/node_modules/axe-core/lib/rules/color-contrast-matches.js +1 -1
  76. data/node_modules/axe-core/lib/rules/color-contrast.json +0 -3
  77. data/node_modules/axe-core/lib/rules/html-namespace-matches.js +1 -0
  78. data/node_modules/axe-core/lib/rules/identical-links-same-purpose-matches.js +13 -0
  79. data/node_modules/axe-core/lib/rules/identical-links-same-purpose.json +14 -0
  80. data/node_modules/axe-core/lib/rules/landmark-no-duplicate-banner.json +1 -1
  81. data/node_modules/axe-core/lib/rules/landmark-no-duplicate-contentinfo.json +1 -1
  82. data/node_modules/axe-core/lib/rules/landmark-no-duplicate-main.json +12 -0
  83. data/node_modules/axe-core/lib/rules/landmark-one-main.json +2 -2
  84. data/node_modules/axe-core/lib/rules/link-name.json +2 -4
  85. data/node_modules/axe-core/lib/rules/meta-viewport.json +1 -1
  86. data/node_modules/axe-core/lib/rules/no-autoplay-audio-matches.js +18 -0
  87. data/node_modules/axe-core/lib/rules/no-autoplay-audio.json +15 -0
  88. data/node_modules/axe-core/lib/rules/region.json +1 -2
  89. data/node_modules/axe-core/lib/rules/role-img-alt.json +2 -1
  90. data/node_modules/axe-core/lib/rules/svg-img-alt.json +24 -0
  91. data/node_modules/axe-core/lib/rules/svg-namespace-matches.js +1 -0
  92. data/node_modules/axe-core/locales/da.json +782 -0
  93. data/node_modules/axe-core/locales/fr.json +221 -42
  94. data/node_modules/axe-core/locales/ja.json +124 -24
  95. data/node_modules/axe-core/package.json +29 -26
  96. data/node_modules/axe-core/sri-history.json +26 -10
  97. metadata +27 -9
  98. data/node_modules/axe-core/doc/examples/jasmine/package-lock.json +0 -1489
  99. data/node_modules/axe-core/doc/examples/jest_react/package-lock.json +0 -7525
  100. data/node_modules/axe-core/doc/examples/mocha/package-lock.json +0 -1671
  101. data/node_modules/axe-core/doc/examples/phantomjs/package-lock.json +0 -862
  102. data/node_modules/axe-core/doc/examples/qunit/package-lock.json +0 -2951
  103. data/node_modules/axe-core/lib/checks/navigation/region-after.js +0 -1
  104. data/node_modules/axe-core/typings/axe-core/axe-core-tests.js +0 -151
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Given a rootNode
3
+ * -> get all HTMLMediaElement's and ensure their metadata is loaded
4
+ *
5
+ * @method preloadMedia
6
+ * @memberof axe.utils
7
+ * @property {Object} options.treeRoot (optional) the DOM tree to be inspected
8
+ */
9
+ axe.utils.preloadMedia = function preloadMedia({ treeRoot = axe._tree[0] }) {
10
+ const mediaVirtualNodes = axe.utils.querySelectorAllFilter(
11
+ treeRoot,
12
+ 'video, audio',
13
+ ({ actualNode }) => {
14
+ /**
15
+ * this is to safe-gaurd against empty `src` values which can get resolved `window.location`, thus never preloading as the URL is not a media asset
16
+ */
17
+ if (actualNode.hasAttribute('src')) {
18
+ return !!actualNode.getAttribute('src');
19
+ }
20
+
21
+ /**
22
+ * The `src` on <source> element is essential for `audio` and `video` elements
23
+ */
24
+ const sourceWithSrc = Array.from(
25
+ actualNode.getElementsByTagName('source')
26
+ ).filter(source => !!source.getAttribute('src'));
27
+ if (sourceWithSrc.length <= 0) {
28
+ return false;
29
+ }
30
+
31
+ return true;
32
+ }
33
+ );
34
+
35
+ return Promise.all(
36
+ mediaVirtualNodes.map(({ actualNode }) => isMediaElementReady(actualNode))
37
+ );
38
+ };
39
+
40
+ /**
41
+ * Ensures a media element's metadata is loaded
42
+ * @param {HTMLMediaElement} elm elm
43
+ * @returns {Promise}
44
+ */
45
+ function isMediaElementReady(elm) {
46
+ return new Promise(resolve => {
47
+ /**
48
+ * See - https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
49
+ */
50
+ if (elm.readyState > 0) {
51
+ resolve(elm);
52
+ }
53
+
54
+ function onMediaReady() {
55
+ elm.removeEventListener('loadedmetadata', onMediaReady);
56
+ resolve(elm);
57
+ }
58
+
59
+ /**
60
+ * Given media is not ready, wire up listener for `loadedmetadata`
61
+ * See - https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event
62
+ */
63
+ elm.addEventListener('loadedmetadata', onMediaReady);
64
+ });
65
+ }
@@ -66,7 +66,7 @@ axe.utils.getPreloadConfig = function getPreloadConfig(options) {
66
66
  if (
67
67
  options.preload.timeout &&
68
68
  typeof options.preload.timeout === 'number' &&
69
- !Number.isNaN(options.preload.timeout)
69
+ !isNaN(options.preload.timeout)
70
70
  ) {
71
71
  config.timeout = options.preload.timeout;
72
72
  }
@@ -81,7 +81,8 @@ axe.utils.getPreloadConfig = function getPreloadConfig(options) {
81
81
  */
82
82
  axe.utils.preload = function preload(options) {
83
83
  const preloadFunctionsMap = {
84
- cssom: axe.utils.preloadCssom
84
+ cssom: axe.utils.preloadCssom,
85
+ media: axe.utils.preloadMedia
85
86
  };
86
87
 
87
88
  const shouldPreload = axe.utils.shouldPreload(options);
@@ -96,12 +97,14 @@ axe.utils.preload = function preload(options) {
96
97
  * Start `timeout` timer for preloading assets
97
98
  * -> reject if allowed time expires.
98
99
  */
99
- setTimeout(() => reject(`Preload assets timed out.`), timeout);
100
+ const preloadTimeout = setTimeout(
101
+ () => reject(new Error(`Preload assets timed out.`)),
102
+ timeout
103
+ );
100
104
 
101
105
  /**
102
106
  * Fetch requested `assets`
103
107
  */
104
-
105
108
  Promise.all(
106
109
  assets.map(asset =>
107
110
  preloadFunctionsMap[asset](options).then(results => {
@@ -110,26 +113,32 @@ axe.utils.preload = function preload(options) {
110
113
  };
111
114
  })
112
115
  )
113
- ).then(results => {
114
- /**
115
- * Combine array of results into an object map
116
- *
117
- * From ->
118
- * [{cssom: [...], aom: [...]}]
119
- * To ->
120
- * {
121
- * cssom: [...]
122
- * aom: [...]
123
- * }
124
- */
125
- const preloadAssets = results.reduce((out, result) => {
126
- return {
127
- ...out,
128
- ...result
129
- };
130
- }, {});
116
+ )
117
+ .then(results => {
118
+ /**
119
+ * Combine array of results into an object map
120
+ *
121
+ * From ->
122
+ * [{cssom: [...], aom: [...]}]
123
+ * To ->
124
+ * {
125
+ * cssom: [...]
126
+ * aom: [...]
127
+ * }
128
+ */
129
+ const preloadAssets = results.reduce((out, result) => {
130
+ return {
131
+ ...out,
132
+ ...result
133
+ };
134
+ }, {});
131
135
 
132
- resolve(preloadAssets);
133
- });
136
+ clearTimeout(preloadTimeout);
137
+ resolve(preloadAssets);
138
+ })
139
+ .catch(err => {
140
+ clearTimeout(preloadTimeout);
141
+ reject(err);
142
+ });
134
143
  });
135
144
  };
@@ -1,191 +1,3 @@
1
- // The lines below is because the latedef option does not work
2
- var convertExpressions = function() {};
3
- var matchExpressions = function() {};
4
-
5
- // todo: implement an option to follow aria-owns
6
-
7
- function matchesTag(vNode, exp) {
8
- return (
9
- vNode.props.nodeType === 1 &&
10
- (exp.tag === '*' || vNode.props.nodeName === exp.tag)
11
- );
12
- }
13
-
14
- function matchesClasses(vNode, exp) {
15
- return !exp.classes || exp.classes.every(cl => vNode.hasClass(cl.value));
16
- }
17
-
18
- function matchesAttributes(vNode, exp) {
19
- return (
20
- !exp.attributes ||
21
- exp.attributes.every(att => {
22
- var nodeAtt = vNode.attr(att.key);
23
- return nodeAtt !== null && (!att.value || att.test(nodeAtt));
24
- })
25
- );
26
- }
27
-
28
- function matchesId(vNode, exp) {
29
- return !exp.id || vNode.props.id === exp.id;
30
- }
31
-
32
- function matchesPseudos(target, exp) {
33
- if (
34
- !exp.pseudos ||
35
- exp.pseudos.every(pseudo => {
36
- if (pseudo.name === 'not') {
37
- return !matchExpressions([target], pseudo.expressions, false).length;
38
- }
39
- throw new Error(
40
- 'the pseudo selector ' + pseudo.name + ' has not yet been implemented'
41
- );
42
- })
43
- ) {
44
- return true;
45
- }
46
- return false;
47
- }
48
-
49
- var escapeRegExp = (function() {
50
- /*! Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan <http://stevenlevithan.com/regex/xregexp/> MIT License */
51
- var from = /(?=[\-\[\]{}()*+?.\\\^$|,#\s])/g;
52
- var to = '\\';
53
- return function(string) {
54
- return string.replace(from, to);
55
- };
56
- })();
57
-
58
- var reUnescape = /\\/g;
59
-
60
- function convertAttributes(atts) {
61
- /*! Credit Mootools Copyright Mootools, MIT License */
62
- if (!atts) {
63
- return;
64
- }
65
- return atts.map(att => {
66
- var attributeKey = att.name.replace(reUnescape, '');
67
- var attributeValue = (att.value || '').replace(reUnescape, '');
68
- var test, regexp;
69
-
70
- switch (att.operator) {
71
- case '^=':
72
- regexp = new RegExp('^' + escapeRegExp(attributeValue));
73
- break;
74
- case '$=':
75
- regexp = new RegExp(escapeRegExp(attributeValue) + '$');
76
- break;
77
- case '~=':
78
- regexp = new RegExp(
79
- '(^|\\s)' + escapeRegExp(attributeValue) + '(\\s|$)'
80
- );
81
- break;
82
- case '|=':
83
- regexp = new RegExp('^' + escapeRegExp(attributeValue) + '(-|$)');
84
- break;
85
- case '=':
86
- test = function(value) {
87
- return attributeValue === value;
88
- };
89
- break;
90
- case '*=':
91
- test = function(value) {
92
- return value && value.includes(attributeValue);
93
- };
94
- break;
95
- case '!=':
96
- test = function(value) {
97
- return attributeValue !== value;
98
- };
99
- break;
100
- default:
101
- test = function(value) {
102
- return !!value;
103
- };
104
- }
105
-
106
- if (attributeValue === '' && /^[*$^]=$/.test(att.operator)) {
107
- test = function() {
108
- return false;
109
- };
110
- }
111
-
112
- if (!test) {
113
- test = function(value) {
114
- return value && regexp.test(value);
115
- };
116
- }
117
- return {
118
- key: attributeKey,
119
- value: attributeValue,
120
- test: test
121
- };
122
- });
123
- }
124
-
125
- function convertClasses(classes) {
126
- if (!classes) {
127
- return;
128
- }
129
- return classes.map(className => {
130
- className = className.replace(reUnescape, '');
131
-
132
- return {
133
- value: className,
134
- regexp: new RegExp('(^|\\s)' + escapeRegExp(className) + '(\\s|$)')
135
- };
136
- });
137
- }
138
-
139
- function convertPseudos(pseudos) {
140
- if (!pseudos) {
141
- return;
142
- }
143
- return pseudos.map(p => {
144
- var expressions;
145
-
146
- if (p.name === 'not') {
147
- expressions = p.value;
148
- expressions = expressions.selectors
149
- ? expressions.selectors
150
- : [expressions];
151
- expressions = convertExpressions(expressions);
152
- }
153
- return {
154
- name: p.name,
155
- expressions: expressions,
156
- value: p.value
157
- };
158
- });
159
- }
160
-
161
- /**
162
- * convert the css-selector-parser format into the Slick format
163
- * @private
164
- * @param Array {Object} expressions
165
- * @return Array {Object}
166
- *
167
- */
168
- convertExpressions = function(expressions) {
169
- return expressions.map(exp => {
170
- var newExp = [];
171
- var rule = exp.rule;
172
- while (rule) {
173
- /* eslint no-restricted-syntax: 0 */
174
- // `.tagName` is a property coming from the `CSSSelectorParser` library
175
- newExp.push({
176
- tag: rule.tagName ? rule.tagName.toLowerCase() : '*',
177
- combinator: rule.nestingOperator ? rule.nestingOperator : ' ',
178
- id: rule.id,
179
- attributes: convertAttributes(rule.attrs),
180
- classes: convertClasses(rule.classNames),
181
- pseudos: convertPseudos(rule.pseudos)
182
- });
183
- rule = rule.rule;
184
- }
185
- return newExp;
186
- });
187
- };
188
-
189
1
  function createLocalVariables(vNodes, anyLevel, thisLevel, parentShadowId) {
190
2
  let retVal = {
191
3
  vNodes: vNodes.slice(),
@@ -197,17 +9,7 @@ function createLocalVariables(vNodes, anyLevel, thisLevel, parentShadowId) {
197
9
  return retVal;
198
10
  }
199
11
 
200
- function matchesSelector(vNode, exp) {
201
- return (
202
- matchesTag(vNode, exp[0]) &&
203
- matchesClasses(vNode, exp[0]) &&
204
- matchesAttributes(vNode, exp[0]) &&
205
- matchesId(vNode, exp[0]) &&
206
- matchesPseudos(vNode, exp[0])
207
- );
208
- }
209
-
210
- matchExpressions = function(domTree, expressions, recurse, filter) {
12
+ function matchExpressions(domTree, expressions, filter) {
211
13
  let stack = [];
212
14
  let vNodes = Array.isArray(domTree) ? domTree : [domTree];
213
15
  let currentLevel = createLocalVariables(
@@ -229,7 +31,7 @@ matchExpressions = function(domTree, expressions, recurse, filter) {
229
31
  let exp = combined[i];
230
32
  if (
231
33
  (!exp[0].id || vNode.shadowId === currentLevel.parentShadowId) &&
232
- matchesSelector(vNode, exp)
34
+ axe.utils.matchesExpression(vNode, exp[0])
233
35
  ) {
234
36
  if (exp.length === 1) {
235
37
  if (!added && (!filter || filter(vNode))) {
@@ -260,8 +62,8 @@ matchExpressions = function(domTree, expressions, recurse, filter) {
260
62
  childAny.push(exp);
261
63
  }
262
64
  }
263
- // "recurse"
264
- if (vNode.children && vNode.children.length && recurse) {
65
+
66
+ if (vNode.children && vNode.children.length) {
265
67
  stack.push(currentLevel);
266
68
  currentLevel = createLocalVariables(
267
69
  vNode.children,
@@ -276,7 +78,7 @@ matchExpressions = function(domTree, expressions, recurse, filter) {
276
78
  }
277
79
  }
278
80
  return result;
279
- };
81
+ }
280
82
 
281
83
  /**
282
84
  * querySelectorAll implementation that operates on the flattened tree (supports shadow DOM)
@@ -301,11 +103,8 @@ axe.utils.querySelectorAll = function(domTree, selector) {
301
103
  * @param {Function} filter function (optional)
302
104
  * @return {Array} Elements matched by any of the selectors and filtered by the filter function
303
105
  */
304
-
305
106
  axe.utils.querySelectorAllFilter = function(domTree, selector, filter) {
306
107
  domTree = Array.isArray(domTree) ? domTree : [domTree];
307
- var expressions = axe.utils.cssParser.parse(selector);
308
- expressions = expressions.selectors ? expressions.selectors : [expressions];
309
- expressions = convertExpressions(expressions);
310
- return matchExpressions(domTree, expressions, true, filter);
108
+ const expressions = axe.utils.convertSelector(selector);
109
+ return matchExpressions(domTree, expressions, filter);
311
110
  };
@@ -8,7 +8,11 @@
8
8
  "description": "Ensures aria-hidden elements do not contain focusable elements",
9
9
  "help": "ARIA hidden element must not contain focusable elements"
10
10
  },
11
- "all": ["focusable-disabled", "focusable-not-tabbable"],
11
+ "all": [
12
+ "focusable-modal-open",
13
+ "focusable-disabled",
14
+ "focusable-not-tabbable"
15
+ ],
12
16
  "any": [],
13
17
  "none": []
14
18
  }
@@ -8,5 +8,5 @@
8
8
  },
9
9
  "all": [],
10
10
  "any": [],
11
- "none": ["invalidrole", "abstractrole", "unsupportedrole"]
11
+ "none": ["fallbackrole", "invalidrole", "abstractrole", "unsupportedrole"]
12
12
  }
@@ -22,4 +22,4 @@
22
22
  "non-empty-title"
23
23
  ],
24
24
  "none": []
25
- }
25
+ }
@@ -98,7 +98,7 @@ if (
98
98
  visibleText === '' ||
99
99
  axe.commons.text.removeUnicode(visibleText, {
100
100
  emoji: true,
101
- nonBmp: true,
101
+ nonBmp: false,
102
102
  punctuations: true
103
103
  }) === ''
104
104
  ) {
@@ -2,9 +2,6 @@
2
2
  "id": "color-contrast",
3
3
  "matches": "color-contrast-matches.js",
4
4
  "excludeHidden": false,
5
- "options": {
6
- "noScroll": false
7
- },
8
5
  "tags": ["cat.color", "wcag2aa", "wcag143"],
9
6
  "metadata": {
10
7
  "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds",