Skip to content

Commit 48a9caf

Browse files
bschlenkaskoufis
andauthored
fix: allow :where and :is in selectors when they only target & (#1702)
Co-authored-by: Adam Skoufis <[email protected]>
1 parent 9e7a583 commit 48a9caf

3 files changed

Lines changed: 69 additions & 32 deletions

File tree

.changeset/mighty-ravens-fail.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@vanilla-extract/css': minor
3+
---
4+
5+
Allow `:where` and `:is` in `selectors` if all selectors target `&`
6+
7+
EXAMPLE USAGE:
8+

packages/css/src/validateSelector.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ describe('validateSelector', () => {
2828
'.a:where(.b, .c) > .target',
2929
'.a:is(.b, .c) > .target',
3030
'.target, .foo .target',
31+
':where(.target)',
32+
':where(.target):hover',
33+
':is(.target)',
34+
':is(.target):hover',
35+
'.a :where(.target)',
36+
'.a > :is(.target)',
37+
':where(.target, .a.target)',
38+
':is(.target, .a.target)',
39+
':where(.target, .a .target)',
40+
':is(.target, .a .target)',
3141
];
3242

3343
validSelectors.forEach((selector) =>
@@ -51,6 +61,14 @@ describe('validateSelector', () => {
5161
'.target + .a',
5262
'.target ~ .a',
5363
'.target:where(:hover, :focus) .a',
64+
':where(.target) .a',
65+
':is(.target) > .a',
66+
':where(.target, .a)',
67+
':is(.target, .a)',
68+
':where(.target, .a, .target)',
69+
':is(.target, .a, .target)',
70+
':where(.target, .target > span)',
71+
':is(.target, .target > span)',
5472
];
5573

5674
invalidSelectors.forEach((selector) =>

packages/css/src/validateSelector.ts

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,44 @@ function escapeRegex(string: string) {
77
return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
88
}
99

10+
type Selector = ReturnType<typeof parse>[number];
11+
12+
function targetsClassName(tokens: Selector, targetClassName: string): boolean {
13+
for (let i = tokens.length - 1; i >= 0; i--) {
14+
const token = tokens[i];
15+
16+
if (
17+
token.type === 'child' ||
18+
token.type === 'parent' ||
19+
token.type === 'sibling' ||
20+
token.type === 'adjacent' ||
21+
token.type === 'descendant'
22+
) {
23+
return false;
24+
}
25+
26+
if (
27+
token.type === 'attribute' &&
28+
token.name === 'class' &&
29+
token.value === targetClassName
30+
) {
31+
return true;
32+
}
33+
34+
if (
35+
token.type === 'pseudo' &&
36+
Array.isArray(token.data) &&
37+
(token.name === 'is' || token.name === 'where')
38+
) {
39+
if (token.data.every((sub) => targetsClassName(sub, targetClassName))) {
40+
return true;
41+
}
42+
}
43+
}
44+
45+
return false;
46+
}
47+
1048
export const validateSelector = (
1149
selector: string,
1250
targetClassName: string,
@@ -28,46 +66,19 @@ export const validateSelector = (
2866
}
2967

3068
selectorParts.forEach((tokens) => {
31-
try {
32-
for (let i = tokens.length - 1; i >= -1; i--) {
33-
if (!tokens[i]) {
34-
throw new Error();
35-
}
36-
37-
const token = tokens[i];
38-
39-
if (
40-
token.type === 'child' ||
41-
token.type === 'parent' ||
42-
token.type === 'sibling' ||
43-
token.type === 'adjacent' ||
44-
token.type === 'descendant'
45-
) {
46-
throw new Error();
47-
}
48-
49-
if (
50-
token.type === 'attribute' &&
51-
token.name === 'class' &&
52-
token.value === targetClassName
53-
) {
54-
return; // Found it
55-
}
56-
}
57-
} catch (err) {
69+
if (!targetsClassName(tokens, targetClassName)) {
5870
throw new Error(
5971
dedent`
6072
Invalid selector: ${replaceTarget()}
61-
73+
6274
Style selectors must target the '&' character (along with any modifiers), e.g. ${'`${parent} &`'} or ${'`${parent} &:hover`'}.
63-
75+
6476
This is to ensure that each style block only affects the styling of a single class.
65-
77+
6678
If your selector is targeting another class, you should move it to the style definition for that class, e.g. given we have styles for 'parent' and 'child' elements, instead of adding a selector of ${'`& ${child}`'}) to 'parent', you should add ${'`${parent} &`'} to 'child').
67-
79+
6880
If your selector is targeting something global, use the 'globalStyle' function instead, e.g. if you wanted to write ${'`& h1`'}, you should instead write 'globalStyle(${'`${parent} h1`'}, { ... })'
6981
`,
70-
{ cause: err },
7182
);
7283
}
7384
});

0 commit comments

Comments
 (0)