govuk_publishing_components 21.22.0 → 21.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/govuk_publishing_components/components/govspeak/_button.scss +2 -1
  3. data/app/views/govuk_publishing_components/components/_character_count.html.erb +14 -13
  4. data/app/views/govuk_publishing_components/components/docs/character_count.yml +8 -0
  5. data/app/views/govuk_publishing_components/components/docs/govspeak.yml +17 -4
  6. data/lib/govuk_publishing_components/version.rb +1 -1
  7. data/node_modules/axe-core/CHANGELOG.md +37 -0
  8. data/node_modules/axe-core/README.md +2 -2
  9. data/node_modules/axe-core/axe.d.ts +1 -1
  10. data/node_modules/axe-core/axe.js +2430 -2242
  11. data/node_modules/axe-core/axe.min.js +3 -3
  12. data/node_modules/axe-core/bower.json +1 -1
  13. data/node_modules/axe-core/doc/API.md +10 -0
  14. data/node_modules/axe-core/doc/check-message-template.md +124 -0
  15. data/node_modules/axe-core/doc/examples/jasmine/package-lock.json +1489 -0
  16. data/node_modules/axe-core/doc/examples/jest_react/package-lock.json +7525 -0
  17. data/node_modules/axe-core/doc/examples/mocha/package-lock.json +1671 -0
  18. data/node_modules/axe-core/doc/examples/phantomjs/package-lock.json +862 -0
  19. data/node_modules/axe-core/doc/examples/qunit/package-lock.json +2951 -0
  20. data/node_modules/axe-core/doc/rule-descriptions.md +2 -2
  21. data/node_modules/axe-core/lib/checks/aria/allowed-attr.json +4 -1
  22. data/node_modules/axe-core/lib/checks/aria/aria-allowed-role.json +8 -2
  23. data/node_modules/axe-core/lib/checks/aria/errormessage.json +4 -1
  24. data/node_modules/axe-core/lib/checks/aria/implicit-role-fallback.json +1 -0
  25. data/node_modules/axe-core/lib/checks/aria/no-implicit-explicit-label.json +1 -1
  26. data/node_modules/axe-core/lib/checks/aria/required-attr.json +4 -1
  27. data/node_modules/axe-core/lib/checks/aria/required-children.json +8 -2
  28. data/node_modules/axe-core/lib/checks/aria/required-parent.json +4 -1
  29. data/node_modules/axe-core/lib/checks/aria/unsupportedattr.json +1 -1
  30. data/node_modules/axe-core/lib/checks/aria/unsupportedrole.json +1 -1
  31. data/node_modules/axe-core/lib/checks/aria/valid-attr-value.json +8 -2
  32. data/node_modules/axe-core/lib/checks/aria/valid-attr.json +5 -2
  33. data/node_modules/axe-core/lib/checks/color/color-contrast.js +1 -1
  34. data/node_modules/axe-core/lib/checks/color/color-contrast.json +4 -4
  35. data/node_modules/axe-core/lib/checks/color/link-in-text-block.js +2 -2
  36. data/node_modules/axe-core/lib/checks/color/link-in-text-block.json +2 -2
  37. data/node_modules/axe-core/lib/checks/forms/fieldset.js +1 -1
  38. data/node_modules/axe-core/lib/checks/forms/fieldset.json +8 -1
  39. data/node_modules/axe-core/lib/checks/forms/group-labelledby.js +2 -2
  40. data/node_modules/axe-core/lib/checks/forms/group-labelledby.json +6 -2
  41. data/node_modules/axe-core/lib/checks/keyboard/landmark-is-top-level.json +2 -2
  42. data/node_modules/axe-core/lib/checks/lists/listitem.js +3 -1
  43. data/node_modules/axe-core/lib/checks/lists/listitem.json +4 -1
  44. data/node_modules/axe-core/lib/checks/mobile/css-orientation-lock.js +216 -99
  45. data/node_modules/axe-core/lib/checks/mobile/css-orientation-lock.json +3 -0
  46. data/node_modules/axe-core/lib/checks/mobile/meta-viewport.json +1 -1
  47. data/node_modules/axe-core/lib/checks/parsing/duplicate-id-active.json +1 -1
  48. data/node_modules/axe-core/lib/checks/parsing/duplicate-id-aria.json +1 -1
  49. data/node_modules/axe-core/lib/checks/shared/aria-label.js +1 -1
  50. data/node_modules/axe-core/lib/checks/shared/avoid-inline-spacing.json +4 -1
  51. data/node_modules/axe-core/lib/checks/shared/non-empty-if-present.js +5 -1
  52. data/node_modules/axe-core/lib/checks/shared/non-empty-if-present.json +4 -1
  53. data/node_modules/axe-core/lib/checks/tables/has-caption.json +1 -0
  54. data/node_modules/axe-core/lib/checks/tables/has-summary.json +1 -0
  55. data/node_modules/axe-core/lib/checks/tables/has-th.json +1 -0
  56. data/node_modules/axe-core/lib/commons/aria/arialabel-text.js +7 -4
  57. data/node_modules/axe-core/lib/commons/aria/get-element-unallowed-roles.js +21 -0
  58. data/node_modules/axe-core/lib/commons/dom/get-element-stack.js +465 -0
  59. data/node_modules/axe-core/lib/commons/dom/shadow-elements-from-point.js +2 -1
  60. data/node_modules/axe-core/lib/core/base/audit.js +39 -17
  61. data/node_modules/axe-core/lib/core/base/virtual-node/serial-virtual-node.js +2 -0
  62. data/node_modules/axe-core/lib/core/base/virtual-node/virtual-node.js +43 -0
  63. data/node_modules/axe-core/lib/core/reporters/helpers/incomplete-fallback-msg.js +3 -1
  64. data/node_modules/axe-core/lib/core/utils/parse-crossorigin-stylesheet.js +1 -0
  65. data/node_modules/axe-core/lib/core/utils/preload-cssom.js +4 -3
  66. data/node_modules/axe-core/lib/core/utils/process-message.js +72 -0
  67. data/node_modules/axe-core/lib/core/utils/publish-metadata.js +20 -7
  68. data/node_modules/axe-core/lib/rules/aria-dpub-role-fallback.json +2 -1
  69. data/node_modules/axe-core/lib/rules/label-content-name-mismatch-matches.js +1 -1
  70. data/node_modules/axe-core/lib/rules/layout-table.json +2 -1
  71. data/node_modules/axe-core/locales/ja.json +1 -1
  72. data/node_modules/axe-core/package.json +16 -16
  73. data/node_modules/axe-core/sri-history.json +4 -0
  74. data/node_modules/axe-core/typings/axe-core/axe-core-tests.js +151 -0
  75. data/node_modules/axe-core/typings/axe-core/axe-core-tests.ts +18 -0
  76. metadata +11 -2
