Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 6x 6x 6x 6x 6x 6x 8x 9x 9x 8x 8x 6x 22x 22x 18x 3x 15x 15x 19x 15x 15x 11x 8x 8x 8x 8x 15x 7x 8x | /**
* Rule 3: no customer hook writes inside pseudo-state rules.
*
* Per-state hooks (`--slds-c-button-color-background-hover`, etc.) must
* be **defined** on the idle/root selector and **read** by the
* pseudo-state rule via a `var()` chain that falls back to the idle
* hook. Writing the per-state hook *inside* the pseudo-state selector
* (`.slds-button:hover { --slds-c-...-background-hover: ... }`) creates
* a "wall": the customer's override on `.slds-button` for the hover
* hook is overridden by this in-pseudo-state assignment, because cascade
* specificity favors the more-specific selector.
*
* The fix is mechanical: hoist every per-state hook write to the idle
* selector. The `:hover` rule then reads the hook normally and inherits
* the customer override.
*/
import { classifySelector, targetsBemModifier } from '../helpers/selectorIssues.js';
import type { ComplianceCheck, ComplianceRow, Offender } from '../../types.js';
import {
declLocation,
failRow,
notRunYetRow,
passRow,
themeLayerFilesFor,
visitHookWriteRules,
} from './internals.js';
const ID = 'structural-no-pseudo-state-hook-writes';
const LABEL = 'No hook writes inside pseudo-state rules';
const CATEGORY = 'customer-reach' as const;
const PSEUDO_STATE_RE =
/:(?:focus-visible|focus-within|placeholder-shown|hover|focus|active|disabled|checked|target|visited|empty)\b/g;
const DISABLED_ATTR_RE = /\[disabled\]/g;
const WHITESPACE_RUN_RE = /\s+/g;
/**
* Strip pseudo-state pseudo-classes and `[disabled]` from each selector
* in the comma list to surface the idle selector(s) the author should
* hoist the hook write to.
*/
function idleSelectorsFor(rawSelector: string): string {
const idleParts = rawSelector
.split(',')
.map((part) =>
part
.replaceAll(PSEUDO_STATE_RE, '')
.replaceAll(DISABLED_ATTR_RE, '')
.replaceAll(WHITESPACE_RUN_RE, ' ')
.trim(),
)
.filter((part) => part.length > 0);
const unique = Array.from(new Set(idleParts));
return unique.join(', ');
}
export const noPseudoStateHookWrites: ComplianceCheck = (input): ComplianceRow => {
const files = themeLayerFilesFor(input);
if (!files) return notRunYetRow(ID, LABEL, CATEGORY);
if (files.length === 0) {
return passRow(ID, LABEL, 'Component has no theme or theme-base files; nothing to check.', CATEGORY);
}
const offenders: Offender[] = [];
for (const file of files) {
visitHookWriteRules(file, ({ rule, hookDecls }) => {
const cls = classifySelector(rule.selector, input.componentName);
if (!cls.issues.includes('pseudo-state')) return;
// Carveout: writes inside a BEM modifier rule (e.g.
// `.slds-button_neutral:hover`) target a system-owned modifier.
// Customers customize the base selector, not modifiers; the
// modifier's per-state write is the system's intended paint.
if (targetsBemModifier(rule.selector)) return;
const idle = idleSelectorsFor(rule.selector);
for (const decl of hookDecls) {
const fix = idle
? `Move \`${decl.prop}\` to \`${idle}\` and read it from this rule via \`var(${decl.prop})\`.`
: `Move \`${decl.prop}\` out of the pseudo-state rule onto the idle selector and read it from this rule via \`var(${decl.prop})\`.`;
offenders.push({
selector: rule.selector,
hook: decl.prop,
location: declLocation(file, decl),
fix,
});
}
});
}
if (offenders.length === 0) {
return passRow(
ID,
LABEL,
'No customer hooks are written inside pseudo-state rules. Per-state hooks are defined on the idle selector and read from the state selector via a var() fallback chain.',
CATEGORY,
);
}
return failRow(
ID,
LABEL,
`${offenders.length} per-state hook write${offenders.length === 1 ? '' : 's'} inside a pseudo-state rule. ` +
`These walls block customer overrides because the in-pseudo-state assignment outranks the customer's idle-selector value. ` +
`Hoist each write to the idle selector and have the pseudo-state rule read the hook via var().`,
offenders,
);
};
|