Skip to content

Commit e2b408a

Browse files
authored
Allow for flattened chained pseudo-selectors, eg. &:hover::after in our types (#1835)
* add chained pseudos * flattened * changeset * add flattened css pseudos to strict api * clean up pseudo classes * patch bump
1 parent fa32a97 commit e2b408a

File tree

7 files changed

+110
-14
lines changed

7 files changed

+110
-14
lines changed

.changeset/slick-waves-punch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@compiled/react': patch
3+
---
4+
5+
Allow for flattened chained pseudo-selectors, eg. `&:hover::after` in our type syntaxes

packages/react/src/create-strict-api/__tests__/__fixtures__/strict-api-recursive.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@ interface PressedProperties {
2424
backgroundColor: BackgroundPressed;
2525
}
2626

27+
interface ChainedProperties {
28+
color: ColorPressed;
29+
backgroundColor: BackgroundPressed;
30+
}
31+
2732
interface CSSPropertiesSchema extends Properties {
2833
'&:hover': HoveredProperties;
2934
'&:active': PressedProperties;
35+
'&:hover::after': ChainedProperties;
3036
}
3137

3238
const { css, cssMap, cx, XCSSProp } = createStrictAPI<CSSPropertiesSchema>();

packages/react/src/create-strict-api/__tests__/generics.test.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('createStrictAPI()', () => {
99
const styles = css({
1010
'&:hover': {},
1111
'&:active': {},
12+
'&:hover::after': {},
1213
'&::before': {},
1314
'&::after': {},
1415
});
@@ -23,6 +24,7 @@ describe('createStrictAPI()', () => {
2324
nested: {
2425
'&:hover': {},
2526
'&:active': {},
27+
'&:hover::after': {},
2628
'&::before': {},
2729
'&::after': {},
2830
},
@@ -40,7 +42,7 @@ describe('createStrictAPI()', () => {
4042
xcss: ReturnType<
4143
typeof XCSSProp<
4244
'backgroundColor' | 'color',
43-
'&:hover' | '&:active' | '&::before' | '&::after'
45+
'&:hover' | '&:active' | '&::before' | '&::after' | '&:hover::after'
4446
>
4547
>;
4648
}) {
@@ -49,7 +51,13 @@ describe('createStrictAPI()', () => {
4951

5052
const { getByTestId } = render(
5153
<Component
52-
xcss={{ '&:hover': {}, '&:active': {}, '&::before': {}, '&::after': {} }}
54+
xcss={{
55+
'&:hover': {},
56+
'&:active': {},
57+
'&::before': {},
58+
'&::after': {},
59+
'&:hover::after': {},
60+
}}
5361
data-testid="div"
5462
/>
5563
);
@@ -76,6 +84,12 @@ describe('createStrictAPI()', () => {
7684
// @ts-expect-error — Type '""' is not assignable to type ...
7785
backgroundColor: '',
7886
},
87+
'&:hover::after': {
88+
// @ts-expect-error — Type '""' is not assignable to type ...
89+
color: '',
90+
// @ts-expect-error — Type '""' is not assignable to type ...
91+
backgroundColor: '',
92+
},
7993
'&::before': {
8094
// @ts-expect-error — Type '""' is not assignable to type ...
8195
color: '',
@@ -114,6 +128,12 @@ describe('createStrictAPI()', () => {
114128
// @ts-expect-error — Type '""' is not assignable to type ...
115129
backgroundColor: '',
116130
},
131+
'&:hover::after': {
132+
// @ts-expect-error — Type '""' is not assignable to type ...
133+
color: '',
134+
// @ts-expect-error — Type '""' is not assignable to type ...
135+
backgroundColor: '',
136+
},
117137
'&::before': {
118138
// @ts-expect-error — Type '""' is not assignable to type ...
119139
color: '',
@@ -139,7 +159,7 @@ describe('createStrictAPI()', () => {
139159
xcss: ReturnType<
140160
typeof XCSSProp<
141161
'backgroundColor' | 'color',
142-
'&:hover' | '&:active' | '&::before' | '&::after'
162+
'&:hover' | '&:active' | '&::before' | '&::after' | '&:hover::after'
143163
>
144164
>;
145165
}) {
@@ -165,6 +185,12 @@ describe('createStrictAPI()', () => {
165185
// @ts-expect-error — Type '""' is not assignable to type ...
166186
backgroundColor: 'var(--ds-success)',
167187
},
188+
'&:hover::after': {
189+
// @ts-expect-error — Type '""' is not assignable to type ...
190+
color: 'var(--ds-text)',
191+
// @ts-expect-error — Type '""' is not assignable to type ...
192+
backgroundColor: 'var(--ds-success)',
193+
},
168194
'&::before': {
169195
// @ts-expect-error — Type '""' is not assignable to type ...
170196
color: '',
@@ -198,6 +224,12 @@ describe('createStrictAPI()', () => {
198224
color: 'var(--ds-text-hovered)',
199225
backgroundColor: 'var(--ds-bold-hovered)',
200226
},
227+
'&:hover::after': {
228+
// @ts-expect-error — should be a value from the schema
229+
padding: '10px',
230+
color: 'var(--ds-text-pressed)',
231+
backgroundColor: 'var(--ds-bold-pressed)',
232+
},
201233
'&:active': {
202234
// @ts-expect-error — should be a value from the schema
203235
padding: '10px',
@@ -243,6 +275,12 @@ describe('createStrictAPI()', () => {
243275
color: 'var(--ds-text-pressed)',
244276
backgroundColor: 'var(--ds-bold-pressed)',
245277
},
278+
'&:hover::after': {
279+
// @ts-expect-error — should be a value from the schema
280+
padding: '10px',
281+
color: 'var(--ds-text-pressed)',
282+
backgroundColor: 'var(--ds-bold-pressed)',
283+
},
246284
'&::before': {
247285
// @ts-expect-error — should be a value from the schema
248286
padding: '10px',
@@ -270,7 +308,7 @@ describe('createStrictAPI()', () => {
270308
xcss: ReturnType<
271309
typeof XCSSProp<
272310
'backgroundColor' | 'color',
273-
'&:hover' | '&:active' | '&::before' | '&::after'
311+
'&:hover' | '&:active' | '&::before' | '&::after' | '&:hover::after'
274312
>
275313
>;
276314
}) {
@@ -290,6 +328,10 @@ describe('createStrictAPI()', () => {
290328
color: 'var(--ds-text-pressed)',
291329
backgroundColor: 'var(--ds-bold-pressed)',
292330
},
331+
'&:hover::after': {
332+
color: 'var(--ds-text-pressed)',
333+
backgroundColor: 'var(--ds-bold-pressed)',
334+
},
293335
'&::before': {
294336
color: 'var(--ds-text)',
295337
backgroundColor: 'var(--ds-bold)',

packages/react/src/create-strict-api/types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type {
22
StrictCSSProperties,
3-
CSSPseudoClasses,
43
CSSPseudoElements,
54
CSSPseudos,
5+
AllCSSPseudoClasses,
66
} from '../types';
77

88
/**
@@ -11,7 +11,7 @@ import type {
1111
* and pseudo elements.
1212
*/
1313
export type CompiledSchemaShape = StrictCSSProperties & {
14-
[Q in CSSPseudoClasses]?: StrictCSSProperties;
14+
[Q in AllCSSPseudoClasses]?: StrictCSSProperties;
1515
};
1616

1717
export type PseudosDeclarations = { [Q in CSSPseudos]?: StrictCSSProperties };
@@ -27,7 +27,7 @@ export type AllowedStyles<TMediaQuery extends string> = StrictCSSProperties &
2727
export type ApplySchemaValue<
2828
TSchema,
2929
TKey extends keyof StrictCSSProperties,
30-
TPseudoKey extends CSSPseudoClasses | ''
30+
TPseudoKey extends AllCSSPseudoClasses | ''
3131
> = TKey extends keyof TSchema
3232
? // TKey is a valid property on the schema
3333
TPseudoKey extends keyof TSchema
@@ -46,11 +46,11 @@ export type ApplySchemaValue<
4646
* value if present, else fallback to its value from {@link StrictCSSProperties}. If
4747
* the property isn't a known property its value will be resolved to `never`.
4848
*/
49-
export type ApplySchema<TObject, TSchema, TPseudoKey extends CSSPseudoClasses | '' = ''> = {
49+
export type ApplySchema<TObject, TSchema, TPseudoKey extends AllCSSPseudoClasses | '' = ''> = {
5050
[TKey in keyof TObject]?: TKey extends keyof StrictCSSProperties
5151
? // TKey is a valid CSS property, try to resolve its value.
5252
ApplySchemaValue<TSchema, TKey, TPseudoKey>
53-
: TKey extends CSSPseudoClasses
53+
: TKey extends AllCSSPseudoClasses
5454
? // TKey is a valid pseudo class, recursively resolve its child properties
5555
// while passing down the parent pseudo key to resolve any specific schema types.
5656
ApplySchema<TObject[TKey], TSchema, TKey>

packages/react/src/types.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ export type CSSPseudoElements =
5151
| '&::target-text'
5252
| '&::view-transition';
5353

54+
export type FlattenedChainedCSSPseudosClasses =
55+
| '&:visited:active'
56+
| '&:visited:hover'
57+
| '&:active:visited'
58+
| '&:hover::before'
59+
| '&:hover::after'
60+
| '&:focus-visible::before'
61+
| '&:focus-visible::after'
62+
| '&:focus:not(:focus-visible)';
63+
5464
export type CSSPseudoClasses =
5565
| '&:active'
5666
| '&:autofill'
@@ -89,14 +99,16 @@ export type CSSPseudoClasses =
8999
| '&:valid'
90100
| '&:visited';
91101

102+
export type AllCSSPseudoClasses = CSSPseudoClasses | FlattenedChainedCSSPseudosClasses;
103+
92104
/*
93-
* This list of pseudo-classes and pseudo-elements are from csstype
105+
* This list of pseudo-classes, chained pseudo-classes, and pseudo-elements are from csstype
94106
* but with & added to the front. Compiled supports both &-ful
95107
* and &-less forms and both will target the current element
96108
* (`&:hover` <==> `:hover`), however we force the use of the
97109
* &-ful form for consistency with the nested spec for new APIs.
98110
*/
99-
export type CSSPseudos = CSSPseudoElements | CSSPseudoClasses;
111+
export type CSSPseudos = CSSPseudoElements | AllCSSPseudoClasses;
100112

101113
/**
102114
* The XCSSProp must be given all known available properties even

packages/react/src/xcss-prop/__tests__/xcss-prop.test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,35 @@ describe('xcss prop', () => {
260260
/>
261261
).toBeObject();
262262
});
263+
264+
it('should allow chained pseudo elements', () => {
265+
function CSSPropComponent({ xcss }: { xcss: XCSSProp<XCSSAllProperties, '&:hover::after'> }) {
266+
return <div className={xcss}>foo</div>;
267+
}
268+
269+
const styles = cssMap({
270+
redColor: { color: 'red', '&:hover::after': { backgroundColor: 'green' } },
271+
});
272+
273+
const { getByText } = render(<CSSPropComponent xcss={styles.redColor} />);
274+
275+
expect(getByText('foo')).toHaveCompiledCss('color', 'red');
276+
});
277+
278+
it('should type error when given a chained pseudo element and none are allowed', () => {
279+
function CSSPropComponent({ xcss }: { xcss: XCSSProp<XCSSAllProperties, '&:hover'> }) {
280+
return <div className={xcss}>foo</div>;
281+
}
282+
283+
const styles = cssMap({
284+
redColor: { color: 'red', '&:hover::after': { backgroundColor: 'green' } },
285+
});
286+
287+
expectTypeOf(
288+
<CSSPropComponent
289+
// @ts-expect-error — Types of property '"&:hover::after"' are incompatible.
290+
xcss={styles.redColor}
291+
/>
292+
).toBeObject();
293+
});
263294
});

packages/react/src/xcss-prop/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import type * as CSS from 'csstype';
22

33
import type { ApplySchemaValue } from '../create-strict-api/types';
44
import { ax } from '../runtime';
5-
import type { CSSPseudos, CSSPseudoClasses, CSSProperties, StrictCSSProperties } from '../types';
5+
import type { CSSPseudos, CSSProperties, StrictCSSProperties, AllCSSPseudoClasses } from '../types';
66

77
type MarkAsRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
88

99
type XCSSValue<
1010
TStyleDecl extends keyof CSSProperties,
1111
TSchema,
12-
TPseudoKey extends CSSPseudoClasses | ''
12+
TPseudoKey extends AllCSSPseudoClasses | ''
1313
> = {
1414
[Q in keyof StrictCSSProperties]: Q extends TStyleDecl
1515
? ApplySchemaValue<TSchema, Q, TPseudoKey>
@@ -24,7 +24,7 @@ type XCSSPseudo<
2424
> = {
2525
[Q in CSSPseudos]?: Q extends TAllowedPseudos
2626
? MarkAsRequired<
27-
XCSSValue<TAllowedProperties, TSchema, Q extends CSSPseudoClasses ? Q : ''>,
27+
XCSSValue<TAllowedProperties, TSchema, Q extends AllCSSPseudoClasses ? Q : ''>,
2828
TRequiredProperties['requiredProperties']
2929
>
3030
: never;

0 commit comments

Comments
 (0)