@@ -4,7 +4,7 @@
4
4
  | area-alt | Ensures <area> elements of image maps have alternate text | Critical | cat.text-alternatives, wcag2a, wcag111, section508, section508.22.a | true | true | false |
5
5
  | aria-allowed-attr | Ensures ARIA attributes are allowed for an element's role | Critical | cat.aria, wcag2a, wcag412 | true | true | false |
6
6
  | aria-allowed-role | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | true | true | true |
7
- | aria-dpub-role-fallback | Ensures unsupported DPUB roles are only used on elements with implicit fallback roles | Moderate | cat.aria, wcag2a, wcag131 | true | true | false |
7
+ | aria-dpub-role-fallback | Ensures unsupported DPUB roles are only used on elements with implicit fallback roles | Moderate | cat.aria, wcag2a, wcag131, deprecated | false | true | false |
8
8
  | aria-hidden-body | Ensures aria-hidden='true' is not present on the document body. | Critical | cat.aria, wcag2a, wcag412 | true | true | false |
9
9
  | aria-hidden-focus | Ensures aria-hidden elements do not contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, wcag131 | true | true | false |
10
10
  | aria-input-field-name | Ensures every ARIA input field has an accessible name | Moderate, Serious | wcag2a, wcag412 | true | true | true |
@@ -57,7 +57,7 @@
57
57
  | landmark-no-duplicate-contentinfo | Ensures the document has at most one contentinfo landmark | Moderate | cat.semantics, best-practice | true | true | false |
58
58
  | landmark-one-main | Ensures the document has only one main landmark and each iframe in the page has at most one main landmark | Moderate | cat.semantics, best-practice | true | true | false |
59
59
  | landmark-unique | Landmarks must have a unique role or role/label/title (i.e. accessible name) combination | Moderate | cat.semantics, best-practice | true | true | false |
60
- | layout-table | Ensures presentational <table> elements do not use <th>, <caption> elements or the summary attribute | Serious | cat.semantics, wcag2a, wcag131 | true | true | false |
60
+ | layout-table | Ensures presentational <table> elements do not use <th>, <caption> elements or the summary attribute | Serious | cat.semantics, wcag2a, wcag131, deprecated | false | true | false |
61
61
  | link-in-text-block | Links can be distinguished without relying on color | Serious | cat.color, experimental, wcag2a, wcag141 | true | true | true |
62
62
  | link-name | Ensures links have discernible text | Serious | cat.name-role-value, wcag2a, wcag412, wcag244, section508, section508.22.a | true | true | false |
63
63
  | list | Ensures that lists are structured correctly | Serious | cat.structure, wcag2a, wcag131 | true | true | false |
@@ -5,7 +5,10 @@
5
5
  "impact": "critical",
6
6
  "messages": {
7
7
  "pass": "ARIA attributes are used correctly for the defined role",
8
- "fail": "ARIA attribute{{=it.data && it.data.length > 1 ? 's are' : ' is'}} not allowed:{{~it.data:value}} {{=value}}{{~}}"
8
+ "fail": {
9
+ "singular": "ARIA attribute is not allowed: ${data.values}",
10
+ "plural": "ARIA attributes are not allowed: ${data.values}"
11
+ }
9
12
  }
