@stackoverflow/stacks 1.10.5 → 1.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ import { html } from "@open-wc/testing";
2
+ import { IconClearSm } from "@stackoverflow/stacks-icons";
3
+ import { defaultOptions, runComponentTests } from "../../test/test-utils";
4
+ import "../../index";
5
+
6
+ const children = {
7
+ default: `default`,
8
+ dismiss: `dismiss <span class="s-tag--dismiss">${IconClearSm}</span>`,
9
+ sponsor: `<img class="s-tag--sponsor" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII" width="16" height="16" alt="sponsor">sponsor`,
10
+ };
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ const template = ({ component, testid }: any) => html`
14
+ <div data-testid="${testid}" class="d-inline-block p4">${component}</div>
15
+ `;
16
+
17
+ describe("tag", () => {
18
+ runComponentTests({
19
+ type: "visual",
20
+ baseClass: "s-tag",
21
+ variants: ["ignored", "watched", "moderator", "muted", "required"],
22
+ modifiers: {
23
+ global: ["is-selected"],
24
+ },
25
+ children,
26
+ template,
27
+ });
28
+
29
+ // Size modifiers
30
+ runComponentTests({
31
+ type: "visual",
32
+ baseClass: "s-tag",
33
+ modifiers: {
34
+ primary: ["xs", "sm", "md", "lg"],
35
+ global: ["is-selected"],
36
+ },
37
+ children: {
38
+ default: `default`,
39
+ },
40
+ template,
41
+ options: {
42
+ ...defaultOptions,
43
+ includeNullModifier: false,
44
+ },
45
+ });
46
+ });
@@ -106,8 +106,8 @@
106
106
  }
107
107
 
