tailwind-a11y 0.13.0 → 0.13.5
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.
|
@@ -26,16 +26,28 @@ const NON_COLOR_SCALE_NAMES = new Set(["opacity", "linear", "conic"]);
|
|
|
26
26
|
export function lastColorToken(className, prefix) {
|
|
27
27
|
let found = null;
|
|
28
28
|
for (const raw of className.split(/\s+/).filter(Boolean)) {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// Variant-scoped classes (hover:/dark:/md:/...) are skipped entirely,
|
|
30
|
+
// not stripped down to their base utility -- fixed after independent
|
|
31
|
+
// testing found a real false negative: `bg-white dark:bg-gray-900`
|
|
32
|
+
// with `text-gray-300` silently passed, because stripping the `dark:`
|
|
33
|
+
// prefix let it participate in last-token-wins as if it were the real,
|
|
34
|
+
// always-rendered resting-state background, when `dark:bg-gray-900`
|
|
35
|
+
// only ever applies under a completely different condition. Mirrors
|
|
36
|
+
// `extractTouchTargets.ts`'s `lastSizeToken`, which already excludes
|
|
37
|
+
// any variant-scoped size token from resting-state resolution the same
|
|
38
|
+
// way (`if (raw.includes(":")) continue;`) -- this had no equivalent
|
|
39
|
+
// guard for colors.
|
|
40
|
+
if (raw.includes(":"))
|
|
31
41
|
continue;
|
|
32
|
-
|
|
42
|
+
if (!raw.startsWith(`${prefix}-`))
|
|
43
|
+
continue;
|
|
44
|
+
const rest = raw.slice(prefix.length + 1);
|
|
33
45
|
if (!COLOR_TOKEN.test(rest))
|
|
34
46
|
continue;
|
|
35
47
|
const scaleName = /^([a-z]+)-\d/.exec(rest)?.[1];
|
|
36
48
|
if (scaleName && NON_COLOR_SCALE_NAMES.has(scaleName))
|
|
37
49
|
continue;
|
|
38
|
-
found =
|
|
50
|
+
found = raw;
|
|
39
51
|
}
|
|
40
52
|
return found;
|
|
41
53
|
}
|
|
@@ -131,6 +131,24 @@ export function suggestContrastFix(textClass, bgClass, required, palette = defau
|
|
|
131
131
|
}
|
|
132
132
|
return null;
|
|
133
133
|
}
|
|
134
|
+
// resolveColorValue() bails out on any bg-side opacity modifier before ever
|
|
135
|
+
// checking whether the underlying color is real (background-side opacity
|
|
136
|
+
// compositing is out of scope -- see CLAUDE.md -- since it depends on
|
|
137
|
+
// knowing what's rendered behind an already-semi-transparent background).
|
|
138
|
+
// Caught in independent adversarial testing: this made checkContrastValueSkips
|
|
139
|
+
// report `bg-gray-800/50 is not a recognized color`, even though gray-800
|
|
140
|
+
// is a perfectly recognized default-palette color -- a developer reading
|
|
141
|
+
// that message would reasonably (and pointlessly) try adding a theme entry
|
|
142
|
+
// for it. Distinguishes the two cases by re-resolving the color with the
|
|
143
|
+
// opacity suffix stripped off: if that succeeds, the real reason is the
|
|
144
|
+
// out-of-scope opacity, not an unrecognized color.
|
|
145
|
+
function bgSkipReason(bgColorClass, palette) {
|
|
146
|
+
const { base, alpha } = splitOpacityModifier(bgColorClass);
|
|
147
|
+
if (alpha < 1 && resolveColorValue(base, palette) !== null) {
|
|
148
|
+
return `${bgColorClass} is a recognized color, but background-side opacity isn't resolved (compositing it correctly requires knowing what's rendered behind it) — skipped`;
|
|
149
|
+
}
|
|
150
|
+
return `${bgColorClass} is not a recognized color (custom theme color or unsupported arbitrary value) — skipped`;
|
|
151
|
+
}
|
|
134
152
|
// A candidate that extractChecks *did* find a background for, but whose
|
|
135
153
|
// text or bg utility didn't resolve to a known value (custom theme color,
|
|
136
154
|
// non-hex arbitrary value, background-side opacity shorthand) — surfaced
|
|
@@ -150,11 +168,7 @@ export function checkContrastValueSkips(checks, palette = defaultPalette) {
|
|
|
150
168
|
const bgHex = resolveColorValue(check.bgColorClass, palette);
|
|
151
169
|
const bgRgb = bgHex ? hexToRgb(bgHex) : null;
|
|
152
170
|
if (!bgRgb) {
|
|
153
|
-
skips.push({
|
|
154
|
-
file: check.file,
|
|
155
|
-
line: check.line,
|
|
156
|
-
reason: `${check.bgColorClass} is not a recognized color (custom theme color or unsupported arbitrary value) — skipped`,
|
|
157
|
-
});
|
|
171
|
+
skips.push({ file: check.file, line: check.line, reason: bgSkipReason(check.bgColorClass, palette) });
|
|
158
172
|
continue;
|
|
159
173
|
}
|
|
160
174
|
const { alpha } = splitOpacityModifier(check.textColorClass);
|
|
@@ -11,9 +11,15 @@ const REMOVAL_BASE = "outline-none";
|
|
|
11
11
|
// resets every element to `border: 0 solid`, so border-width stays 0
|
|
12
12
|
// regardless of style and no border is ever drawn, same failure mode as
|
|
13
13
|
// border-0.
|
|
14
|
+
// inset-shadow-none/inset-ring-0 (Tailwind v4's inset-* box-shadow family,
|
|
15
|
+
// missed by the original one-time audit -- these utilities didn't exist,
|
|
16
|
+
// or weren't considered, at the time) are the exact same failure mode as
|
|
17
|
+
// shadow-none/ring-0: verified against a real build that inset-shadow-none
|
|
18
|
+
// computes to a fully transparent shadow and inset-ring-0 computes to a
|
|
19
|
+
// 0px-wide inset ring, both real but invisible.
|
|
14
20
|
const DEGENERATE_BASES = new Set([
|
|
15
21
|
"outline-none", "ring-0", "border-0", "shadow-none", "bg-transparent",
|
|
16
|
-
"border-none", "border-hidden",
|
|
22
|
+
"border-none", "border-hidden", "inset-shadow-none", "inset-ring-0",
|
|
17
23
|
]);
|
|
18
24
|
// Modifier-only utilities (opacity/offset/inset) don't set a concrete value
|
|
19
25
|
// on their own — e.g. bg-opacity-50 with no bg-* color, or ring-offset-4
|
|
@@ -58,16 +64,35 @@ const NON_VISUAL_PATTERN = /^bg-blend-|^border-spacing(-[xy])?-/;
|
|
|
58
64
|
// offset cases above, but shaped like a color token rather than a fixed
|
|
59
65
|
// suffix, so it reuses COLOR_TOKEN (the exact same "is this a color value"
|
|
60
66
|
// test extractClasses.ts uses) instead of a third, drifting definition of
|
|
61
|
-
// what a color looks like.
|
|
67
|
+
// what a color looks like. inset-shadow-{color}/inset-ring-{color} are the
|
|
68
|
+
// same mechanism one prefix-family over (verified against a real build:
|
|
69
|
+
// inset-shadow-blue-500 alone only sets --tw-inset-shadow-color,
|
|
70
|
+
// inset-ring-blue-500 alone only sets --tw-inset-ring-color -- neither
|
|
71
|
+
// produces a box-shadow without a companion size utility).
|
|
62
72
|
function isColorOnlyShadowOrRing(base) {
|
|
63
|
-
const shadowMatch = /^shadow-(.+)$/.exec(base);
|
|
73
|
+
const shadowMatch = /^(?:inset-)?shadow-(.+)$/.exec(base);
|
|
64
74
|
if (shadowMatch && COLOR_TOKEN.test(shadowMatch[1]))
|
|
65
75
|
return true;
|
|
66
|
-
const ringMatch = /^ring-(.+)$/.exec(base);
|
|
76
|
+
const ringMatch = /^(?:inset-)?ring-(.+)$/.exec(base);
|
|
67
77
|
if (ringMatch && COLOR_TOKEN.test(ringMatch[1]))
|
|
68
78
|
return true;
|
|
69
79
|
return false;
|
|
70
80
|
}
|
|
81
|
+
// ring-offset-{color} (e.g. ring-offset-blue-500) sets only the
|
|
82
|
+
// --tw-ring-offset-color CSS variable -- the offset ring itself is only
|
|
83
|
+
// ever drawn when a real ring width is *also* present (ring-2, etc.),
|
|
84
|
+
// verified against a real Tailwind v4 build. MODIFIER_ONLY above already
|
|
85
|
+
// excludes the numeric width form (ring-offset-4), but its regex requires
|
|
86
|
+
// digits after "offset-", so it never matched this color-shaped form --
|
|
87
|
+
// caught in independent adversarial testing (a real false negative: this
|
|
88
|
+
// fell through to the generic ring-* prefix match at the bottom of
|
|
89
|
+
// isReplacement and was silently accepted as a real replacement). One
|
|
90
|
+
// level deeper than isColorOnlyShadowOrRing above, so a separate check
|
|
91
|
+
// rather than folding into it.
|
|
92
|
+
function isColorOnlyRingOffset(base) {
|
|
93
|
+
const match = /^ring-offset-(.+)$/.exec(base);
|
|
94
|
+
return !!match && COLOR_TOKEN.test(match[1]);
|
|
95
|
+
}
|
|
71
96
|
function baseUtility(raw) {
|
|
72
97
|
return raw.slice(raw.lastIndexOf(":") + 1);
|
|
73
98
|
}
|
|
@@ -79,7 +104,16 @@ function isReplacement(raw) {
|
|
|
79
104
|
return false;
|
|
80
105
|
if (isColorOnlyShadowOrRing(base))
|
|
81
106
|
return false;
|
|
82
|
-
|
|
107
|
+
if (isColorOnlyRingOffset(base))
|
|
108
|
+
return false;
|
|
109
|
+
// inset-shadow-*/inset-ring-* (Tailwind v4) are their own prefix
|
|
110
|
+
// families, not `ring`/`shadow` with a suffix -- a string starting with
|
|
111
|
+
// "inset-" doesn't match the "shadow"/"ring" alternatives below at all,
|
|
112
|
+
// so a real inset-shadow-sm/inset-ring-2 replacement was previously
|
|
113
|
+
// rejected outright (a false positive on the overall check: it reported
|
|
114
|
+
// the outline as removed with nothing put back, when something real
|
|
115
|
+
// was). Caught in independent adversarial testing.
|
|
116
|
+
return /^(ring|border|shadow|bg|outline|inset-shadow|inset-ring)(-|$)/.test(base);
|
|
83
117
|
}
|
|
84
118
|
export function checkFocusIndicators(checks) {
|
|
85
119
|
const violations = [];
|
|
@@ -115,23 +149,30 @@ const FOCUS_INDICATOR_MIN_THICKNESS_PX = 2;
|
|
|
115
149
|
// can't currently distinguish, so bare ring/outline contributes color (if
|
|
116
150
|
// paired with an explicit color utility) but never a thickness value.
|
|
117
151
|
const WIDTH_SCALE = { "0": 0, "1": 1, "2": 2, "4": 4, "8": 8 };
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// last-token-wins
|
|
121
|
-
//
|
|
122
|
-
// which
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
152
|
+
// Last-token-wins WITHIN one prefix only. Fixed after independent testing
|
|
153
|
+
// found a real bug: the original version raced outline-* against ring-*
|
|
154
|
+
// in one shared last-token-wins slot, so an element with BOTH a passing
|
|
155
|
+
// outline-* and a failing ring-* (or vice versa) got a verdict that
|
|
156
|
+
// depended purely on which was written later in the class string -- even
|
|
157
|
+
// though outline-*/ring-* are two independent CSS mechanisms
|
|
158
|
+
// (outline-color/-width vs. box-shadow) that both render simultaneously
|
|
159
|
+
// regardless of order (verified against a real Tailwind v4 build).
|
|
160
|
+
// Last-token-wins is still correct *within* a single prefix (e.g.
|
|
161
|
+
// `outline-red-500 outline-blue-600` really does render as blue-600, the
|
|
162
|
+
// later declaration winning in CSS) -- only racing the two prefixes
|
|
163
|
+
// against each other was wrong. Reuses COLOR_TOKEN (the shared
|
|
164
|
+
// "is this color-shaped" test) and only excludes the "opacity" scale name
|
|
165
|
+
// locally -- extractClasses.ts's lastColorToken's full NON_COLOR_SCALE_NAMES
|
|
166
|
+
// set also excludes "linear"/"conic", but those are bg-gradient-angle
|
|
167
|
+
// utilities with no outline-*/ring-* equivalent, so they can never appear
|
|
168
|
+
// here.
|
|
169
|
+
function lastColorTokenForIndicator(focusClasses, prefix) {
|
|
128
170
|
let found = null;
|
|
129
171
|
for (const raw of focusClasses) {
|
|
130
172
|
const base = raw.slice(raw.lastIndexOf(":") + 1);
|
|
131
|
-
|
|
132
|
-
if (!match)
|
|
173
|
+
if (!base.startsWith(`${prefix}-`))
|
|
133
174
|
continue;
|
|
134
|
-
const rest =
|
|
175
|
+
const rest = base.slice(prefix.length + 1);
|
|
135
176
|
if (!COLOR_TOKEN.test(rest))
|
|
136
177
|
continue;
|
|
137
178
|
const scaleName = /^([a-z]+)-\d/.exec(rest)?.[1];
|
|
@@ -141,19 +182,18 @@ function lastIndicatorColorToken(focusClasses) {
|
|
|
141
182
|
}
|
|
142
183
|
return found;
|
|
143
184
|
}
|
|
144
|
-
// Same last-token-wins
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
function
|
|
185
|
+
// Same per-prefix last-token-wins shape as above, but for width: only an
|
|
186
|
+
// enumerated outline-{N}/ring-{N} or an arbitrary [Npx] sets a thickness.
|
|
187
|
+
// A color token (ring-blue-400), ring-offset-*, ring-inset, etc. don't
|
|
188
|
+
// match either shape and are silently ignored here -- they're a different
|
|
189
|
+
// utility's job (color, offset, inset), not this one's.
|
|
190
|
+
function thicknessTokenForIndicator(focusClasses, prefix) {
|
|
150
191
|
let found = null;
|
|
151
192
|
for (const raw of focusClasses) {
|
|
152
193
|
const base = raw.slice(raw.lastIndexOf(":") + 1);
|
|
153
|
-
|
|
154
|
-
if (!match)
|
|
194
|
+
if (!base.startsWith(`${prefix}-`))
|
|
155
195
|
continue;
|
|
156
|
-
const token =
|
|
196
|
+
const token = base.slice(prefix.length + 1);
|
|
157
197
|
if (token in WIDTH_SCALE) {
|
|
158
198
|
found = WIDTH_SCALE[token];
|
|
159
199
|
continue;
|
|
@@ -169,26 +209,44 @@ export function checkFocusContrast(checks, strict = false, palette = defaultPale
|
|
|
169
209
|
for (const check of checks) {
|
|
170
210
|
if (!check.bgClass)
|
|
171
211
|
continue; // no resolvable background — skip, not a guess
|
|
172
|
-
const indicatorBase = lastIndicatorColorToken(check.focusClasses);
|
|
173
|
-
if (!indicatorBase)
|
|
174
|
-
continue; // no explicit outline-*/ring-* color — out of scope, see CLAUDE.md
|
|
175
|
-
const indicatorHex = resolveColorValue(indicatorBase, palette);
|
|
176
212
|
const bgHex = resolveColorValue(check.bgClass, palette);
|
|
177
|
-
|
|
213
|
+
const bgRgb = bgHex ? hexToRgb(bgHex) : null;
|
|
214
|
+
if (!bgRgb)
|
|
178
215
|
continue; // custom theme color / unsupported arbitrary value — skip
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
// outline-* and ring-* are evaluated as independent candidates, not
|
|
217
|
+
// raced against each other -- both are real, simultaneously-rendering
|
|
218
|
+
// mechanisms (see lastColorTokenForIndicator's comment for why), so an
|
|
219
|
+
// element can have zero, one, or both present at once.
|
|
220
|
+
const candidates = [];
|
|
221
|
+
for (const prefix of ["outline", "ring"]) {
|
|
222
|
+
const indicatorBase = lastColorTokenForIndicator(check.focusClasses, prefix);
|
|
223
|
+
if (!indicatorBase)
|
|
224
|
+
continue; // this mechanism isn't in use on this element
|
|
225
|
+
const indicatorHex = resolveColorValue(indicatorBase, palette);
|
|
226
|
+
const indicatorRgb = indicatorHex ? hexToRgb(indicatorHex) : null;
|
|
227
|
+
if (!indicatorRgb)
|
|
228
|
+
continue; // custom theme color / unsupported arbitrary value — skip this candidate
|
|
229
|
+
const ratio = contrastRatio(indicatorRgb, bgRgb);
|
|
230
|
+
const contrastFails = ratio < NON_TEXT_MIN_RATIO;
|
|
231
|
+
let thicknessPx = null;
|
|
232
|
+
if (strict)
|
|
233
|
+
thicknessPx = thicknessTokenForIndicator(check.focusClasses, prefix);
|
|
234
|
+
const thicknessFails = strict && thicknessPx !== null && thicknessPx < FOCUS_INDICATOR_MIN_THICKNESS_PX;
|
|
235
|
+
candidates.push({ indicatorBase, ratio, contrastFails, thicknessPx, thicknessFails });
|
|
236
|
+
}
|
|
237
|
+
if (candidates.length === 0)
|
|
238
|
+
continue; // no resolvable outline-*/ring-* color at all
|
|
239
|
+
// A user only needs ONE sufficiently visible (and, under strict,
|
|
240
|
+
// sufficiently thick) focus indicator to perceive the focus state --
|
|
241
|
+
// if any present candidate fully passes, this element is compliant
|
|
242
|
+
// even if another present indicator independently would have failed.
|
|
243
|
+
const allFail = candidates.every((c) => c.contrastFails || c.thicknessFails);
|
|
244
|
+
if (!allFail)
|
|
190
245
|
continue;
|
|
191
|
-
|
|
246
|
+
// More than one candidate present and both fail -- report the worst
|
|
247
|
+
// (lowest-ratio) offender.
|
|
248
|
+
const worst = candidates.reduce((a, b) => (b.ratio < a.ratio ? b : a));
|
|
249
|
+
const rawIndicatorClass = check.focusClasses.find((raw) => raw.slice(raw.lastIndexOf(":") + 1) === worst.indicatorBase);
|
|
192
250
|
violations.push({
|
|
193
251
|
type: "focus-contrast",
|
|
194
252
|
file: check.file,
|
|
@@ -196,11 +254,11 @@ export function checkFocusContrast(checks, strict = false, palette = defaultPale
|
|
|
196
254
|
tagName: check.tagName,
|
|
197
255
|
indicatorClass: rawIndicatorClass,
|
|
198
256
|
bgClass: check.bgClass,
|
|
199
|
-
ratio,
|
|
257
|
+
ratio: worst.ratio,
|
|
200
258
|
required: NON_TEXT_MIN_RATIO,
|
|
201
|
-
level: thicknessFails ? "AAA" : "AA",
|
|
202
|
-
...(thicknessFails && thicknessPx !== null
|
|
203
|
-
? { thicknessPx, requiredThicknessPx: FOCUS_INDICATOR_MIN_THICKNESS_PX }
|
|
259
|
+
level: worst.thicknessFails ? "AAA" : "AA",
|
|
260
|
+
...(worst.thicknessFails && worst.thicknessPx !== null
|
|
261
|
+
? { thicknessPx: worst.thicknessPx, requiredThicknessPx: FOCUS_INDICATOR_MIN_THICKNESS_PX }
|
|
204
262
|
: {}),
|
|
205
263
|
});
|
|
206
264
|
}
|
|
@@ -60,22 +60,60 @@ export function checkReducedMotion(checks, strict = false) {
|
|
|
60
60
|
return [];
|
|
61
61
|
const violations = [];
|
|
62
62
|
for (const check of checks) {
|
|
63
|
-
let
|
|
63
|
+
let realTransition = null;
|
|
64
64
|
let hasMotionReduceGuard = false;
|
|
65
65
|
for (const raw of check.classes) {
|
|
66
66
|
const base = baseUtility(raw);
|
|
67
67
|
const segments = variantSegments(raw);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// A transition counts as "real" (needs checking) unless its variant
|
|
69
|
+
// stack makes it not actually apply at the moment the interaction
|
|
70
|
+
// begins:
|
|
71
|
+
// - an interaction pseudo-class (hover:/focus:/focus-within:/
|
|
72
|
+
// active:) anywhere in the stack means the transition-property
|
|
73
|
+
// only exists *during* that momentary state, not before it -- CSS
|
|
74
|
+
// has nothing to transition *from* right as the interaction
|
|
75
|
+
// starts, so `hover:transition-transform hover:scale-110` still
|
|
76
|
+
// snaps instantly, same as before this fix (this is the one case
|
|
77
|
+
// the original `segments.length === 0` check happened to get
|
|
78
|
+
// right, so it's preserved here under its real reason instead).
|
|
79
|
+
// - motion-safe: anywhere in the stack means the transition simply
|
|
80
|
+
// doesn't exist unless motion is already safe -- a complete,
|
|
81
|
+
// persistent exemption, unrelated to interaction timing.
|
|
82
|
+
// Any *other* scoping (dark:, sm:, lg:, ...) is a persistent
|
|
83
|
+
// precondition, not a momentary one -- the transition genuinely is
|
|
84
|
+
// present in the resting state whenever that condition holds, with
|
|
85
|
+
// zero relationship to prefers-reduced-motion. Caught in independent
|
|
86
|
+
// adversarial testing: the previous version required
|
|
87
|
+
// `segments.length === 0` (no variant at all), so
|
|
88
|
+
// `dark:transition hover:scale-110` was silently treated as
|
|
89
|
+
// compliant even though it animates on hover in dark mode
|
|
90
|
+
// regardless of the user's motion preference.
|
|
91
|
+
const isInteractionGated = segments.some((v) => INTERACTION_VARIANTS.has(v));
|
|
92
|
+
const isMotionSafeGated = segments.includes("motion-safe");
|
|
93
|
+
if (TRANSITION_BASES.has(base) && !isInteractionGated && !isMotionSafeGated)
|
|
94
|
+
realTransition = raw;
|
|
95
|
+
// Only a *bare* motion-reduce:transition-none/transform-none (no
|
|
96
|
+
// other variant stacked with it) is trusted as a full guard --
|
|
97
|
+
// caught in independent adversarial testing: `sm:motion-reduce:
|
|
98
|
+
// transition-none` was being accepted as fully protective even
|
|
99
|
+
// though it only suppresses the transition at/above the `sm`
|
|
100
|
+
// breakpoint, leaving it completely unguarded below that width.
|
|
101
|
+
// Correctly modeling arbitrary variant-subset relationships (does
|
|
102
|
+
// this guard's other conditions always hold whenever the real
|
|
103
|
+
// trigger's conditions hold?) is out of scope -- requiring the
|
|
104
|
+
// guard to be unconditional is the same "skip/flag rather than
|
|
105
|
+
// guess" posture used everywhere else in this project, erring
|
|
106
|
+
// toward a false positive over the worse failure mode, a false
|
|
107
|
+
// negative.
|
|
108
|
+
if (segments.length === 1 && segments[0] === "motion-reduce" && (base === "transition-none" || base === "transform-none")) {
|
|
71
109
|
hasMotionReduceGuard = true;
|
|
72
110
|
}
|
|
73
111
|
}
|
|
74
|
-
//
|
|
75
|
-
//
|
|
112
|
+
// No real (non-motion-safe-guarded) transition at all means it simply
|
|
113
|
+
// doesn't exist unless motion is already safe -- a complete
|
|
76
114
|
// alternative way of satisfying 2.3.3, not a partial one -- so this
|
|
77
115
|
// correctly falls through as a pass, not a skip-because-unresolvable.
|
|
78
|
-
if (!
|
|
116
|
+
if (!realTransition)
|
|
79
117
|
continue;
|
|
80
118
|
if (hasMotionReduceGuard)
|
|
81
119
|
continue;
|
|
@@ -100,7 +138,7 @@ export function checkReducedMotion(checks, strict = false) {
|
|
|
100
138
|
file: check.file,
|
|
101
139
|
line: check.line,
|
|
102
140
|
tagName: check.tagName,
|
|
103
|
-
transitionClass:
|
|
141
|
+
transitionClass: realTransition,
|
|
104
142
|
motionClass,
|
|
105
143
|
level: "AAA",
|
|
106
144
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tailwind-a11y",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.5",
|
|
4
4
|
"description": "Static analysis CLI that catches WCAG accessibility violations — color contrast, touch target size, and focus indicator removal/contrast — in Tailwind CSS class combinations before they ship.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|