10
13
  }
11
14
  }
@@ -9,8 +9,14 @@
9
9
  "impact": "minor",
10
10
  "messages": {
11
11
  "pass": "ARIA role is allowed for given element",
12
- "fail": "ARIA role{{=it.data && it.data.length > 1 ? 's' : ''}} {{=it.data.join(', ')}} {{=it.data && it.data.length > 1 ? 'are' : ' is'}} not allowed for given element",
13
- "incomplete": "ARIA role{{=it.data && it.data.length > 1 ? 's' : ''}} {{=it.data.join(', ')}} must be removed when the element is made visible, as {{=it.data && it.data.length > 1 ? 'they are' : 'it is'}} not allowed for the element"
12
+ "fail": {
13
+ "singular": "ARIA role ${data.values} is not allowed for given element",
14
+ "plural": "ARIA roles ${data.values} are not allowed for given element"
15
+ },
16
+ "incomplete": {
17
+ "singular": "ARIA role ${data.values} must be removed when the element is made visible, as it is not allowed for the element",
18
+ "plural": "ARIA roles ${data.values} must be removed when the element is made visible, as they are not allowed for the element"
19
+ }
14
20
  }
15
21
  }
16
22
  }
@@ -5,7 +5,10 @@
5
5
  "impact": "critical",
6
6
  "messages": {
7
7
  "pass": "Uses a supported aria-errormessage technique",
8
- "fail": "aria-errormessage value{{=it.data && it.data.length > 1 ? 's' : ''}} {{~it.data:value}} `{{=value}}{{~}}` must use a technique to announce the message (e.g., aria-live, aria-describedby, role=alert, etc.)"
8
+ "fail": {
9
+ "singular": "aria-errormessage value `${data.values}` must use a technique to announce the message (e.g., aria-live, aria-describedby, role=alert, etc.)",
10
+ "plural": "aria-errormessage values `${data.values}` must use a technique to announce the message (e.g., aria-live, aria-describedby, role=alert, etc.)"
11
+ }
9
12
  }
10
13
  }
11
14
  }
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "implicit-role-fallback",
3
3
  "evaluate": "implicit-role-fallback.js",
4
+ "deprecated": true,
4
5
  "metadata": {
5
6
  "impact": "moderate",
6
7
  "messages": {
@@ -5,7 +5,7 @@
5
5
  "impact": "moderate",
6
6
  "messages": {
7
7
  "pass": "There is no mismatch between a <label> and accessible name",
8
- "incomplete": "Check that the <label> does not need be part of the ARIA {{=it.data}} field's name"
8
+ "incomplete": "Check that the <label> does not need be part of the ARIA ${data} field's name"
9
9
  }
10
10
  }
11
11
  }
@@ -5,7 +5,10 @@
5
5
  "impact": "critical",
6
6
  "messages": {
7
7
  "pass": "All required ARIA attributes are present",
8
- "fail": "Required ARIA attribute{{=it.data && it.data.length > 1 ? 's' : ''}} not present:{{~it.data:value}} {{=value}}{{~}}"
8
+ "fail": {
9
+ "singular": "Required ARIA attribute not present: ${data.values}",
10
+ "plural": "Required ARIA attributes not present: ${data.values}"
11
+ }
9
12
  }
10
13
  }
11
14
  }
@@ -19,8 +19,14 @@
19
19
  "impact": "critical",
20
20
  "messages": {
21
21
  "pass": "Required ARIA children are present",
22
- "fail": "Required ARIA {{=it.data && it.data.length > 1 ? 'children' : 'child'}} role not present:{{~it.data:value}} {{=value}}{{~}}",
23
- "incomplete": "Expecting ARIA {{=it.data && it.data.length > 1 ? 'children' : 'child'}} role to be added:{{~it.data:value}} {{=value}}{{~}}"
22
+ "fail": {
23
+ "singular": "Required ARIA child role not present: ${data.values}",
24
+ "plural": "Required ARIA children role not present: ${data.values}"
25
+ },
26
+ "incomplete": {
27
+ "singular": "Expecting ARIA child role to be added: ${data.values}",
28
+ "plural": "Expecting ARIA children role to be added: ${data.values}"
29
+ }
24
30
  }
25
31
  }
26
32
  }
@@ -5,7 +5,10 @@
5
5
  "impact": "critical",
6
6
  "messages": {
7
7
  "pass": "Required ARIA parent role present",
8
- "fail": "Required ARIA parent{{=it.data && it.data.length > 1 ? 's' : ''}} role not present:{{~it.data:value}} {{=value}}{{~}}"
8
+ "fail": {
9
+ "singular": "Required ARIA parent role not present: ${data.values}",
10
+ "plural": "Required ARIA parents role not present: ${data.values}"
11
+ }
9
12
  }
10
13
  }
11
14
  }
@@ -5,7 +5,7 @@
5
5
  "impact": "critical",
