@eagami/ui 5.18.0 → 5.20.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/fesm2022/eagami-ui.mjs +1538 -339
- package/fesm2022/eagami-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/src/styles/_mixins.scss +26 -0
- package/src/styles/wcag-floors.spec.ts +122 -0
- package/types/eagami-ui.d.ts +702 -571
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eagami/ui",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.20.0",
|
|
4
4
|
"description": "Lightweight, accessible, themeable Angular UI component library and icon set built on CSS custom properties",
|
|
5
5
|
"author": "Michal Wiraszka <michal@eagami.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/styles/_mixins.scss
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// @use '../../styles/mixins' as ea;
|
|
3
3
|
// .ea-foo__close { @include ea.icon-button; }
|
|
4
4
|
|
|
5
|
+
// WCAG 2.2 SC 2.5.8 Target Size (Minimum), Level AA. Any pointer target the
|
|
6
|
+
// library ships has to reach this in at least one dimension pair, or qualify
|
|
7
|
+
// for one of the SC's exceptions (inline, spacing, essential).
|
|
8
|
+
$target-size-min: 24px;
|
|
9
|
+
|
|
5
10
|
// Standard focus indicator. The ring is a soft box-shadow halo, but box-shadow
|
|
6
11
|
// is dropped in forced-colors (Windows High Contrast) mode, which would leave
|
|
7
12
|
// keyboard users with no visible focus. Pair the halo with a real outline that
|
|
@@ -47,6 +52,8 @@
|
|
|
47
52
|
// it reads clearly inside the box regardless of how much padding the icon's own
|
|
48
53
|
// viewBox carries (the feather `x`, for instance, only fills its middle half).
|
|
49
54
|
@mixin icon-button {
|
|
55
|
+
// Anchors the grown pointer target below
|
|
56
|
+
position: relative;
|
|
50
57
|
display: inline-flex;
|
|
51
58
|
align-items: center;
|
|
52
59
|
justify-content: center;
|
|
@@ -67,6 +74,25 @@
|
|
|
67
74
|
font-size: 1.25em;
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
// The visible box is deliberately small on the dense tiers (17.5px at 2xs,
|
|
78
|
+
// 21px at xs), which is under the 24px WCAG 2.2 SC 2.5.8 target minimum.
|
|
79
|
+
// SC 2.5.8 measures the interactive target rather than the painted control,
|
|
80
|
+
// so the target is grown here and the visuals are left alone.
|
|
81
|
+
//
|
|
82
|
+
// A layout that stacks or abuts these buttons closer than the minimum must
|
|
83
|
+
// opt out via `--ea-icon-button-target: 0`, because grown targets that
|
|
84
|
+
// overlap steal each other's clicks, which is worse than the undersized
|
|
85
|
+
// targets the SC's spacing exception already covers.
|
|
86
|
+
&::after {
|
|
87
|
+
content: '';
|
|
88
|
+
position: absolute;
|
|
89
|
+
top: 50%;
|
|
90
|
+
left: 50%;
|
|
91
|
+
width: max(100%, var(--ea-icon-button-target, #{$target-size-min}));
|
|
92
|
+
height: max(100%, var(--ea-icon-button-target, #{$target-size-min}));
|
|
93
|
+
transform: translate(-50%, -50%);
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
&:hover {
|
|
71
97
|
background-color: var(--color-state-hover);
|
|
72
98
|
color: var(--color-text-primary);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Source-level guards for the accessibility floors the library commits to.
|
|
7
|
+
*
|
|
8
|
+
* These cannot be covered by the per-component axe specs: jsdom has no layout,
|
|
9
|
+
* so nothing at runtime can measure a rendered target box or a computed
|
|
10
|
+
* font-size. The floors are therefore asserted against the stylesheets that
|
|
11
|
+
* define them. Both floors regressed silently once, when the `2xs` tier scaled
|
|
12
|
+
* every em-derived dimension down a step without anything failing.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const SRC = process.cwd();
|
|
16
|
+
|
|
17
|
+
function read(relativePath: string): string {
|
|
18
|
+
return readFileSync(join(SRC, relativePath), 'utf8');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function componentStylesheets(): { name: string; css: string }[] {
|
|
22
|
+
const lib = join(SRC, 'src/lib');
|
|
23
|
+
return readdirSync(lib, { withFileTypes: true })
|
|
24
|
+
.filter(entry => entry.isDirectory())
|
|
25
|
+
.flatMap(dir =>
|
|
26
|
+
readdirSync(join(lib, dir.name))
|
|
27
|
+
.filter(file => file.endsWith('.component.scss'))
|
|
28
|
+
.map(file => ({
|
|
29
|
+
name: `${dir.name}/${file}`,
|
|
30
|
+
css: readFileSync(join(lib, dir.name, file), 'utf8'),
|
|
31
|
+
})),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Font-size in px of the smallest tier, from the typography tokens. */
|
|
36
|
+
const SMALLEST_TIER_PX = 10;
|
|
37
|
+
|
|
38
|
+
describe('WCAG floors', () => {
|
|
39
|
+
describe('SC 2.5.8 Target Size (Minimum)', () => {
|
|
40
|
+
const mixins = read('src/styles/_mixins.scss');
|
|
41
|
+
|
|
42
|
+
it('declares the 24px minimum as a single shared constant', () => {
|
|
43
|
+
expect(mixins).toMatch(/\$target-size-min:\s*24px;/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('grows the shared icon-button target to that minimum', () => {
|
|
47
|
+
const mixin = mixins.slice(mixins.indexOf('@mixin icon-button'));
|
|
48
|
+
const body = mixin.slice(0, mixin.indexOf('\n}\n'));
|
|
49
|
+
|
|
50
|
+
// The painted box stays small on the dense tiers, so the target has to be
|
|
51
|
+
// grown independently of width/height, defaulting to the shared minimum
|
|
52
|
+
expect(body).toContain('position: relative');
|
|
53
|
+
expect(body).toMatch(
|
|
54
|
+
/width:\s*max\(100%,\s*var\(--ea-icon-button-target,\s*#\{\$target-size-min\}\)\)/,
|
|
55
|
+
);
|
|
56
|
+
expect(body).toMatch(
|
|
57
|
+
/height:\s*max\(100%,\s*var\(--ea-icon-button-target,\s*#\{\$target-size-min\}\)\)/,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('makes a component that shrinks the box decide about the grown target', () => {
|
|
62
|
+
// A box smaller than the minimum either grows its target or opts out; it
|
|
63
|
+
// must not silently keep a 24px target that can overlap its neighbour
|
|
64
|
+
const shrunk = componentStylesheets().filter(({ css }) =>
|
|
65
|
+
/--ea-icon-button-size:\s*(0|1(\.\d+)?)em/.test(css),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Zero matches is a valid state: it means nothing currently shrinks the
|
|
69
|
+
// box below the minimum, which is the outcome this rule is steering toward
|
|
70
|
+
for (const { name, css } of shrunk) {
|
|
71
|
+
expect(`${name}: ${/--ea-icon-button-target:/.test(css)}`).toBe(`${name}: true`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('routes every icon button through the mixin rather than hand-rolling the box', () => {
|
|
76
|
+
const handRolled = componentStylesheets().filter(({ css }) =>
|
|
77
|
+
/\.ea-[a-z-]+__(close|clear|dismiss)\s*\{[^}]*\bwidth:/.test(css),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(handRolled.map(f => f.name)).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('Legible floors for field sub-text', () => {
|
|
85
|
+
it('floors the field label at 12px however far the tier scales down', () => {
|
|
86
|
+
const css = read('src/lib/field/field-label.component.scss');
|
|
87
|
+
|
|
88
|
+
expect(css).toMatch(
|
|
89
|
+
/font-size:\s*max\(var\(--ea-field-label-size[^)]*\)[^,]*,\s*0\.75rem\)/,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('floors error and hint text at 11px however far the tier scales down', () => {
|
|
94
|
+
const css = read('src/lib/field/field-messages.component.scss');
|
|
95
|
+
|
|
96
|
+
expect(css).toMatch(
|
|
97
|
+
/font-size:\s*max\(var\(--ea-field-messages-size[^)]*\)[^,]*,\s*0\.6875rem\)/,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('keeps the floor at the point of use, not in each component that sets the variable', () => {
|
|
102
|
+
// 18 components declare these variables; a floor copied into each would
|
|
103
|
+
// be missed by the next one added
|
|
104
|
+
const setters = componentStylesheets().filter(({ css }) =>
|
|
105
|
+
/--ea-field-(label|messages)-size:/.test(css),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(setters.length).toBeGreaterThan(10);
|
|
109
|
+
for (const { name, css } of setters) {
|
|
110
|
+
expect(`${name}: ${/--ea-field-\w+-size:\s*max\(/.test(css)}`).toBe(
|
|
111
|
+
`${name}: false`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('would otherwise scale sub-text below the floors at the smallest tier', () => {
|
|
117
|
+
// Documents why the floors exist: the unclamped em values at 2xs
|
|
118
|
+
expect(SMALLEST_TIER_PX * 0.875).toBeLessThan(12);
|
|
119
|
+
expect(SMALLEST_TIER_PX * 0.8125).toBeLessThan(11);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|