Bug 2068024 - Stylelint: create the no-background-without-text-color rule r=hjones,desktop-theme-reviewers
The rule ships `meta.fixable: false`; bug 2068773 adds the fix, holding it off on the annotated sites with the disable comment stylelint already honours for fixes. Differential Revision: https://phabricator.services.mozilla.com/D322597
This commit is contained in:
committed by
dgottwald@mozilla.com
parent
ac0311aaf6
commit
12eef3d296
@@ -279,6 +279,7 @@ module.exports = {
|
|||||||
// unrelated errors in the same query.
|
// unrelated errors in the same query.
|
||||||
"media-query-no-invalid": null,
|
"media-query-no-invalid": null,
|
||||||
"stylelint-plugin-mozilla/media-query-no-invalid": true,
|
"stylelint-plugin-mozilla/media-query-no-invalid": true,
|
||||||
|
"stylelint-plugin-mozilla/no-background-without-text-color": true,
|
||||||
"stylelint-plugin-mozilla/no-base-design-tokens": true,
|
"stylelint-plugin-mozilla/no-base-design-tokens": true,
|
||||||
"stylelint-plugin-mozilla/no-has-selector": true,
|
"stylelint-plugin-mozilla/no-has-selector": true,
|
||||||
"stylelint-plugin-mozilla/use-design-tokens": true,
|
"stylelint-plugin-mozilla/use-design-tokens": true,
|
||||||
|
|||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
# no-background-without-text-color (stylelint)
|
||||||
|
|
||||||
|
This rule requires a text color from any declaration block that paints its
|
||||||
|
background with a design token that has a paired text color token.
|
||||||
|
|
||||||
|
A block that paints a background claims a surface, and it owes that surface a
|
||||||
|
text color. Without one, the text takes whatever color an unrelated ancestor
|
||||||
|
happens to supply, and nothing guarantees the two have sufficient contrast in
|
||||||
|
every theme, in dark mode, under `prefers-contrast`, or under `forced-colors` —
|
||||||
|
high contrast mode, which applies to chrome windows on Windows and to `about:`
|
||||||
|
pages on every platform.
|
||||||
|
|
||||||
|
The counterpart of the background token is the text color the surface was
|
||||||
|
designed for, so the rule's message names it;
|
||||||
|
[use-paired-color-tokens](use-paired-color-tokens.md) is what checks a
|
||||||
|
combination once both halves are declared.
|
||||||
|
|
||||||
|
## Rule Scope
|
||||||
|
|
||||||
|
The rule reports a block whose winning `background` or `background-color`
|
||||||
|
declaration reads a background token that has a paired text color token, and
|
||||||
|
which sets no `color`. A background token without a counterpart is meant to
|
||||||
|
combine with whatever the surface inherits, so it makes no claim to report.
|
||||||
|
|
||||||
|
Blocks selected by a state — `:hover`, `:focus`, `[open]`, `[disabled]` and
|
||||||
|
their kin — are exempt, along with blocks nested inside one. A state variant
|
||||||
|
usually restyles an element its base rule has already given a text color, and
|
||||||
|
that base rule is generally a flat sibling the rule cannot reach. A negated
|
||||||
|
state such as `:not(:hover)` names the base state itself, so a block selected
|
||||||
|
that way owes the surface a text color and is reported.
|
||||||
|
|
||||||
|
Read that as a gap in the rule, not as permission. Declare both halves in a
|
||||||
|
state variant too: `--button-text-color-hover` and `--button-text-color` are
|
||||||
|
separate tokens that can resolve to entirely different values under
|
||||||
|
`forced-colors`, so a state that repaints the background and inherits the base
|
||||||
|
rule's text color is not safe there.
|
||||||
|
|
||||||
|
Declarations directly inside an at-rule are checked as their own block, since a
|
||||||
|
`@media` query can paint a background the rule around it does not. The element
|
||||||
|
is the same one either way, so a `color` on the rule covers what the at-rule
|
||||||
|
paints:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.card {
|
||||||
|
color: var(--panel-text-color);
|
||||||
|
|
||||||
|
@media -moz-pref("browser.nova.enabled") {
|
||||||
|
/* Fine: the color above applies to this element in every query. */
|
||||||
|
background-color: var(--panel-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A nested *rule* matches a different element and stands on its own.
|
||||||
|
|
||||||
|
A block can hand the surface a text color through a custom property instead of a
|
||||||
|
`color` declaration, which is how a component that renders the text in its own
|
||||||
|
shadow tree takes one. Defining a paired text token, or a property that reads
|
||||||
|
one, satisfies the rule:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.new-badge {
|
||||||
|
background-color: var(--badge-background-color-filled);
|
||||||
|
--badge-text-color: var(--badge-text-color-filled);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The rule does not require the reverse: a block that sets only a text color is
|
||||||
|
usually a descendant of the element painting the background, which the rule
|
||||||
|
cannot see.
|
||||||
|
|
||||||
|
## Examples of incorrect usage for this rule
|
||||||
|
|
||||||
|
```css
|
||||||
|
#header {
|
||||||
|
background-color: var(--sidebar-background-color);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples of correct usage for this rule
|
||||||
|
|
||||||
|
```css
|
||||||
|
#header {
|
||||||
|
background-color: var(--sidebar-background-color);
|
||||||
|
color: var(--sidebar-text-color);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.toolbar-button:hover {
|
||||||
|
background-color: var(--button-background-color-hover);
|
||||||
|
color: var(--button-text-color-hover);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Autofix functionality
|
||||||
|
|
||||||
|
None yet;
|
||||||
|
[bug 2068773](https://bugzilla.mozilla.org/show_bug.cgi?id=2068773) adds it. The
|
||||||
|
counterpart is fixed by the design system rather than chosen by the author,
|
||||||
|
which is why the message can name it, and the rule only reports where that
|
||||||
|
counterpart exists, so the declaration to insert is determined. What the block
|
||||||
|
alone does not say is whether the surface takes its text color from elsewhere on
|
||||||
|
purpose, where inserting one is a silent rendering change. Such a block carries
|
||||||
|
a disable comment, which stylelint honours for fixes as well as reports — so the
|
||||||
|
fix goes in ungated and the disable comment described below is what holds it
|
||||||
|
off.
|
||||||
|
|
||||||
|
## Disabling the rule
|
||||||
|
|
||||||
|
**Prefer declaring the color.** The tempting disable is the one that reasons
|
||||||
|
about what the block contains — this element holds no text, its icon takes the
|
||||||
|
color from a `fill` below, a child element colors itself. Those are all easy to
|
||||||
|
falsify: markup changes, `forced-colors` turns a translucent wash into an opaque
|
||||||
|
`ButtonFace`, and the next person to put a text node in the element inherits
|
||||||
|
whatever happens to be there. Declaring the counterpart costs one line and is
|
||||||
|
usually value-neutral in the default theme.
|
||||||
|
|
||||||
|
A `::part()` is not an exception to that. A `::part()` rule in the outer tree
|
||||||
|
overrides the component's own declaration for that part, so a block repainting a
|
||||||
|
part's background sets the `color` in the same block:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.section-context-menu.context-menu-open > moz-button::part(button) {
|
||||||
|
background-color: var(--button-background-color-ghost-hover);
|
||||||
|
color: var(--button-text-color-ghost-hover);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`moz-select.css` is the in-tree example to follow: its
|
||||||
|
`panel-item[selected]::part(button)` sets `background-color` and `color` together
|
||||||
|
on the element `panel-item.css` gives `background-color: transparent` and
|
||||||
|
`color: inherit`.
|
||||||
|
|
||||||
|
Where a component remaps a background token into its own palette, remap the text
|
||||||
|
counterpart beside it. Pairing the remapped background with the palette's own
|
||||||
|
text token instead is what
|
||||||
|
[use-paired-color-tokens](use-paired-color-tokens.md) rejects:
|
||||||
|
|
||||||
|
```css
|
||||||
|
panel-list {
|
||||||
|
--button-background-color-hover: var(--smartwindow-panel-item-background-color-hover);
|
||||||
|
--button-text-color-hover: var(--panel-list-text-color);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That leaves the disable worth writing, for a block where declaring the color
|
||||||
|
would do damage or would not resolve at all:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* The tracker count below takes the box text color; a color here would recolor
|
||||||
|
the shield. */
|
||||||
|
/* stylelint-disable-next-line stylelint-plugin-mozilla/no-background-without-text-color */
|
||||||
|
background-color: var(--urlbar-box-background-color);
|
||||||
|
```
|
||||||
|
|
||||||
|
The comment must name what setting a color here would break, or where the color
|
||||||
|
comes from instead, in terms a reviewer can check. If the surface should have a
|
||||||
|
text color of its own but the token does not exist, file a bug for the missing
|
||||||
|
token and reference it from a `TODO`.
|
||||||
+4
-1
@@ -15,7 +15,10 @@ in the theme the author did not try.
|
|||||||
The rule only reports a declaration block that sets both a background color and
|
The rule only reports a declaration block that sets both a background color and
|
||||||
a text color, because a block that sets one of them takes the other from
|
a text color, because a block that sets one of them takes the other from
|
||||||
somewhere the rule cannot see: an ancestor, a sibling rule, or another
|
somewhere the rule cannot see: an ancestor, a sibling rule, or another
|
||||||
pseudo-element.
|
pseudo-element. Whether a block that paints a surface owes it a text color at
|
||||||
|
all is
|
||||||
|
[no-background-without-text-color](no-background-without-text-color.md)'s
|
||||||
|
question.
|
||||||
|
|
||||||
Within such a block it reports two things:
|
Within such a block it reports two things:
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import mediaQueryNoInvalid from "./media-query-no-invalid.mjs";
|
import mediaQueryNoInvalid from "./media-query-no-invalid.mjs";
|
||||||
|
import noBackgroundWithoutTextColor from "./no-background-without-text-color.mjs";
|
||||||
import noBaseDesignTokens from "./no-base-design-tokens.mjs";
|
import noBaseDesignTokens from "./no-base-design-tokens.mjs";
|
||||||
import noBrowserRefsInToolkit from "./no-browser-refs-in-toolkit.mjs";
|
import noBrowserRefsInToolkit from "./no-browser-refs-in-toolkit.mjs";
|
||||||
import noHasSelector from "./no-has-selector.mjs";
|
import noHasSelector from "./no-has-selector.mjs";
|
||||||
@@ -13,6 +14,7 @@ import usePairedColorTokens from "./use-paired-color-tokens.mjs";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
"media-query-no-invalid": mediaQueryNoInvalid,
|
"media-query-no-invalid": mediaQueryNoInvalid,
|
||||||
|
"no-background-without-text-color": noBackgroundWithoutTextColor,
|
||||||
"no-base-design-tokens": noBaseDesignTokens,
|
"no-base-design-tokens": noBaseDesignTokens,
|
||||||
"no-browser-refs-in-toolkit": noBrowserRefsInToolkit,
|
"no-browser-refs-in-toolkit": noBrowserRefsInToolkit,
|
||||||
"no-has-selector": noHasSelector,
|
"no-has-selector": noHasSelector,
|
||||||
|
|||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import stylelint from "stylelint";
|
||||||
|
import {
|
||||||
|
backgroundToText,
|
||||||
|
customPropertiesRead,
|
||||||
|
findColorDeclarations,
|
||||||
|
isCustomPropertyDefinition,
|
||||||
|
namespace,
|
||||||
|
textToBackground,
|
||||||
|
} from "../helpers.mjs";
|
||||||
|
|
||||||
|
const {
|
||||||
|
utils: { report, ruleMessages, validateOptions },
|
||||||
|
} = stylelint;
|
||||||
|
|
||||||
|
let ruleName = namespace("no-background-without-text-color");
|
||||||
|
let messages = ruleMessages(ruleName, {
|
||||||
|
noTextColor: (background, text) =>
|
||||||
|
`"${background}" should be used with a text color; add "color: var(${text})", or disable the rule with a comment saying where the text color comes from.`,
|
||||||
|
});
|
||||||
|
let meta = {
|
||||||
|
url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/no-background-without-text-color.html",
|
||||||
|
fixable: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// A state variant usually restyles an element its base rule has already given a
|
||||||
|
// text color, so state selectors are exempt. That base rule is generally a flat
|
||||||
|
// sibling (`.foo:hover {}` beside `.foo {}`) rather than a nesting parent, which
|
||||||
|
// is why this keys on the selector rather than walking up.
|
||||||
|
const STATE_PSEUDO_CLASS =
|
||||||
|
/:(?:active|checked|current|default|disabled|enabled|focus|focus-visible|focus-within|hover|in-range|indeterminate|invalid|open|out-of-range|past|paused|placeholder-shown|playing|popover-open|read-only|read-write|target|user-invalid|user-valid|valid|visited|-moz-broken|-moz-drag-over|-moz-focusring|-moz-window-inactive)\b/;
|
||||||
|
|
||||||
|
// Chrome markup carries the states a pseudo-class cannot express as attributes
|
||||||
|
// instead, so `menulist[disabled]` is a state variant of `menulist`.
|
||||||
|
const STATE_ATTRIBUTE =
|
||||||
|
/\[\s*(?:active|aria-checked|aria-current|aria-disabled|aria-expanded|aria-pressed|aria-selected|busy|checked|disabled|focused|open|pressed|selected)\s*[\]~^|$*=]/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the argument of every `:not()`, innermost first. A negated state names
|
||||||
|
* the base state rather than a variant of it, so `&:not(:hover)` is the rule
|
||||||
|
* that owes the surface a text color, not one that inherits it from elsewhere.
|
||||||
|
*
|
||||||
|
* @param {string} selector
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
let stripNegations = selector => {
|
||||||
|
for (let previous; selector != previous; ) {
|
||||||
|
previous = selector;
|
||||||
|
selector = selector.replace(/:not\([^()]*\)/g, "");
|
||||||
|
}
|
||||||
|
return selector;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a selector says the text color is not this block's to declare.
|
||||||
|
*
|
||||||
|
* @param {string} selector
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
let isExemptSelector = selector => {
|
||||||
|
let states = stripNegations(selector);
|
||||||
|
return STATE_PSEUDO_CLASS.test(states) || STATE_ATTRIBUTE.test(states);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the selector of a block, or of a block it is nested in, exempts it.
|
||||||
|
*
|
||||||
|
* @param {object} block - A PostCSS Rule or AtRule.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
let isExempt = block => {
|
||||||
|
for (let node = block; node; node = node.parent) {
|
||||||
|
if (
|
||||||
|
node.type == "rule" &&
|
||||||
|
isExemptSelector(node.raws.selector?.raw ?? node.selector)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the element a block styles is given a text color. An at-rule nested
|
||||||
|
* in a rule styles that rule's element, so a `color` the rule sets applies to
|
||||||
|
* the at-rule's surface too; a nested rule matches a different element.
|
||||||
|
*
|
||||||
|
* @param {object} block - A PostCSS Rule or AtRule.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
let hasTextColor = block => {
|
||||||
|
for (let node = block; node; node = node.parent) {
|
||||||
|
if (findColorDeclarations(node).text) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (node.type != "atrule") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a block gives the surface a text color through a custom property,
|
||||||
|
* which is how a component rendering the text in its own shadow tree takes
|
||||||
|
* one.
|
||||||
|
*
|
||||||
|
* @param {object} block - A PostCSS Rule or AtRule.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
let definesTextColor = block =>
|
||||||
|
block.nodes.some(
|
||||||
|
node =>
|
||||||
|
node.type == "decl" &&
|
||||||
|
isCustomPropertyDefinition(node) &&
|
||||||
|
(textToBackground.has(node.prop) ||
|
||||||
|
customPropertiesRead(node.value).some(token =>
|
||||||
|
textToBackground.has(token)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports a declaration block that paints a paired background token and sets
|
||||||
|
* no text color, leaving the surface to inherit one no theme guarantees the
|
||||||
|
* contrast of. Only the declarations that win the cascade within the block are
|
||||||
|
* considered.
|
||||||
|
*
|
||||||
|
* @param {object} block - A PostCSS Rule or AtRule.
|
||||||
|
* @param {object} result - The PostCSS result to report to.
|
||||||
|
*/
|
||||||
|
let checkBlock = (block, result) => {
|
||||||
|
let { background } = findColorDeclarations(block);
|
||||||
|
if (
|
||||||
|
!background ||
|
||||||
|
hasTextColor(block) ||
|
||||||
|
definesTextColor(block) ||
|
||||||
|
isExempt(block)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [paired] = customPropertiesRead(background.value).filter(token =>
|
||||||
|
backgroundToText.has(token)
|
||||||
|
);
|
||||||
|
if (!paired) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
report({
|
||||||
|
message: messages.noTextColor(paired, backgroundToText.get(paired)),
|
||||||
|
node: background,
|
||||||
|
result,
|
||||||
|
ruleName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let ruleFunction = primaryOption => {
|
||||||
|
return (root, result) => {
|
||||||
|
let validOptions = validateOptions(result, ruleName, {
|
||||||
|
actual: primaryOption,
|
||||||
|
possible: [true],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!validOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declarations nest directly inside an at-rule as well as inside a rule,
|
||||||
|
// which is the shape of every generated token sheet. A statement at-rule
|
||||||
|
// such as @namespace has no block and so no nodes at all.
|
||||||
|
root.walk(node => {
|
||||||
|
if ((node.type == "rule" || node.type == "atrule") && node.nodes) {
|
||||||
|
checkBlock(node, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
ruleFunction.ruleName = ruleName;
|
||||||
|
ruleFunction.messages = messages;
|
||||||
|
ruleFunction.meta = meta;
|
||||||
|
export default ruleFunction;
|
||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Bug 1948378: remove this exception when the eslint import plugin fully
|
||||||
|
// supports exports in package.json files
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
import { testRule } from "stylelint-test-rule-node";
|
||||||
|
import stylelint from "stylelint";
|
||||||
|
import noBackgroundWithoutTextColor from "../rules/no-background-without-text-color.mjs";
|
||||||
|
|
||||||
|
let plugin = stylelint.createPlugin(
|
||||||
|
noBackgroundWithoutTextColor.ruleName,
|
||||||
|
noBackgroundWithoutTextColor
|
||||||
|
);
|
||||||
|
let {
|
||||||
|
ruleName,
|
||||||
|
rule: { messages },
|
||||||
|
} = plugin;
|
||||||
|
|
||||||
|
testRule({
|
||||||
|
plugins: [plugin],
|
||||||
|
ruleName,
|
||||||
|
config: [true],
|
||||||
|
fix: false,
|
||||||
|
accept: [
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--button-background-color); color: var(--button-text-color); }",
|
||||||
|
description: "A background token with a text color.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { color: var(--button-text-color); }",
|
||||||
|
description: "A block that paints no background claims no surface.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--color-accent-primary); }",
|
||||||
|
description: "A base color is not a background token.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--text-color-deemphasized); }",
|
||||||
|
description:
|
||||||
|
"A token with no background counterpart guarantees nothing to pair with.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: transparent; }",
|
||||||
|
description: "A background that paints nothing claims no surface.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--my-local-background); }",
|
||||||
|
description: "A local custom property is not a token.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { --box-background: var(--button-background-color); }",
|
||||||
|
description: "Defining a custom property is not painting a surface.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a:hover { background-color: var(--button-background-color-hover); }",
|
||||||
|
description:
|
||||||
|
"A state variant takes the text color from the rule it varies.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { &:hover { background-color: var(--button-background-color-hover); } }",
|
||||||
|
description: "A nested state variant is one as well.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a:hover { .b { background-color: var(--button-background-color-hover); } }",
|
||||||
|
description: "A block nested inside a state variant is covered too.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "menulist[disabled] { background-color: var(--button-background-color); }",
|
||||||
|
description: "A state carried as an attribute is a state variant.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '.a[aria-expanded="true"] { background-color: var(--button-background-color); }',
|
||||||
|
description: "An ARIA state attribute is one as well.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a:not(.b):is(:hover, [open]) { background-color: var(--button-background-color); }",
|
||||||
|
description: "A state beside a negation still exempts the block.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--button-background-color); color: inherit; }",
|
||||||
|
description: "Any text color satisfies the rule, token or not.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { color: CanvasText; @media (prefers-contrast) { background-color: var(--button-background-color); } }",
|
||||||
|
description:
|
||||||
|
"An at-rule paints the surface of the rule around it, which has a text color.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { color: var(--panel-text-color); background-color: var(--panel-background-color); }",
|
||||||
|
description: "Declaration order does not matter.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "@namespace url(http://www.w3.org/1998/Math/MathML);",
|
||||||
|
description: "A statement at-rule has no block to check.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a[type='text'] { background-color: var(--button-background-color); color: var(--button-text-color); }",
|
||||||
|
description: "An attribute that is not a state is not a carve-out.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--badge-background-color-filled); --badge-text-color: var(--badge-text-color-filled); }",
|
||||||
|
description:
|
||||||
|
"A paired text token handed to a component through a custom property.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--button-background-color-ghost); --button-text-color-ghost: currentColor; }",
|
||||||
|
description: "Defining the paired text token itself counts as well.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
reject: [
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--sidebar-background-color); }",
|
||||||
|
message: messages.noTextColor(
|
||||||
|
"--sidebar-background-color",
|
||||||
|
"--sidebar-text-color"
|
||||||
|
),
|
||||||
|
description: "A block that paints a surface and names no text color.",
|
||||||
|
line: 1,
|
||||||
|
column: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background: var(--panel-background-color) no-repeat; }",
|
||||||
|
message: messages.noTextColor(
|
||||||
|
"--panel-background-color",
|
||||||
|
"--panel-text-color"
|
||||||
|
),
|
||||||
|
description: "The background shorthand paints a surface too.",
|
||||||
|
line: 1,
|
||||||
|
column: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: light-dark(var(--button-background-color-hover), var(--button-background-color-hover)); }",
|
||||||
|
message: messages.noTextColor(
|
||||||
|
"--button-background-color-hover",
|
||||||
|
"--button-text-color-hover"
|
||||||
|
),
|
||||||
|
description: "A token nested in light-dark() paints a surface.",
|
||||||
|
line: 1,
|
||||||
|
column: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { @media (prefers-contrast) { background-color: var(--sidebar-background-color); } }",
|
||||||
|
message: messages.noTextColor(
|
||||||
|
"--sidebar-background-color",
|
||||||
|
"--sidebar-text-color"
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
"Declarations nested directly inside an at-rule are checked.",
|
||||||
|
line: 1,
|
||||||
|
column: 34,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--sidebar-background-color); color: var(--sidebar-text-color); &::after { background-color: var(--panel-background-color); } }",
|
||||||
|
message: messages.noTextColor(
|
||||||
|
"--panel-background-color",
|
||||||
|
"--panel-text-color"
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
"A pseudo-element paints its own surface and inherits nothing from its host block.",
|
||||||
|
line: 1,
|
||||||
|
column: 102,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { --box-radius: 4px; background-color: var(--sidebar-background-color); }",
|
||||||
|
message: messages.noTextColor(
|
||||||
|
"--sidebar-background-color",
|
||||||
|
"--sidebar-text-color"
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
"A custom property that carries no text color does not excuse the block.",
|
||||||
|
line: 1,
|
||||||
|
column: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { background-color: var(--button-background-color); @media (prefers-contrast) { color: CanvasText; } }",
|
||||||
|
message: messages.noTextColor(
|
||||||
|
"--button-background-color",
|
||||||
|
"--button-text-color"
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
"A text color set only inside a nested at-rule leaves the surface without one outside it.",
|
||||||
|
line: 1,
|
||||||
|
column: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a { &:not(:hover) { background-color: var(--button-background-color); } }",
|
||||||
|
message: messages.noTextColor(
|
||||||
|
"--button-background-color",
|
||||||
|
"--button-text-color"
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
"A negated state names the base state, which owes the surface a text color.",
|
||||||
|
line: 1,
|
||||||
|
column: 22,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: ".a:not([open]) { background-color: var(--button-background-color); }",
|
||||||
|
message: messages.noTextColor(
|
||||||
|
"--button-background-color",
|
||||||
|
"--button-text-color"
|
||||||
|
),
|
||||||
|
description: "A negated state attribute is no exemption either.",
|
||||||
|
line: 1,
|
||||||
|
column: 18,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user