6
6
  "messages": {
7
7
  "pass": "ARIA attribute is supported",
8
- "fail": "ARIA attribute is not widely supported in screen readers and assistive technologies: {{~it.data:value}} {{=value}}{{~}}"
8
+ "fail": "ARIA attribute is not widely supported in screen readers and assistive technologies: ${data.values}"
9
9
  }
10
10
  }
11
11
  }
@@ -5,7 +5,7 @@
5
5
  "impact": "critical",
6
6
  "messages": {
7
7
  "pass": "ARIA role is supported",
8
- "fail": "The role used is not widely supported in screen readers and assistive technologies: {{~it.data:value}} {{=value}}{{~}}"
8
+ "fail": "The role used is not widely supported in screen readers and assistive technologies: ${data.values}"
9
9
  }
10
10
  }
11
11
  }
@@ -6,8 +6,14 @@
6
6
  "impact": "critical",
7
7
  "messages": {
8
8
  "pass": "ARIA attribute values are valid",
9
- "fail": "Invalid ARIA attribute value{{=it.data && it.data.length > 1 ? 's' : ''}}:{{~it.data:value}} {{=value}}{{~}}",
10
- "incomplete": "ARIA attribute{{=it.data && it.data.length > 1 ? 's' : ''}} element ID does not exist on the page:{{~it.data:value}} {{=value}}{{~}}"
9
+ "fail": {
10
+ "singular": "Invalid ARIA attribute value: ${data.values}",
11
+ "plural": "Invalid ARIA attribute values: ${data.values}"
12
+ },
13
+ "incomplete": {
14
+ "singular": "ARIA attribute element ID does not exist on the page: ${data.values}",
15
+ "plural": "ARIA attributes element ID does not exist on the page: ${data.values}"
16
+ }
11
17
  }
12
18
  }
13
19
  }
@@ -5,8 +5,11 @@
5
5
  "metadata": {
6
6
  "impact": "critical",
7
7
  "messages": {
8
- "pass": "ARIA attribute name{{=it.data && it.data.length > 1 ? 's' : ''}} are valid",
9
- "fail": "Invalid ARIA attribute name{{=it.data && it.data.length > 1 ? 's' : ''}}:{{~it.data:value}} {{=value}}{{~}}"
8
+ "pass": "ARIA attribute name is valid",
9
+ "fail": {
10
+ "singular": "Invalid ARIA attribute name: ${data.values}",
11
+ "plural": "Invalid ARIA attribute names: ${data.values}"
12
+ }
10
13
  }
11
14
  }
12
15
  }
@@ -44,7 +44,7 @@ const data = {
44
44
  contrastRatio: cr ? truncatedResult : undefined,
45
45
  fontSize: `${((fontSize * 72) / 96).toFixed(1)}pt (${fontSize}px)`,
46
46
  fontWeight: bold ? 'bold' : 'normal',
47
- missingData: missing,
47
+ messageKey: missing,
48
48
  expectedContrastRatio: cr.expectedContrastRatio + ':1'
49
49
  };
50
50
 
@@ -4,9 +4,10 @@
4
4
  "metadata": {
5
5
  "impact": "serious",
6
6
  "messages": {
7
- "pass": "Element has sufficient color contrast of {{=it.data.contrastRatio}}",
8
- "fail": "Element has insufficient color contrast of {{=it.data.contrastRatio}} (foreground color: {{=it.data.fgColor}}, background color: {{=it.data.bgColor}}, font size: {{=it.data.fontSize}}, font weight: {{=it.data.fontWeight}}). Expected contrast ratio of {{=it.data.expectedContrastRatio}}",
7
+ "pass": "Element has sufficient color contrast of ${data.contrastRatio}",
8
+ "fail": "Element has insufficient color contrast of ${data.contrastRatio} (foreground color: ${data.fgColor}, background color: ${data.bgColor}, font size: ${data.fontSize}, font weight: ${data.fontWeight}). Expected contrast ratio of ${data.expectedContrastRatio}",
9
9
  "incomplete": {
10
+ "default": "Unable to determine contrast ratio",
10
11
  "bgImage": "Element's background color could not be determined due to a background image",
11
12
  "bgGradient": "Element's background color could not be determined due to a background gradient",
12
13
  "imgNode": "Element's background color could not be determined because element contains an image node",
@@ -16,8 +17,7 @@
16
17
  "elmPartiallyObscuring": "Element's background color could not be determined because it partially overlaps other elements",
17
18
  "outsideViewport": "Element's background color could not be determined because it's outside the viewport",
18
19
  "equalRatio": "Element has a 1:1 contrast ratio with the background",
19
- "shortTextContent": "Element content is too short to determine if it is actual text content",
20
- "default": "Unable to determine contrast ratio"
20
+ "shortTextContent": "Element content is too short to determine if it is actual text content"
21
21
  }
22
22
  }
23
23
  }