108
108
  & &--type {
109
- a {
110
- color: inherit;
109
+ a:not(.s-link) {
110
+ color: inherit !important;
111
111
  }
112
112
 
113
113
  color: var(--_uc-type-fc);
@@ -32,3 +32,22 @@ axe.configure({
32
32
  console.log(results);
33
33
  });
34
34
  ```
35
+
36
+ ### Using custom APCA thresholds
37
+
38
+ To set custom thresholds for APCA checks, follow these steps:
39
+
40
+ 1. Use `custom` as the first argument when calling `registerAxeAPCA`.
41
+ 1. Provide a function as the second argument, optionally accepting `fontSize` and `fontWeight` arguments.
42
+
43
+
44
+ ```js
45
+ const customConformanceThresholdFn = (fontSize, fontWeight) => {
46
+ const size = parseFloat(fontSize);
47
+ const weight = parseFloat(fontWeight);
48
+
49
+ return size >= 32 || weight > 700 ? 45 : 60;
50
+ };
51
+
52
+ registerAxeAPCA('custom', customConformanceThresholdFn);
53
+ ```
@@ -13,6 +13,82 @@ const runAxe = async (el: HTMLElement): Promise<AxeResults> => {
13
13
  };
14
14
 
15
15
  describe("axe-apca", () => {
16
+ describe("custom conformance level", () => {
17
+ beforeEach(() => {
18
+ const customConformanceThresholdFn = (
19
+ fontSize: string
20
+ ): number | null => {
21
+ return parseFloat(fontSize) >= 32 ? 45 : 60;
22
+ };
23
+
24
+ registerAxeAPCA("custom", customConformanceThresholdFn);
25
+ });
26
+
27
+ it("should check for APCA accessibility contrast violations", async () => {
28
+ const el: HTMLElement = await fixture(
29
+ html`<p
30
+ style="background: white; color: black; font-size: 12px; font-weight: 400;"
31
+ >
32
+ Some copy
33
+ </p>`
34
+ );
35
+
36
+ const results = await runAxe(el);
37
+
38
+ const apcaPasses = results.passes.filter((violation) =>
39
+ violation.id.includes("color-contrast-apca-custom")
40
+ );
41
+
42
+ await expect(apcaPasses.length).to.equal(1);
43
+
44
+ const passNode = apcaPasses[0].nodes[0];
45
+ expect(passNode.all[0].message).to.include(
46
+ "Element has sufficient APCA custom level lightness contrast"
47
+ );
48
+ });
49
+
50
+ it("should detect APCA accessibility contrast violations", async () => {
51
+ const el: HTMLElement = await fixture(
52
+ html`<p
53
+ style="background: white; color: lightgray; font-size: 12px; font-weight: 400;"
54
+ >
55
+ Some copy
56
+ </p>`
57
+ );
58
+
59
+ const results = await runAxe(el);
60
+
61
+ const apcaViolations = results.violations.filter((violation) =>
62
+ violation.id.includes("color-contrast-apca-custom")
63
+ );
64
+
65
+ await expect(apcaViolations.length).to.equal(1);
66
+
67
+ const violationNode = apcaViolations[0].nodes[0];
68
+ expect(violationNode.failureSummary).to.include(
69
+ "Element has insufficient APCA custom level contrast"
70
+ );
71
+ });
72
+
73
+ it("should check nested nodes", async () => {
74
+ const el: HTMLElement = await fixture(
75
+ html`<div style="background: black;">
76
+ <h2>Some title</h2>
77
+ <p>Some copy</p>
78
+ <button style="background: black;">Some button</button>
79
+ </div>`
80
+ );
81
+
82
+ const results = await runAxe(el);
83
+
84
+ const apcaViolations = results.violations.filter((violation) =>
85
+ violation.id.includes("color-contrast-apca-custom")
86
+ );
87
+
88
+ await expect(apcaViolations[0].nodes.length).to.equal(3);
89
+ });
90
+ });
91
+
16
92
  describe("bronze conformance level", () => {
17
93
  beforeEach(() => {
18
94
  registerAxeAPCA("bronze");
@@ -44,7 +120,7 @@ describe("axe-apca", () => {
44
120
  it("should detect APCA accessibility contrast violations", async () => {
45
121
  const el: HTMLElement = await fixture(
46
122
  html`<p
47
- style="background: white; color: lightgray; font-size: 12px; font-weight: 400;"
123
+ style="background: white; color: gray; font-size: 12px; font-weight: 400;"
48
124
  >
49
125
  Some copy
50
126
  </p>`
@@ -12,6 +12,13 @@ type Color = {
12
12
  toHexString: () => string;
13
13
  };
14
14
 
15
+ type ConformanceLevel = "bronze" | "silver" | "custom";
16
+
17
+ type ConformanceThresholdFn = (
18
+ fontSize: string,
19
+ fontWeight: string
20
+ ) => number | null;
21
+
15
22
  declare module "axe-core" {
16
23
  const commons: {
17
24
  color: {
@@ -89,10 +96,7 @@ const getAPCABronzeThreshold = (fontSize: string): number | null => {
89
96
 
90
97
  const generateColorContrastAPCAConformanceCheck = (
91
98
  conformanceLevel: string,
92
- conformanceThresholdFn: (
93
- fontSize: string,
94
- fontWeight: string
95
- ) => number | null
99
+ conformanceThresholdFn: ConformanceThresholdFn
96
100
  ): Check => ({
97
101
  id: `color-contrast-apca-${conformanceLevel}-conformance`,
98
102
  metadata: {
@@ -188,15 +192,19 @@ const colorContrastAPCABronzeConformanceCheck =
188
192
  const colorContrastAPCASilverRule = generateColorContrastAPCARule("silver");
189
193
  const colorContrastAPCABronzeRule = generateColorContrastAPCARule("bronze");
190
194
 
191
- const registerAxeAPCA = (conformanceLevel: "bronze" | "silver") => {
195
+ const registerAxeAPCA = (
196
+ conformanceLevel: ConformanceLevel,
197
+ customConformanceThresholdFn?: ConformanceThresholdFn
198
+ ) => {
192
199
  axe.configure({
193
200
  rules: [generateColorContrastAPCARule(conformanceLevel)],
194
201
  checks: [
195
202
  generateColorContrastAPCAConformanceCheck(
196
203
  conformanceLevel,
197
- conformanceLevel === "bronze"
198
- ? getAPCABronzeThreshold
199
- : getAPCASilverPlusThreshold
204
+ customConformanceThresholdFn ||
205
+ (conformanceLevel === "silver"
206
+ ? getAPCASilverPlusThreshold
207
+ : getAPCABronzeThreshold)
200
208
  ),
201
209
  ],
202
210
  });
@@ -5,7 +5,11 @@ import type { TemplateResult } from "lit-html";
5
5
  import axe from "axe-core";
6
6
  import registerAxeAPCA from "./axe-apca";
7
7
 
8
- registerAxeAPCA("bronze");
8
+ const customConformanceThresholdFn = (fontSize: string): number | null => {
9
+ return parseFloat(fontSize) >= 32 ? 45 : 60;
10
+ };
11
+
12
+ registerAxeAPCA("custom", customConformanceThresholdFn);
9
13
 
10
14
  const colorThemes = ["dark", "light"];
11
15
  const baseThemes = ["", "highcontrast"];
@@ -201,9 +205,7 @@ const buildTestElement = ({
201
205
  <${unsafe.tag}
202
206
  ${unsafe.attributes}
203
207
  data-testid="${testid}"
204
- >
205
- ${unsafe.children}
206
- </${unsafe.tag}>
208
+ >${unsafe.children}</${unsafe.tag}>
207
209
  `;
208
210
  };
209
211
 
@@ -325,10 +327,10 @@ const runComponentTest = ({
325
327
  axe.configure({
326
328
  rules: [
327
329
  // for non-high contrast, we disable WCAG 2.1 AA (4.5:1)
328
- // and use APCA bronze level instead
330
+ // and use a Stacks-specific APCA custom level instead
329
331
  { id: "color-contrast", enabled: false },
330
332
  {
331
- id: "color-contrast-apca-bronze",
333
+ id: "color-contrast-apca-custom",
332
334
  enabled: !highcontrast,
333
335
  },
334
336
  // for high contrast, we check against WCAG 2.1 AAA (7:1)
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "https://github.com/StackExchange/Stacks.git"
7
7
  },
8
- "version": "1.10.5",
8
+ "version": "1.10.6",
9
9
  "files": [
10
10
  "dist",
11
11
  "lib"
@@ -46,12 +46,12 @@
46
46
  "@open-wc/testing": "^3.2.0",
47
47
  "@rollup/plugin-commonjs": "^25.0.4",
48
48
  "@rollup/plugin-replace": "^5.0.2",
49
- "@stackoverflow/stacks-editor": "^0.8.8",
49
+ "@stackoverflow/stacks-editor": "^0.8.9",
50
50
  "@stackoverflow/stacks-icons": "^5.5.0",
51
- "@testing-library/dom": "^9.3.1",
52
- "@testing-library/user-event": "^14.4.3",
53
- "@typescript-eslint/eslint-plugin": "^6.5.0",
54
- "@typescript-eslint/parser": "^6.6.0",
51
+ "@testing-library/dom": "^9.3.3",
52
+ "@testing-library/user-event": "^14.5.1",
53
+ "@typescript-eslint/eslint-plugin": "^6.7.3",
54
+ "@typescript-eslint/parser": "^6.7.2",
55
55
  "@web/dev-server-esbuild": "^0.4.1",
56
56
  "@web/dev-server-rollup": "^0.5.2",
57
57
  "@web/test-runner": "^0.17.1",
@@ -64,7 +64,7 @@
64
64
  "docsearch.js": "^2.6.3",
65
65
  "eleventy-plugin-highlightjs": "^1.1.0",
66
66
  "eleventy-plugin-nesting-toc": "^1.3.0",
67
- "eslint": "^8.48.0",
67
+ "eslint": "^8.50.0",
68
68
  "eslint-config-prettier": "^9.0.0",
69
69
  "eslint-plugin-no-unsanitized": "^4.0.2",
70
70
  "jquery": "^3.7.1",