@stackoverflow/stacks 2.5.8 → 2.7.0
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.
- package/dist/css/stacks.css +91 -65
- package/dist/css/stacks.min.css +1 -1
- package/dist/js/stacks.js +2 -5
- package/lib/components/badge/badge.less +269 -258
- package/lib/components/button/button.less +35 -5
- package/lib/components/expandable/expandable.ts +238 -238
- package/lib/components/input_textarea/input_textarea.less +150 -150
- package/lib/components/link/link.less +136 -136
- package/lib/components/pagination/pagination.less +77 -65
- package/lib/components/sidebar-widget/sidebar-widget.less +34 -73
- package/lib/components/table/table.less +309 -307
- package/lib/components/uploader/uploader.ts +207 -207
- package/lib/exports/color-sets.less +711 -711
- package/lib/exports/color.less +65 -65
- package/lib/stacks.ts +113 -113
- package/lib/test/a11y-test-utils.ts +94 -94
- package/lib/test/test-utils.ts +364 -364
- package/package.json +12 -12
package/lib/exports/color.less
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
@import (reference) "./color-mixins.less";
|
|
2
|
-
|
|
3
|
-
body {
|
|
4
|
-
--_o-disabled: 0.
|
|
5
|
-
--_o-disabled-static: 0.
|
|
6
|
-
--_black-static: .set-black()[default];
|
|
7
|
-
--_white-static: .set-white()[default];
|
|
8
|
-
|
|
9
|
-
&,
|
|
10
|
-
&.theme-highcontrast,
|
|
11
|
-
&:not(.theme-highcontrast) {
|
|
12
|
-
&:not(.theme-dark),
|
|
13
|
-
&.theme-dark .theme-light__forced,
|
|
14
|
-
&.theme-system .theme-light__forced,
|
|
15
|
-
&.theme-dark,
|
|
16
|
-
&:not(.theme-dark) .theme-dark__forced {
|
|
17
|
-
.create-colors(.sets-aliased-utility-variables());
|
|
18
|
-
}
|
|
19
|
-
&.theme-system {
|
|
20
|
-
@media (prefers-color-scheme: dark) {
|
|
21
|
-
.create-colors(.sets-aliased-utility-variables());
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Light, dark mode
|
|
28
|
-
body:not(.theme-highcontrast) {
|
|
29
|
-
// Light mode
|
|
30
|
-
&:not(.theme-dark),
|
|
31
|
-
&.theme-dark .theme-light__forced,
|
|
32
|
-
&.theme-system .theme-light__forced {
|
|
33
|
-
.generate-colors(light);
|
|
34
|
-
}
|
|
35
|
-
// Dark mode
|
|
36
|
-
&.theme-dark,
|
|
37
|
-
&:not(.theme-dark) .theme-dark__forced {
|
|
38
|
-
.generate-colors(dark);
|
|
39
|
-
}
|
|
40
|
-
&.theme-system {
|
|
41
|
-
@media (prefers-color-scheme: dark) {
|
|
42
|
-
.generate-colors(dark);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// High contrast mode
|
|
48
|
-
body.theme-highcontrast {
|
|
49
|
-
// Light high contrast mode
|
|
50
|
-
&:not(.theme-dark),
|
|
51
|
-
&.theme-dark .theme-light__forced,
|
|
52
|
-
&.theme-system .theme-light__forced {
|
|
53
|
-
.generate-colors(light-highcontrast);
|
|
54
|
-
}
|
|
55
|
-
// Dark high contrast mode
|
|
56
|
-
&.theme-dark,
|
|
57
|
-
&:not(.theme-dark) .theme-dark__forced {
|
|
58
|
-
.generate-colors(dark-highcontrast);
|
|
59
|
-
}
|
|
60
|
-
&.theme-system {
|
|
61
|
-
@media (prefers-color-scheme: dark) {
|
|
62
|
-
.generate-colors(dark-highcontrast);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
@import (reference) "./color-mixins.less";
|
|
2
|
+
|
|
3
|
+
body {
|
|
4
|
+
--_o-disabled: 0.55;
|
|
5
|
+
--_o-disabled-static: 0.55;
|
|
6
|
+
--_black-static: .set-black()[default];
|
|
7
|
+
--_white-static: .set-white()[default];
|
|
8
|
+
|
|
9
|
+
&,
|
|
10
|
+
&.theme-highcontrast,
|
|
11
|
+
&:not(.theme-highcontrast) {
|
|
12
|
+
&:not(.theme-dark),
|
|
13
|
+
&.theme-dark .theme-light__forced,
|
|
14
|
+
&.theme-system .theme-light__forced,
|
|
15
|
+
&.theme-dark,
|
|
16
|
+
&:not(.theme-dark) .theme-dark__forced {
|
|
17
|
+
.create-colors(.sets-aliased-utility-variables());
|
|
18
|
+
}
|
|
19
|
+
&.theme-system {
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
.create-colors(.sets-aliased-utility-variables());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Light, dark mode
|
|
28
|
+
body:not(.theme-highcontrast) {
|
|
29
|
+
// Light mode
|
|
30
|
+
&:not(.theme-dark),
|
|
31
|
+
&.theme-dark .theme-light__forced,
|
|
32
|
+
&.theme-system .theme-light__forced {
|
|
33
|
+
.generate-colors(light);
|
|
34
|
+
}
|
|
35
|
+
// Dark mode
|
|
36
|
+
&.theme-dark,
|
|
37
|
+
&:not(.theme-dark) .theme-dark__forced {
|
|
38
|
+
.generate-colors(dark);
|
|
39
|
+
}
|
|
40
|
+
&.theme-system {
|
|
41
|
+
@media (prefers-color-scheme: dark) {
|
|
42
|
+
.generate-colors(dark);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// High contrast mode
|
|
48
|
+
body.theme-highcontrast {
|
|
49
|
+
// Light high contrast mode
|
|
50
|
+
&:not(.theme-dark),
|
|
51
|
+
&.theme-dark .theme-light__forced,
|
|
52
|
+
&.theme-system .theme-light__forced {
|
|
53
|
+
.generate-colors(light-highcontrast);
|
|
54
|
+
}
|
|
55
|
+
// Dark high contrast mode
|
|
56
|
+
&.theme-dark,
|
|
57
|
+
&:not(.theme-dark) .theme-dark__forced {
|
|
58
|
+
.generate-colors(dark-highcontrast);
|
|
59
|
+
}
|
|
60
|
+
&.theme-system {
|
|
61
|
+
@media (prefers-color-scheme: dark) {
|
|
62
|
+
.generate-colors(dark-highcontrast);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/lib/stacks.ts
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
import * as Stimulus from "@hotwired/stimulus";
|
|
2
|
-
|
|
3
|
-
export class StacksApplication extends Stimulus.Application {
|
|
4
|
-
static _initializing = true;
|
|
5
|
-
|
|
6
|
-
load(...definitions: Stimulus.Definition[]): void;
|
|
7
|
-
load(definitions: Stimulus.Definition[]): void;
|
|
8
|
-
load(
|
|
9
|
-
head: Stimulus.Definition | Stimulus.Definition[],
|
|
10
|
-
...rest: Stimulus.Definition[]
|
|
11
|
-
) {
|
|
12
|
-
const definitions = Array.isArray(head) ? head : [head, ...rest];
|
|
13
|
-
|
|
14
|
-
for (const definition of definitions) {
|
|
15
|
-
const hasPrefix = /^s-/.test(definition.identifier);
|
|
16
|
-
if (StacksApplication._initializing && !hasPrefix) {
|
|
17
|
-
throw 'Stacks-created Stimulus controller names must start with "s-".';
|
|
18
|
-
}
|
|
19
|
-
if (!StacksApplication._initializing && hasPrefix) {
|
|
20
|
-
throw 'The "s-" prefix on Stimulus controller names is reserved for Stacks-created controllers.';
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
super.load(definitions);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
static start(
|
|
28
|
-
element?: Element,
|
|
29
|
-
schema?: Stimulus.Schema
|
|
30
|
-
): StacksApplication {
|
|
31
|
-
const application = new StacksApplication(element, schema);
|
|
32
|
-
|
|
33
|
-
application.start();
|
|
34
|
-
return application;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static finalize() {
|
|
38
|
-
StacksApplication._initializing = false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const application: Stimulus.Application = StacksApplication.start();
|
|
43
|
-
|
|
44
|
-
export class StacksController extends Stimulus.Controller {
|
|
45
|
-
protected getElementData(element: Element, key: string) {
|
|
46
|
-
return element.getAttribute("data-" + this.identifier + "-" + key);
|
|
47
|
-
}
|
|
48
|
-
protected setElementData(element: Element, key: string, value: string) {
|
|
49
|
-
element.setAttribute("data-" + this.identifier + "-" + key, value);
|
|
50
|
-
}
|
|
51
|
-
protected removeElementData(element: Element, key: string) {
|
|
52
|
-
element.removeAttribute("data-" + this.identifier + "-" + key);
|
|
53
|
-
}
|
|
54
|
-
protected triggerEvent<T>(
|
|
55
|
-
eventName: string,
|
|
56
|
-
detail?: T,
|
|
57
|
-
optionalElement?: Element
|
|
58
|
-
) {
|
|
59
|
-
const namespacedName = this.identifier + ":" + eventName;
|
|
60
|
-
let event: CustomEvent<T>;
|
|
61
|
-
try {
|
|
62
|
-
event = new CustomEvent(namespacedName, {
|
|
63
|
-
bubbles: true,
|
|
64
|
-
cancelable: true,
|
|
65
|
-
detail: detail,
|
|
66
|
-
});
|
|
67
|
-
} catch
|
|
68
|
-
// Internet Explorer
|
|
69
|
-
|
|
70
|
-
event = document.createEvent("CustomEvent");
|
|
71
|
-
event.initCustomEvent(namespacedName, true, true, detail);
|
|
72
|
-
}
|
|
73
|
-
(optionalElement || this.element).dispatchEvent(event);
|
|
74
|
-
return event;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ControllerDefinition/createController/addController is here to make
|
|
79
|
-
// it easier to consume Stimulus from ES5 files (without classes)
|
|
80
|
-
export interface ControllerDefinition {
|
|
81
|
-
[name: string]: unknown;
|
|
82
|
-
targets?: string[];
|
|
83
|
-
}
|
|
84
|
-
export function createController(
|
|
85
|
-
controllerDefinition: ControllerDefinition
|
|
86
|
-
): typeof StacksController {
|
|
87
|
-
// eslint-disable-next-line no-prototype-builtins
|
|
88
|
-
const Controller = controllerDefinition.hasOwnProperty("targets")
|
|
89
|
-
? class Controller extends StacksController {
|
|
90
|
-
static targets = controllerDefinition.targets ?? [];
|
|
91
|
-
}
|
|
92
|
-
: class Controller extends StacksController {};
|
|
93
|
-
|
|
94
|
-
for (const prop in controllerDefinition) {
|
|
95
|
-
const ownPropDescriptor =
|
|
96
|
-
// eslint-disable-next-line no-prototype-builtins
|
|
97
|
-
controllerDefinition.hasOwnProperty(prop) &&
|
|
98
|
-
Object.getOwnPropertyDescriptor(controllerDefinition, prop);
|
|
99
|
-
if (prop !== "targets" && ownPropDescriptor) {
|
|
100
|
-
Object.defineProperty(
|
|
101
|
-
Controller.prototype,
|
|
102
|
-
prop,
|
|
103
|
-
ownPropDescriptor
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return Controller;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function addController(name: string, controller: ControllerDefinition) {
|
|
112
|
-
application.register(name, createController(controller));
|
|
113
|
-
}
|
|
1
|
+
import * as Stimulus from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export class StacksApplication extends Stimulus.Application {
|
|
4
|
+
static _initializing = true;
|
|
5
|
+
|
|
6
|
+
load(...definitions: Stimulus.Definition[]): void;
|
|
7
|
+
load(definitions: Stimulus.Definition[]): void;
|
|
8
|
+
load(
|
|
9
|
+
head: Stimulus.Definition | Stimulus.Definition[],
|
|
10
|
+
...rest: Stimulus.Definition[]
|
|
11
|
+
) {
|
|
12
|
+
const definitions = Array.isArray(head) ? head : [head, ...rest];
|
|
13
|
+
|
|
14
|
+
for (const definition of definitions) {
|
|
15
|
+
const hasPrefix = /^s-/.test(definition.identifier);
|
|
16
|
+
if (StacksApplication._initializing && !hasPrefix) {
|
|
17
|
+
throw 'Stacks-created Stimulus controller names must start with "s-".';
|
|
18
|
+
}
|
|
19
|
+
if (!StacksApplication._initializing && hasPrefix) {
|
|
20
|
+
throw 'The "s-" prefix on Stimulus controller names is reserved for Stacks-created controllers.';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
super.load(definitions);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static start(
|
|
28
|
+
element?: Element,
|
|
29
|
+
schema?: Stimulus.Schema
|
|
30
|
+
): StacksApplication {
|
|
31
|
+
const application = new StacksApplication(element, schema);
|
|
32
|
+
|
|
33
|
+
application.start();
|
|
34
|
+
return application;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static finalize() {
|
|
38
|
+
StacksApplication._initializing = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const application: Stimulus.Application = StacksApplication.start();
|
|
43
|
+
|
|
44
|
+
export class StacksController extends Stimulus.Controller {
|
|
45
|
+
protected getElementData(element: Element, key: string) {
|
|
46
|
+
return element.getAttribute("data-" + this.identifier + "-" + key);
|
|
47
|
+
}
|
|
48
|
+
protected setElementData(element: Element, key: string, value: string) {
|
|
49
|
+
element.setAttribute("data-" + this.identifier + "-" + key, value);
|
|
50
|
+
}
|
|
51
|
+
protected removeElementData(element: Element, key: string) {
|
|
52
|
+
element.removeAttribute("data-" + this.identifier + "-" + key);
|
|
53
|
+
}
|
|
54
|
+
protected triggerEvent<T>(
|
|
55
|
+
eventName: string,
|
|
56
|
+
detail?: T,
|
|
57
|
+
optionalElement?: Element
|
|
58
|
+
) {
|
|
59
|
+
const namespacedName = this.identifier + ":" + eventName;
|
|
60
|
+
let event: CustomEvent<T>;
|
|
61
|
+
try {
|
|
62
|
+
event = new CustomEvent(namespacedName, {
|
|
63
|
+
bubbles: true,
|
|
64
|
+
cancelable: true,
|
|
65
|
+
detail: detail,
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
// Internet Explorer
|
|
69
|
+
|
|
70
|
+
event = document.createEvent("CustomEvent");
|
|
71
|
+
event.initCustomEvent(namespacedName, true, true, detail);
|
|
72
|
+
}
|
|
73
|
+
(optionalElement || this.element).dispatchEvent(event);
|
|
74
|
+
return event;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ControllerDefinition/createController/addController is here to make
|
|
79
|
+
// it easier to consume Stimulus from ES5 files (without classes)
|
|
80
|
+
export interface ControllerDefinition {
|
|
81
|
+
[name: string]: unknown;
|
|
82
|
+
targets?: string[];
|
|
83
|
+
}
|
|
84
|
+
export function createController(
|
|
85
|
+
controllerDefinition: ControllerDefinition
|
|
86
|
+
): typeof StacksController {
|
|
87
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
88
|
+
const Controller = controllerDefinition.hasOwnProperty("targets")
|
|
89
|
+
? class Controller extends StacksController {
|
|
90
|
+
static targets = controllerDefinition.targets ?? [];
|
|
91
|
+
}
|
|
92
|
+
: class Controller extends StacksController {};
|
|
93
|
+
|
|
94
|
+
for (const prop in controllerDefinition) {
|
|
95
|
+
const ownPropDescriptor =
|
|
96
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
97
|
+
controllerDefinition.hasOwnProperty(prop) &&
|
|
98
|
+
Object.getOwnPropertyDescriptor(controllerDefinition, prop);
|
|
99
|
+
if (prop !== "targets" && ownPropDescriptor) {
|
|
100
|
+
Object.defineProperty(
|
|
101
|
+
Controller.prototype,
|
|
102
|
+
prop,
|
|
103
|
+
ownPropDescriptor
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return Controller;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function addController(name: string, controller: ControllerDefinition) {
|
|
112
|
+
application.register(name, createController(controller));
|
|
113
|
+
}
|
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
import { html, fixture, expect } from "@open-wc/testing";
|
|
2
|
-
import { screen } from "@testing-library/dom";
|
|
3
|
-
import axe from "axe-core";
|
|
4
|
-
import registerAPCACheck from "apca-check";
|
|
5
|
-
import { generateTestVariations, type TestVariationArgs } from "./test-utils";
|
|
6
|
-
import type { AdditionalAssertion } from "./assertions";
|
|
7
|
-
|
|
8
|
-
type A11yTestArgs = TestVariationArgs & {
|
|
9
|
-
/**
|
|
10
|
-
* Additional assertions to run against the test element
|
|
11
|
-
*/
|
|
12
|
-
additionalAssertions?: AdditionalAssertion[];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
// register Stack APCA conformance threshold function
|
|
16
|
-
// see also https://stackoverflow.design/product/foundation/accessibility/#visual-accessibility
|
|
17
|
-
const customConformanceThresholdFn = (fontSize: string): number | null => {
|
|
18
|
-
// if the font size is 32px or larger, we use a 45Lc threshold
|
|
19
|
-
// otherwise, we use a 60Lc threshold
|
|
20
|
-
return parseFloat(fontSize) >= 32 ? 45 : 60;
|
|
21
|
-
};
|
|
22
|
-
registerAPCACheck("custom", customConformanceThresholdFn);
|
|
23
|
-
|
|
24
|
-
const scheduleA11yTest = ({
|
|
25
|
-
element,
|
|
26
|
-
testid,
|
|
27
|
-
theme,
|
|
28
|
-
additionalAssertions = [],
|
|
29
|
-
}: {
|
|
30
|
-
element: ReturnType<typeof html>;
|
|
31
|
-
testid: string;
|
|
32
|
-
theme: string[];
|
|
33
|
-
additionalAssertions?: AdditionalAssertion[];
|
|
34
|
-
}) => {
|
|
35
|
-
it(`a11y: ${testid} should be accessible`, async () => {
|
|
36
|
-
await fixture(element);
|
|
37
|
-
const el = screen.getByTestId(testid);
|
|
38
|
-
|
|
39
|
-
document.body.className = "";
|
|
40
|
-
|
|
41
|
-
if (theme?.length) {
|
|
42
|
-
const prefixedThemes = theme.map((t) => `theme-${t}`);
|
|
43
|
-
document.body.classList.add(...prefixedThemes);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const highcontrast = theme?.includes("highcontrast");
|
|
47
|
-
|
|
48
|
-
axe.configure({
|
|
49
|
-
rules: [
|
|
50
|
-
// for non-high contrast, we disable WCAG 2.1 AA (4.5:1)
|
|
51
|
-
// and use a Stacks-specific APCA custom level instead
|
|
52
|
-
{ id: "color-contrast", enabled: false },
|
|
53
|
-
{
|
|
54
|
-
id: "color-contrast-apca-custom",
|
|
55
|
-
enabled: !highcontrast,
|
|
56
|
-
},
|
|
57
|
-
// for high contrast, we check against WCAG 2.1 AAA (7:1)
|
|
58
|
-
{ id: "color-contrast-enhanced", enabled: highcontrast },
|
|
59
|
-
],
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
await expect(el).to.be.accessible();
|
|
63
|
-
el.remove();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
additionalAssertions.forEach((assertion) => {
|
|
67
|
-
it(`a11y: ${testid} ${assertion.description}`, async () => {
|
|
68
|
-
await fixture(element);
|
|
69
|
-
const el = screen.getByTestId(testid);
|
|
70
|
-
await assertion.assertion(el);
|
|
71
|
-
el.remove();
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const runA11yTests = (args: A11yTestArgs) => {
|
|
77
|
-
const testVariations = generateTestVariations(args);
|
|
78
|
-
testVariations.forEach((variation) => {
|
|
79
|
-
if (variation.skipped) {
|
|
80
|
-
it.skip(`a11y: ${variation.testid} (skipped)`, () => {
|
|
81
|
-
return;
|
|
82
|
-
});
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
scheduleA11yTest({
|
|
87
|
-
...variation,
|
|
88
|
-
additionalAssertions: args.additionalAssertions,
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export type { AdditionalAssertion };
|
|
94
|
-
export { runA11yTests };
|
|
1
|
+
import { html, fixture, expect } from "@open-wc/testing";
|
|
2
|
+
import { screen } from "@testing-library/dom";
|
|
3
|
+
import axe from "axe-core";
|
|
4
|
+
import registerAPCACheck from "apca-check";
|
|
5
|
+
import { generateTestVariations, type TestVariationArgs } from "./test-utils";
|
|
6
|
+
import type { AdditionalAssertion } from "./assertions";
|
|
7
|
+
|
|
8
|
+
type A11yTestArgs = TestVariationArgs & {
|
|
9
|
+
/**
|
|
10
|
+
* Additional assertions to run against the test element
|
|
11
|
+
*/
|
|
12
|
+
additionalAssertions?: AdditionalAssertion[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// register Stack APCA conformance threshold function
|
|
16
|
+
// see also https://stackoverflow.design/product/foundation/accessibility/#visual-accessibility
|
|
17
|
+
const customConformanceThresholdFn = (fontSize: string): number | null => {
|
|
18
|
+
// if the font size is 32px or larger, we use a 45Lc threshold
|
|
19
|
+
// otherwise, we use a 60Lc threshold
|
|
20
|
+
return parseFloat(fontSize) >= 32 ? 45 : 60;
|
|
21
|
+
};
|
|
22
|
+
registerAPCACheck("custom", customConformanceThresholdFn);
|
|
23
|
+
|
|
24
|
+
const scheduleA11yTest = ({
|
|
25
|
+
element,
|
|
26
|
+
testid,
|
|
27
|
+
theme,
|
|
28
|
+
additionalAssertions = [],
|
|
29
|
+
}: {
|
|
30
|
+
element: ReturnType<typeof html>;
|
|
31
|
+
testid: string;
|
|
32
|
+
theme: string[];
|
|
33
|
+
additionalAssertions?: AdditionalAssertion[];
|
|
34
|
+
}) => {
|
|
35
|
+
it(`a11y: ${testid} should be accessible`, async () => {
|
|
36
|
+
await fixture(element);
|
|
37
|
+
const el = screen.getByTestId(testid);
|
|
38
|
+
|
|
39
|
+
document.body.className = "";
|
|
40
|
+
|
|
41
|
+
if (theme?.length) {
|
|
42
|
+
const prefixedThemes = theme.map((t) => `theme-${t}`);
|
|
43
|
+
document.body.classList.add(...prefixedThemes);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const highcontrast = theme?.includes("highcontrast");
|
|
47
|
+
|
|
48
|
+
axe.configure({
|
|
49
|
+
rules: [
|
|
50
|
+
// for non-high contrast, we disable WCAG 2.1 AA (4.5:1)
|
|
51
|
+
// and use a Stacks-specific APCA custom level instead
|
|
52
|
+
{ id: "color-contrast", enabled: false },
|
|
53
|
+
{
|
|
54
|
+
id: "color-contrast-apca-custom",
|
|
55
|
+
enabled: !highcontrast,
|
|
56
|
+
},
|
|
57
|
+
// for high contrast, we check against WCAG 2.1 AAA (7:1)
|
|
58
|
+
{ id: "color-contrast-enhanced", enabled: highcontrast },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await expect(el).to.be.accessible();
|
|
63
|
+
el.remove();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
additionalAssertions.forEach((assertion) => {
|
|
67
|
+
it(`a11y: ${testid} ${assertion.description}`, async () => {
|
|
68
|
+
await fixture(element);
|
|
69
|
+
const el = screen.getByTestId(testid);
|
|
70
|
+
await assertion.assertion(el);
|
|
71
|
+
el.remove();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const runA11yTests = (args: A11yTestArgs) => {
|
|
77
|
+
const testVariations = generateTestVariations(args);
|
|
78
|
+
testVariations.forEach((variation) => {
|
|
79
|
+
if (variation.skipped) {
|
|
80
|
+
it.skip(`a11y: ${variation.testid} (skipped)`, () => {
|
|
81
|
+
return;
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
scheduleA11yTest({
|
|
87
|
+
...variation,
|
|
88
|
+
additionalAssertions: args.additionalAssertions,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type { AdditionalAssertion };
|
|
94
|
+
export { runA11yTests };
|