@@ -50,7 +50,7 @@ if (color.elementIsDistinct(node, parentBlock)) {
50
50
  } else if (contrast >= 3.0) {
51
51
  axe.commons.color.incompleteData.set('fgColor', 'bgContrast');
52
52
  this.data({
53
- missingData: axe.commons.color.incompleteData.get('fgColor')
53
+ messageKey: axe.commons.color.incompleteData.get('fgColor')
54
54
  });
55
55
  axe.commons.color.incompleteData.clear();
56
56
  // User needs to check whether there is a hover and a focus style
@@ -74,7 +74,7 @@ if (color.elementIsDistinct(node, parentBlock)) {
74
74
  }
75
75
  axe.commons.color.incompleteData.set('fgColor', reason);
76
76
  this.data({
77
- missingData: axe.commons.color.incompleteData.get('fgColor')
77
+ messageKey: axe.commons.color.incompleteData.get('fgColor')
78
78
  });
79
79
  axe.commons.color.incompleteData.clear();
80
80
  return undefined;
@@ -7,12 +7,12 @@
7
7
  "pass": "Links can be distinguished from surrounding text in some way other than by color",
8
8
  "fail": "Links need to be distinguished from surrounding text in some way other than by color",
9
9
  "incomplete": {
10
+ "default": "Unable to determine contrast ratio",
10
11
  "bgContrast": "Element's contrast ratio could not be determined. Check for a distinct hover/focus style",
11
12
  "bgImage": "Element's contrast ratio could not be determined due to a background image",
12
13
  "bgGradient": "Element's contrast ratio could not be determined due to a background gradient",
13
14
  "imgNode": "Element's contrast ratio could not be determined because element contains an image node",
14
- "bgOverlap": "Element's contrast ratio could not be determined because of element overlap",
15
- "default": "Unable to determine contrast ratio"
15
+ "bgOverlap": "Element's contrast ratio could not be determined because of element overlap"
16
16
  }
17
17
  }
18
18
  }
@@ -102,7 +102,7 @@ var data = {
102
102
 
103
103
  var result = runCheck(virtualNode);
104
104
  if (!result) {
105
- data.failureCode = failureCode;
105
+ data.messageKey = failureCode;
106
106
  }
107
107
  this.data(data);
108
108
 
@@ -7,7 +7,14 @@
7
7
  "impact": "critical",
8
8
  "messages": {
9
9
  "pass": "Element is contained in a fieldset",
10
- "fail": "{{var code = it.data && it.data.failureCode;}}{{? code === 'no-legend'}}Fieldset does not have a legend as its first child{{?? code === 'empty-legend'}}Legend does not have text that is visible to screen readers{{?? code === 'mixed-inputs'}}Fieldset contains unrelated inputs{{?? code === 'no-group-label'}}ARIA group does not have aria-label or aria-labelledby{{?? code === 'group-mixed-inputs'}}ARIA group contains unrelated inputs{{??}}Element does not have a containing fieldset or ARIA group{{?}}"
10
+ "fail": {
11
+ "default": "Element does not have a containing fieldset or ARIA group",
12
+ "no-legend": "Fieldset does not have a legend as its first child",
13
+ "empty-legend": "Legend does not have text that is visible to screen readers",
14
+ "mixed-inputs": "Fieldset contains unrelated inputs",
15
+ "no-group-label": "ARIA group does not have aria-label or aria-labelledby",
16
+ "group-mixed-inputs": "ARIA group contains unrelated inputs"
17
+ }
11
18
  }
12
19
  }
13
20
  }
@@ -57,9 +57,9 @@ if (uniqueLabels.length > 0 && sharedLabels.length > 0) {
57
57
  }
58
58
 
59
59
  if (uniqueLabels.length > 0 && sharedLabels.length === 0) {
60
- data.failureCode = 'no-shared-label';
60
+ data.messageKey = 'no-shared-label';
61
61
  } else if (uniqueLabels.length === 0 && sharedLabels.length > 0) {
62
- data.failureCode = 'no-unique-label';
62
+ data.messageKey = 'no-unique-label';
63
63
  }
64
64
 
65
65
  this.data(data);
@@ -6,8 +6,12 @@
6
6
  "metadata": {
7
7
  "impact": "critical",
8
8
  "messages": {
9
- "pass": "Elements with the name \"{{=it.data.name}}\" have both a shared label, and a unique label, referenced through aria-labelledby",
10
- "fail": "{{var code = it.data && it.data.failureCode;}}Elements with the name \"{{=it.data.name}}\" do not all have {{? code === 'no-shared-label' }}a shared label{{?? code === 'no-unique-label' }}a unique label{{??}}both a shared label, and a unique label{{?}}, referenced through aria-labelledby"
9
+ "pass": "Elements with the name \"${data.name}\" have both a shared label, and a unique label, referenced through aria-labelledby",
10
+ "fail": {
11
+ "default": "Elements with the name \"${data.name}\" do not all have both a shared label, and a unique label referenced through aria-labelledby",
12
+ "no-shared-label": "Elements with the name \"${data.name}\" do not all have a shared label referenced through aria-labelledby",
13
+ "no-unique-label": "Elements with the name \"${data.name}\" do not all have a unique label referenced through aria-labelledby"
14
+ }
11
15
  }
12
16
  }
