Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: handle nesting in comma separated selectors",
"packageName": "@griffel/core",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: adopt API changes from @griffel/core",
"packageName": "@griffel/webpack-extraction-plugin",
"email": "[email protected]",
"dependentChangeType": "patch"
}
36 changes: 19 additions & 17 deletions packages/core/src/runtime/compileCSS.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { compileCSS, CompileCSSOptions, normalizePseudoSelector } from './compileCSS';

const defaultOptions: Pick<CompileCSSOptions, 'rtlClassName' | 'className' | 'media' | 'pseudo' | 'support' | 'layer'> =
{
className: 'foo',
rtlClassName: 'rtl-foo',
media: '',
pseudo: '',
support: '',
layer: '',
};
const defaultOptions: Pick<
CompileCSSOptions,
'rtlClassName' | 'className' | 'media' | 'selectors' | 'support' | 'layer'
> = {
className: 'foo',
rtlClassName: 'rtl-foo',
media: '',
selectors: [],
support: '',
layer: '',
};

describe('compileCSS', () => {
it('handles pseudo', () => {
expect(
compileCSS({
...defaultOptions,
pseudo: ':hover',
selectors: [':hover'],
property: 'color',
value: 'red',
}),
Expand All @@ -27,7 +29,7 @@ describe('compileCSS', () => {
expect(
compileCSS({
...defaultOptions,
pseudo: ':focus:hover',
selectors: [':focus:hover'],
property: 'color',
value: 'red',
}),
Expand Down Expand Up @@ -98,11 +100,11 @@ describe('compileCSS', () => {
`);
});

it('handles rtl properties with preudo selectors', () => {
it('handles rtl properties with pseudo selectors', () => {
expect(
compileCSS({
...defaultOptions,
pseudo: ':before',
selectors: [':before'],

property: 'paddingLeft',
value: '10px',
Expand Down Expand Up @@ -140,7 +142,7 @@ describe('compileCSS', () => {
expect(
compileCSS({
...defaultOptions,
pseudo: ':global(body)',
selectors: [':global(body)'],
property: 'color',
value: 'red',
}),
Expand All @@ -152,13 +154,13 @@ describe('compileCSS', () => {
expect(
compileCSS({
...defaultOptions,
pseudo: ':global(body) &',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original selector is invalid, replaced it with a proper test case.

selectors: [':global(.fui-FluentProvider)', '& .focus:hover'],
property: 'color',
value: 'red',
}),
).toMatchInlineSnapshot(`
Array [
"body .foo{color:red;}",
".fui-FluentProvider .foo .focus:hover{color:red;}",
]
`);
});
Expand All @@ -167,7 +169,7 @@ describe('compileCSS', () => {
expect(
compileCSS({
...defaultOptions,
pseudo: ':global(body)',
selectors: [':global(body)'],
property: 'paddingLeft',
value: '10px',

Expand Down
78 changes: 43 additions & 35 deletions packages/core/src/runtime/compileCSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { normalizeNestedProperty } from './utils/normalizeNestedProperty';
export interface CompileCSSOptions {
className: string;

pseudo: string;
selectors: string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would the length of selectors ever exceed 2 ? It seems that the only case we are handling here is pseudo selector + element. It shouldn't be possible to go further since it would be invalid css ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please provide an example?

media: string;
layer: string;
support: string;
Expand Down Expand Up @@ -60,50 +60,58 @@ export function compileCSSRules(cssRules: string): string[] {
return rules;
}

export function compileCSS(options: CompileCSSOptions): [string /* ltr definition */, string? /* rtl definition */] {
const { className, media, layer, pseudo, support, property, rtlClassName, rtlProperty, rtlValue, value } = options;
function createCSSRule(classNameSelector: string, cssDeclaration: string, pseudos: string[]): string {
let globalSelector = '';
let cssRule = cssDeclaration;

const classNameSelector = `.${className}`;
const cssDeclaration = Array.isArray(value)
? `{ ${value.map(v => `${hyphenateProperty(property)}: ${v}`).join(';')}; }`
: `{ ${hyphenateProperty(property)}: ${value}; }`;
if (pseudos.length > 0) {
cssRule = pseudos.reduceRight((acc, selector, index) => {
// Should be handled by namespace plugin of Stylis, is buggy now
// Issues are reported:
// https://github.com/thysultan/stylis.js/issues/253
// https://github.com/thysultan/stylis.js/issues/252
if (selector.indexOf(':global(') === 0) {
// 👇 :global(GROUP_1)GROUP_2
const GLOBAL_PSEUDO_REGEX = /global\((.+)\)(.+)?/;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe hoist the regex so it doesn't get created on every iteration ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, but in a followup I will get it of this part anyway (I will replace this part with a stylis plugin).

const result = GLOBAL_PSEUDO_REGEX.exec(selector)!;

let rtlClassNameSelector: string | null = null;
let rtlCSSDeclaration: string | null = null;
globalSelector = result[1] + ' ';
const restPseudo = result[2] || '';

if (rtlProperty && rtlClassName) {
rtlClassNameSelector = `.${rtlClassName}`;
rtlCSSDeclaration = Array.isArray(rtlValue)
? `{ ${rtlValue.map(v => `${hyphenateProperty(rtlProperty)}: ${v}`).join(';')}; }`
: `{ ${hyphenateProperty(rtlProperty)}: ${rtlValue}; }`;
}
// should be normalized to handle ":global(SELECTOR) &"
const normalizedPseudoSelector = normalizePseudoSelector(restPseudo);

let cssRule = '';
if (normalizedPseudoSelector === '') {
return acc;
}

// Should be handled by namespace plugin of Stylis, is buggy now
// Issues are reported:
// https://github.com/thysultan/stylis.js/issues/253
// https://github.com/thysultan/stylis.js/issues/252
if (pseudo.indexOf(':global(') === 0) {
// 👇 :global(GROUP_1)GROUP_2
const GLOBAL_PSEUDO_REGEX = /global\((.+)\)(.+)?/;
const [, globalSelector, restPseudo = ''] = GLOBAL_PSEUDO_REGEX.exec(pseudo)!;
return `${normalizedPseudoSelector} { ${acc} }`;
}
Comment on lines +70 to +89
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the hack with small changes, will replace it with a Stylis plugin in a follow up PR.


// should be normalized to handle ":global(SELECTOR) &"
const normalizedPseudo = normalizeNestedProperty(restPseudo.trim());
return `${normalizePseudoSelector(selector)} { ${acc} }`;
}, cssDeclaration);
}

const ltrRule = `${classNameSelector}${normalizedPseudo} ${cssDeclaration}`;
const rtlRule = rtlProperty ? `${rtlClassNameSelector}${normalizedPseudo} ${rtlCSSDeclaration}` : '';
return `${globalSelector}${classNameSelector}{${cssRule}}`;
}

cssRule = `${globalSelector} { ${ltrRule}; ${rtlRule} }`;
} else {
const normalizedPseudo = normalizePseudoSelector(pseudo);
export function compileCSS(options: CompileCSSOptions): [string /* ltr definition */, string? /* rtl definition */] {
const { className, media, layer, selectors, support, property, rtlClassName, rtlProperty, rtlValue, value } = options;

const classNameSelector = `.${className}`;
const cssDeclaration = Array.isArray(value)
? `${value.map(v => `${hyphenateProperty(property)}: ${v}`).join(';')};`
: `${hyphenateProperty(property)}: ${value};`;

let cssRule = createCSSRule(classNameSelector, cssDeclaration, selectors);

cssRule = `${classNameSelector}{${normalizedPseudo} ${cssDeclaration}};`;
if (rtlProperty && rtlClassName) {
const rtlClassNameSelector = `.${rtlClassName}`;
const rtlCSSDeclaration = Array.isArray(rtlValue)
? `${rtlValue.map(v => `${hyphenateProperty(rtlProperty)}: ${v}`).join(';')};`
: `${hyphenateProperty(rtlProperty)}: ${rtlValue};`;

if (rtlProperty) {
cssRule = `${cssRule}; ${rtlClassNameSelector}{${normalizedPseudo} ${rtlCSSDeclaration}};`;
}
cssRule += createCSSRule(rtlClassNameSelector, rtlCSSDeclaration, selectors);
}

if (media) {
Expand Down
27 changes: 15 additions & 12 deletions packages/core/src/runtime/getStyleBucketName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ import { getStyleBucketName } from './getStyleBucketName';

describe('getStyleBucketName', () => {
it('returns bucketName based on mapping', () => {
expect(getStyleBucketName(' :link', '', '', '')).toBe('l');
expect(getStyleBucketName(' :hover ', '', '', '')).toBe('h');
expect(getStyleBucketName([], '', '', '')).toBe('d');
expect(getStyleBucketName(['.foo'], '', '', '')).toBe('d');
Comment on lines +5 to +6
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New test cases.


expect(getStyleBucketName(':link', '', '', '')).toBe('l');
expect(getStyleBucketName(':visited', '', '', '')).toBe('v');
expect(getStyleBucketName(':focus-within', '', '', '')).toBe('w');
expect(getStyleBucketName(':focus', '', '', '')).toBe('f');
expect(getStyleBucketName(':focus-visible', '', '', '')).toBe('i');
expect(getStyleBucketName(':hover', '', '', '')).toBe('h');
expect(getStyleBucketName(':active', '', '', '')).toBe('a');
expect(getStyleBucketName([' :link'], '', '', '')).toBe('l');
expect(getStyleBucketName([' :hover '], '', '', '')).toBe('h');

expect(getStyleBucketName(':active', 'theme', '', '')).toBe('t');
expect(getStyleBucketName(':active', '', '(max-width: 100px)', '')).toBe('m');
expect(getStyleBucketName(':active', '', '', '(display: table-cell)')).toBe('t');
expect(getStyleBucketName([':link'], '', '', '')).toBe('l');
expect(getStyleBucketName([':visited'], '', '', '')).toBe('v');
expect(getStyleBucketName([':focus-within'], '', '', '')).toBe('w');
expect(getStyleBucketName([':focus'], '', '', '')).toBe('f');
expect(getStyleBucketName([':focus-visible'], '', '', '')).toBe('i');
expect(getStyleBucketName([':hover'], '', '', '')).toBe('h');
expect(getStyleBucketName([':active'], '', '', '')).toBe('a');

expect(getStyleBucketName([':active'], 'theme', '', '')).toBe('t');
expect(getStyleBucketName([':active'], '', '(max-width: 100px)', '')).toBe('m');
expect(getStyleBucketName([':active'], '', '', '(display: table-cell)')).toBe('t');
});
});
33 changes: 20 additions & 13 deletions packages/core/src/runtime/getStyleBucketName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ const pseudosMap: Record<string, StyleBucketName | undefined> = {
*
* @internal
*/
export function getStyleBucketName(pseudo: string, layer: string, media: string, support: string): StyleBucketName {
export function getStyleBucketName(
selectors: string[],
layer: string,
media: string,
support: string,
): StyleBucketName {
if (media) {
return 'm';
}
Expand All @@ -52,20 +57,22 @@ export function getStyleBucketName(pseudo: string, layer: string, media: string,
return 't';
}

const normalizedPseudo = pseudo.trim();
if (selectors.length > 0) {
const normalizedPseudo = selectors[0].trim();
Comment on lines +60 to +61
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We take into account only the first pseudo selector, so it's find to use the first element.


if (normalizedPseudo.charCodeAt(0) === 58 /* ":" */) {
// We send through a subset of the string instead of the full pseudo name.
// For example:
// - `"focus-visible"` name would instead of `"us-v"`.
// - `"focus"` name would instead of `"us"`.
// Return a mapped pseudo else default bucket.
if (normalizedPseudo.charCodeAt(0) === 58 /* ":" */) {
// We send through a subset of the string instead of the full pseudo name.
// For example:
// - `"focus-visible"` name would instead of `"us-v"`.
// - `"focus"` name would instead of `"us"`.
// Return a mapped pseudo else default bucket.

return (
pseudosMap[normalizedPseudo.slice(4, 8)] /* allows to avoid collisions between "focus-visible" & "focus" */ ||
pseudosMap[normalizedPseudo.slice(3, 5)] ||
'd'
);
return (
pseudosMap[normalizedPseudo.slice(4, 8)] /* allows to avoid collisions between "focus-visible" & "focus" */ ||
pseudosMap[normalizedPseudo.slice(3, 5)] ||
'd'
);
}
}

// Return default bucket
Expand Down
59 changes: 33 additions & 26 deletions packages/core/src/runtime/resolveStyleRules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,21 @@ describe('resolveStyleRules', () => {
padding-right: 10px;
}
`);

expect(
resolveStyleRules({
':hover,:focus-within': {
'::before': {
color: 'orange',
},
},
}),
).toMatchInlineSnapshot(`
.fij4gri:hover::before,
.fij4gri:focus-within::before {
color: orange;
}
`);
Comment on lines +416 to +429
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test case from #204.

});

it('handles media queries', () => {
Expand Down Expand Up @@ -573,18 +588,6 @@ describe('resolveStyleRules', () => {
`);
});

it('handles :global selector', () => {
expect(
resolveStyleRules({
':global(body) &': { color: 'green' },
}),
).toMatchInlineSnapshot(`
body .fm1e7ra {
color: green;
}
`);
});
Comment on lines -576 to -586
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again a test that does not have any sense.


it('handles :global selector', () => {
expect(
resolveStyleRules({
Expand All @@ -609,34 +612,38 @@ describe('resolveStyleRules', () => {
`);
expect(
resolveStyleRules({
':global(body):focus': { color: 'pink' },
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now spaces are handled (#206).

':global(body) :focus': { color: 'green' },
':global(body) :focus:hover': { color: 'blue' },
':global(body) :focus .foo': { color: 'yellow' },
}),
).toMatchInlineSnapshot(`
body .frou13r:focus {
body .fug6i29:focus {
color: pink;
}
body .frou13r :focus {
color: green;
}
body .f1emv7y1:focus:hover {
body .f1emv7y1 :focus:hover {
color: blue;
}
body .f1g015sp:focus .foo {
body .f1g015sp :focus .foo {
color: yellow;
}
`);
});

// it.todo('supports :global as a nested selector', () => {
// expect(
// resolveStyleRules({
// ':focus': { ':global(body)': { color: 'green' } },
// }),
// ).toMatchInlineSnapshot(`
// body .fm1e7ra0:focus {
// color: green;
// }
// `);
// });
it('supports :global as a nested selector', () => {
expect(
resolveStyleRules({
':focus': { ':global(body)': { color: 'green' } },
}),
).toMatchInlineSnapshot(`
body .fz7er5p:focus {
color: green;
}
`);
});
Comment on lines +636 to +646
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As now we have scopes this test surprisingly works 🐱

});

describe('keyframes', () => {
Expand Down
Loading