13
17
  }
@@ -4,8 +4,8 @@
4
4
  "metadata": {
5
5
  "impact": "moderate",
6
6
  "messages": {
7
- "pass": "The {{=it.data.role }} landmark is at the top level.",
8
- "fail": "The {{=it.data.role }} landmark is contained in another landmark."
7
+ "pass": "The ${data.role} landmark is at the top level.",
8
+ "fail": "The ${data.role} landmark is contained in another landmark."
9
9
  }
10
10
  }
11
11
  }
@@ -12,7 +12,9 @@ if (parentRole === 'list') {
12
12
  }
13
13
 
14
14
  if (parentRole && axe.commons.aria.isValidRole(parentRole)) {
15
- this.data('roleNotValid');
15
+ this.data({
16
+ messageKey: 'roleNotValid'
17
+ });
16
18
  return false;
17
19
  }
18
20
 
@@ -5,7 +5,10 @@
5
5
  "impact": "serious",
6
6
  "messages": {
7
7
  "pass": "List item has a <ul>, <ol> or role=\"list\" parent element",
8
- "fail": "List item does not have a <ul>, <ol>{{? it.data === 'roleNotValid'}} without a role, or a role=\"list\"{{?}} parent element"
8
+ "fail": {
9
+ "default": "List item does not have a <ul>, <ol> parent element",
10
+ "roleNotValid": "List item does not have a <ul>, <ol> parent element without a role, or a role=\"list\""
11
+ }
9
12
  }
10
13
  }
11
14
  }
@@ -1,134 +1,251 @@
1
1
  /* global context */
2
-
3
- // extract asset of type `cssom` from context
4
2
  const { cssom = undefined } = context || {};
5
-
6
- // if there is no cssom <- return incomplete
3
+ const { degreeThreshold = 0 } = options || {};
7
4
  if (!cssom || !cssom.length) {
8
5
  return undefined;
9
6
  }
10
7
 
11
- // combine all rules from each sheet into one array
12
- const rulesGroupByDocumentFragment = cssom.reduce(
13
- (out, { sheet, root, shadowId }) => {
14
- // construct key based on shadowId or top level document
8
+ let isLocked = false;
9
+ let relatedElements = [];
10
+ const rulesGroupByDocumentFragment = groupCssomByDocument(cssom);
11
+
12
+ for (const key of Object.keys(rulesGroupByDocumentFragment)) {
13
+ const { root, rules } = rulesGroupByDocumentFragment[key];
14
+ const orientationRules = rules.filter(isMediaRuleWithOrientation);
15
+ if (!orientationRules.length) {
16
+ continue;
17
+ }
18
+
19
+ orientationRules.forEach(({ cssRules }) => {
20
+ Array.from(cssRules).forEach(cssRule => {
21
+ const locked = getIsOrientationLocked(cssRule);
22
+
23
+ // if locked and not root HTML, preserve as relatedNodes
24
+ if (locked && cssRule.selectorText.toUpperCase() !== 'HTML') {
25
+ const elms =
26
+ Array.from(root.querySelectorAll(cssRule.selectorText)) || [];
27
+ relatedElements = relatedElements.concat(elms);
28
+ }
29
+
30
+ isLocked = isLocked || locked;
31
+ });
32
+ });
33
+ }
34
+
35
+ if (!isLocked) {
36
+ return true;
37
+ }
38
+ if (relatedElements.length) {
39
+ this.relatedNodes(relatedElements);
40
+ }
41
+ return false;
42
+
43
+ /**
44
+ * Group given cssom by document/ document fragment
45
+ * @param {Array<Object>} allCssom cssom
46
+ * @return {Object}
47
+ */
48
+ function groupCssomByDocument(cssObjectModel) {
49
+ return cssObjectModel.reduce((out, { sheet, root, shadowId }) => {
15
50
  const key = shadowId ? shadowId : 'topDocument';
16
- // init property if does not exist
51
+
17
52
  if (!out[key]) {
18
- out[key] = {
19
- root,
20
- rules: []
21
- };
53
+ out[key] = { root, rules: [] };
22
54
  }
23
- // check if sheet and rules exist
55
+
24
56
  if (!sheet || !sheet.cssRules) {
25
- //return
26
57
  return out;
27
58
  }
59
+
28
60
  const rules = Array.from(sheet.cssRules);
29
- // add rules into same document fragment
30
61
  out[key].rules = out[key].rules.concat(rules);
31
62
 
32
- //return
33
63
  return out;
34
- },
35
- {}
36
- );
64
+ }, {});
65
+ }
37
66
 
38
- // Note:
39
- // Some of these functions can be extracted to utils, but best to do it when other cssom rules are authored.
67
+ /**
68
+ * Filter CSS Rules that target Orientation CSS Media Features
69
+ * @param {Array<Object>} cssRules
70
+ * @returns {Array<Object>}
71
+ */
72
+ function isMediaRuleWithOrientation({ type, cssText }) {
73
+ /**
74
+ * Filter:
75
+ * CSSRule.MEDIA_Rule
76
+ * -> https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
77
+ */
78
+ if (type !== 4) {
79
+ return false;
80
+ }
40
81
 
41
- // extract styles for each orientation rule to verify transform is applied
42
- let isLocked = false;
43
- let relatedElements = [];
82
+ /**
83
+ * Filter:
84
+ * CSSRule with conditionText of `orientation`
85
+ */
86
+ return (
87
+ /orientation:\s*landscape/i.test(cssText) ||
88
+ /orientation:\s*portrait/i.test(cssText)
89
+ );
90
+ }
44
91
 
45
- Object.keys(rulesGroupByDocumentFragment).forEach(key => {
46
- const { root, rules } = rulesGroupByDocumentFragment[key];
92
+ /**
93
+ * Interpolate a given CSS Rule to ascertain if orientation is locked by use of any transformation functions that affect rotation along the Z Axis
94
+ * @param {Object} cssRule given CSS Rule
95
+ * @property {String} cssRule.selectorText selector text targetted by given cssRule
96
+ * @property {Object} cssRule.style style
97
+ * @return {Boolean}
98
+ */
99
+ function getIsOrientationLocked({ selectorText, style }) {
100
+ if (!selectorText || style.length <= 0) {
101
+ return false;
102
+ }
47
103
 
48
- // filter media rules from all rules
49
- const mediaRules = rules.filter(r => {
50
- // doc: https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
51
- // type value of 4 (CSSRule.MEDIA_RULE) pertains to media rules
52
- return r.type === 4;
53
- });
54
- if (!mediaRules || !mediaRules.length) {
55
- return;
104
+ const transformStyle =
105
+ style.transform || style.webkitTransform || style.msTransform || false;
106
+ if (!transformStyle) {
107
+ return false;
56
108
  }
57
109
 
58
- // narrow down to media rules with `orientation` as a keyword
59
- const orientationRules = mediaRules.filter(r => {
60
- // conditionText exists on media rules, which contains only the @media condition
61
- // eg: screen and (max-width: 767px) and (min-width: 320px) and (orientation: landscape)
62
- const cssText = r.cssText;
63
- return (
64
- /orientation:\s*landscape/i.test(cssText) ||
65
- /orientation:\s*portrait/i.test(cssText)
66
- );
67
- });
68
- if (!orientationRules || !orientationRules.length) {
69
- return;
110
+ /**
111
+ * get last match/occurence of a transformation function that can affect rotation along Z axis
112
+ */
113
+ const matches = transformStyle.match(
114
+ /(rotate|rotateZ|rotate3d|matrix|matrix3d)\(([^)]+)\)(?!.*(rotate|rotateZ|rotate3d|matrix|matrix3d))/
115
+ );
116
+ if (!matches) {
117
+ return false;
70
118
  }
71
119
 
72
- orientationRules.forEach(r => {
73
- // r.cssRules is a RULEList and not an array
74
- if (!r.cssRules.length) {
75
- return;
76
- }
77
- // cssRules ia a list of rules
78
- // a media query has framents of css styles applied to various selectors
79
- // iteration through cssRules and see if orientation lock has been applied
80
- Array.from(r.cssRules).forEach(cssRule => {
81
- // ensure selectorText exists
82
- if (!cssRule.selectorText) {
83
- return;
84
- }
85
- // ensure the given selector has styles declared (non empty selector)
86
- if (cssRule.style.length <= 0) {
87
- return;
88
- }
120
+ const [, transformFn, transformFnValue] = matches;
121
+ let degrees = getRotationInDegrees(transformFn, transformFnValue);
122
+ if (!degrees) {
123
+ return false;
124
+ }
125
+ degrees = Math.abs(degrees);
126
+
127
+ /**
128
+ * When degree is a multiple of 180, it is not considered an orientation lock
129
+ */
130
+ if (Math.abs(degrees - 180) % 180 <= degreeThreshold) {
131
+ return false;
132
+ }
133
+
134
+ return Math.abs(degrees - 90) % 90 <= degreeThreshold;
135
+ }
89
136
 
90
- // check if transform style exists (don't forget vendor prefixes)
91
- const transformStyleValue =
92
- cssRule.style.transform ||
93
- cssRule.style.webkitTransform ||
94
- cssRule.style.msTransform ||
95
- false;
96
- // transformStyleValue -> is the value applied to property
97
- // eg: "rotate(-90deg)"
98
- if (!transformStyleValue) {
137
+ /**
138
+ * Interpolate rotation along the z axis from a given value to a transform function
139
+ * @param {String} transformFunction CSS transformation function
140
+ * @param {String} transformFnValue value applied to a transform function (contains a unit)
141
+ * @returns {Number}
142
+ */
143
+ function getRotationInDegrees(transformFunction, transformFnValue) {
144
+ switch (transformFunction) {
145
+ case 'rotate':
146
+ case 'rotateZ':
147
+ return getAngleInDegrees(transformFnValue);
148
+ case 'rotate3d':
149
+ const [, , z, angleWithUnit] = transformFnValue
150
+ .split(',')
151
+ .map(value => value.trim());
152
+ if (parseInt(z) === 0) {
153
+ // no transform is applied along z axis -> ignore
99
154
  return;
100
155
  }
156
+ return getAngleInDegrees(angleWithUnit);
157
+ case 'matrix':
158
+ case 'matrix3d':
159
+ return getAngleInDegreesFromMatrixTransform(transformFnValue);
160
+ default:
161
+ return;
162
+ }
163
+ }
101
164
 
102
- const rotate = transformStyleValue.match(/rotate\(([^)]+)deg\)/);
103
- const deg = parseInt((rotate && rotate[1]) || 0);
104
- const locked = deg % 90 === 0 && deg % 180 !== 0;
165
+ /**
166
+ * Get angle in degrees from a transform value by interpolating the unit of measure
167
+ * @param {String} angleWithUnit value applied to a transform function (Eg: 1turn)
168
+ * @returns{Number|undefined}
169
+ */
170
+ function getAngleInDegrees(angleWithUnit) {
171
+ const [unit] = angleWithUnit.match(/(deg|grad|rad|turn)/) || [];
172
+ if (!unit) {
173
+ return;
174
+ }
105
175
 
106
- // if locked
107
- // and not root HTML
108
- // preserve as relatedNodes
109
- if (locked && cssRule.selectorText.toUpperCase() !== 'HTML') {
110
- const selector = cssRule.selectorText;
111
- const elms = Array.from(root.querySelectorAll(selector));
112
- if (elms && elms.length) {
113
- relatedElements = relatedElements.concat(elms);
114
- }
115
- }
176
+ const angle = parseFloat(angleWithUnit.replace(unit, ``));
177
+ switch (unit) {
178
+ case 'rad':
179
+ return convertRadToDeg(angle);
180
+ case 'grad':
181
+ return convertGradToDeg(angle);
182
+ case 'turn':
183
+ return convertTurnToDeg(angle);
184
+ case 'deg':
185
+ default:
186
+ return parseInt(angle);
187
+ }
188
+ }
116
189
 
117
- // set locked boolean
118
- isLocked = locked;
119
- });
120
- });
121
- });
190
+ /**
191
+ * Get angle in degrees from a transform value applied to `matrix` or `matrix3d` transform functions
192
+ * @param {String} transformFnValue value applied to a transform function (contains a unit)
193
+ * @returns {Number}
194
+ */
195
+ function getAngleInDegreesFromMatrixTransform(transformFnValue) {
196
+ const values = transformFnValue.split(',');
197
+
198
+ /**
199
+ * Matrix 2D
200
+ * Notes: https://css-tricks.com/get-value-of-css-rotation-through-javascript/
201
+ */
202
+ if (values.length <= 6) {
203
+ const [a, b] = values;
204
+ const radians = Math.atan2(parseFloat(b), parseFloat(a));
205
+ return convertRadToDeg(radians);
206
+ }
122
207
 
123
- if (!isLocked) {
124
- // return
125
- return true;
208
+ /**
209
+ * Matrix 3D
210
+ * Notes: https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix
211
+ */
212
+ const sinB = parseFloat(values[8]);
213
+ const b = Math.asin(sinB);
214
+ const cosB = Math.cos(b);
215
+ const rotateZRadians = Math.acos(parseFloat(values[0]) / cosB);
216
+ return convertRadToDeg(rotateZRadians);
126
217
  }
127
218
 
128
- // set relatedNodes
129
- if (relatedElements.length) {
130
- this.relatedNodes(relatedElements);
219
+ /**
220
+ * Convert angle specified in unit radians to degrees
221
+ * See - https://drafts.csswg.org/css-values-3/#rad
222
+ * @param {Number} radians radians
223
+ * @return {Number}
224
+ */
225
+ function convertRadToDeg(radians) {
226
+ return Math.round(radians * (180 / Math.PI));
131
227
  }
132
228
 
133
- // return fail
134
- return false;
229
+ /**
230
+ * Convert angle specified in unit grad to degrees
231
+ * See - https://drafts.csswg.org/css-values-3/#grad
232
+ * @param {Number} grad grad
233
+ * @return {Number}
234
+ */
235
+ function convertGradToDeg(grad) {
236
+ grad = grad % 400;
237
+ if (grad < 0) {
238
+ grad += 400;
239
+ }
240
+ return Math.round((grad / 400) * 360);
241
+ }
242
+
243
+ /**
244
+ * Convert angle specifed in unit turn to degrees
245
+ * See - https://drafts.csswg.org/css-values-3/#turn
246
+ * @param {Number} turn
247
+ * @returns {Number}
248
+ */
249
+ function convertTurnToDeg(turn) {
250
+ return Math.round(360 / (1 / turn));
251
+ }