Skip to content

Commit

Permalink
feat: support reset styles (#636)
Browse files Browse the repository at this point in the history
* feat: support reset styles

* chore: remove log

* chore: set slot name

* chore: improve regex

* chore: remove log

* chore: address direction in merge

* chore: use real class in example

* chore: rename param

* chore: improve naming

* chore: one loop

* chore: remove loop

* chore: multiple rules

* chore: handle hover

* chore: mergeDebugSequence UT

* chore: replace array.from usage

* chore: improve getResetDebugSequence

* Change files

---------

Co-authored-by: Charles Assunção <[email protected]>
  • Loading branch information
chpalac and Charles Assunção authored Jan 21, 2025
1 parent dac3edc commit 6bac9a8
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: support reset styles",
"packageName": "@griffel/core",
"email": "email not defined",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: support reset styles",
"packageName": "@griffel/devtools",
"email": "email not defined",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { makeStyles } from '../makeStyles';
import type { MakeStylesOptions } from '../makeStyles';
import { mergeClasses } from '../mergeClasses';
import { createDOMRenderer } from '../renderer/createDOMRenderer';
import { getDebugTree } from './getDebugTree';
import { getAtomicDebugSequenceTree } from './getAtomicDebugSequenceTree';

jest.mock('./isDevToolsEnabled', () => ({
isDevToolsEnabled: true,
Expand All @@ -19,7 +19,7 @@ const findSequenceHash = (classNames: string) =>

const sourceURLregex = /.*[/\\].*:[0-9]+:[0-9]+/; // url with line and column number

describe('getDebugTree', () => {
describe('getAtomicDebugSequenceTree', () => {
it.each<'ltr' | 'rtl'>(['ltr', 'rtl'])('returns styles merge tree when dir=%p', dir => {
const classes = makeStyles({
block: { display: 'block' },
Expand All @@ -35,7 +35,7 @@ describe('getDebugTree', () => {
const sequence1 = findSequenceHash(className1);
const sequence2 = findSequenceHash(className2);

expect(getDebugTree(sequence2!)).toEqual({
expect(getAtomicDebugSequenceTree(sequence2!)).toEqual({
children: [
{
children: [],
Expand Down Expand Up @@ -108,7 +108,7 @@ describe('getDebugTree', () => {
const sequence1 = findSequenceHash(className1);
const sequence2 = findSequenceHash(className2);

expect(getDebugTree(sequence2!)).toEqual({
expect(getAtomicDebugSequenceTree(sequence2!)).toEqual({
children: [
{
children: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { debugData } from './store';
import type { DebugSequence } from './types';
import { getDebugClassNames } from './utils';

export function getDebugTree(debugSequenceHash: SequenceHash, parentNode?: DebugSequence) {
export function getAtomicDebugSequenceTree(debugSequenceHash: SequenceHash, parentNode?: DebugSequence) {
const lookupItem: LookupItem | undefined = DEFINITION_LOOKUP_TABLE[debugSequenceHash];
if (lookupItem === undefined) {
return undefined;
Expand All @@ -29,7 +29,7 @@ export function getDebugTree(debugSequenceHash: SequenceHash, parentNode?: Debug
childrenSequences
.reverse() // first process the overriding children that are merged last
.forEach((sequence: SequenceHash) => {
const child = getDebugTree(sequence, node);
const child = getAtomicDebugSequenceTree(sequence, node);
if (child) {
node.children.push(child);
}
Expand Down
67 changes: 67 additions & 0 deletions packages/core/src/devtools/getResetDebugSequence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { RESET_HASH_PREFIX } from '../constants';
import { makeResetStyles } from '../makeResetStyles';
import type { MakeStylesOptions } from '../makeStyles';
import { mergeClasses } from '../mergeClasses';
import { createDOMRenderer } from '../renderer/createDOMRenderer';
import { getResetDebugSequence } from './getResetDebugSequence';

jest.mock('./isDevToolsEnabled', () => ({
isDevToolsEnabled: true,
}));

const options: MakeStylesOptions = {
dir: 'ltr',
renderer: createDOMRenderer(document),
};

const findResetHash = (classNames: string) =>
classNames.split(' ').find(className => className.startsWith(RESET_HASH_PREFIX));

describe('getResetDebugSequence', () => {
it('returns correct debug tree for reset styles', () => {
const resetClassName = makeResetStyles({
margin: '0',
padding: '0',
})(options);
const className = mergeClasses('ui-button', resetClassName);
const classNameForLookup = findResetHash(className)!;
const result = getResetDebugSequence(classNameForLookup)!;
expect(result).toEqual({
children: [],
debugClassNames: [{ className: classNameForLookup }],
direction: options.dir,
rules: expect.any(Object),
sequenceHash: classNameForLookup,
slot: 'makeResetStyles()',
});
expect(result.rules).toMatchInlineSnapshot(`
Object {
"r1l95nvm": ".r1l95nvm{margin:0;padding:0;}",
}
`);
});
it('handles reset styles with nested selectors', () => {
const resetClassName = makeResetStyles({
margin: '0',
padding: '0',
':hover': { color: 'blue' },
'& .foo': { color: 'red' },
})(options);
const className = mergeClasses('ui-button', resetClassName);
const classNameForLookup = findResetHash(className)!;
const result = getResetDebugSequence(classNameForLookup)!;
expect(result).toEqual({
children: [],
debugClassNames: [{ className: classNameForLookup }],
direction: options.dir,
rules: expect.any(Object),
sequenceHash: classNameForLookup,
slot: 'makeResetStyles()',
});
expect(result.rules).toMatchInlineSnapshot(`
Object {
"rf14qlw": ".rf14qlw{margin:0;padding:0;}.rf14qlw:hover{color:blue;}.rf14qlw .foo{color:red;}",
}
`);
});
});
32 changes: 32 additions & 0 deletions packages/core/src/devtools/getResetDebugSequence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DEBUG_RESET_CLASSES } from '../constants';
import type { SequenceHash } from '../types';
import { debugData } from './store';
import type { DebugSequence } from './types';

export function getResetDebugSequence(debugSequenceHash: SequenceHash) {
const resetClass = DEBUG_RESET_CLASSES[debugSequenceHash];
if (resetClass === undefined) {
return undefined;
}

const debugClassNames = [{ className: debugSequenceHash }];

const node: DebugSequence = {
sequenceHash: debugSequenceHash,
direction: 'ltr',
children: [],
debugClassNames,
};

node.rules = {};
node.slot = 'makeResetStyles()';

const [{ className }] = node.debugClassNames;
const cssRules = debugData.getCSSRules().filter(cssRule => {
return cssRule.includes(`.${className}`);
});

node.rules![className] = cssRules.join('');

return node;
}
24 changes: 15 additions & 9 deletions packages/core/src/devtools/injectDevTools.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { SEQUENCE_PREFIX } from '../constants';
import { getDebugTree } from './getDebugTree';
import { SEQUENCE_PREFIX, DEBUG_RESET_CLASSES } from '../constants';
import { mergeDebugSequence } from './mergeDebugSequence';

export function injectDevTools(document: Document) {
const window = document.defaultView;
if (!window || window.__GRIFFEL_DEVTOOLS__) {
return;
}

const devtools: typeof window['__GRIFFEL_DEVTOOLS__'] = {
const devtools: (typeof window)['__GRIFFEL_DEVTOOLS__'] = {
getInfo: (element: HTMLElement) => {
const rootDebugSequenceHash = Array.from(element.classList).find(className =>
className.startsWith(SEQUENCE_PREFIX),
);
if (rootDebugSequenceHash === undefined) {
return undefined;
let rootDebugSequenceHash: string | undefined;
let rootResetDebugClassName: string | undefined;

for (const className of element.classList) {
if (className.startsWith(SEQUENCE_PREFIX)) {
rootDebugSequenceHash = className;
return;
}
if (DEBUG_RESET_CLASSES[className]) {
rootResetDebugClassName = className;
}
}

return getDebugTree(rootDebugSequenceHash);
return mergeDebugSequence(rootDebugSequenceHash, rootResetDebugClassName);
},
};

Expand Down
83 changes: 83 additions & 0 deletions packages/core/src/devtools/mergeDebugSequence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { DEFINITION_LOOKUP_TABLE, RESET_HASH_PREFIX, SEQUENCE_PREFIX } from '../constants';
import { makeResetStyles } from '../makeResetStyles';
import { makeStyles } from '../makeStyles';
import type { MakeStylesOptions } from '../makeStyles';
import { createDOMRenderer } from '../renderer/createDOMRenderer';
import { mergeDebugSequence } from './mergeDebugSequence';
import { getDebugClassNames } from './utils';

jest.mock('./isDevToolsEnabled', () => ({
isDevToolsEnabled: true,
}));

const options: MakeStylesOptions = {
dir: 'ltr',
renderer: createDOMRenderer(document),
};

const findSequenceHash = (classNames: string) =>
classNames.split(' ').find(className => className.startsWith(SEQUENCE_PREFIX));

const findResetHash = (classNames: string) =>
classNames.split(' ').find(className => className.startsWith(RESET_HASH_PREFIX));

describe('mergeDebugSequence', () => {
it('returns undefined when both inputs are undefined', () => {
expect(mergeDebugSequence(undefined, undefined)).toBeUndefined();
});

it('returns atomic debug tree when reset classes is undefined', () => {
const classes = makeStyles({
block: { display: 'block' },
})(options);

const atomicSequence = findSequenceHash(classes.block);
const result = mergeDebugSequence(atomicSequence, undefined);

expect(result?.sequenceHash).toBe(atomicSequence);
expect(result?.children).toHaveLength(0);
});

it('returns reset debug tree when atomic classes is undefined', () => {
const classes = makeResetStyles({ margin: 0 })(options);

const resetSequence = findResetHash(classes);
const result = mergeDebugSequence(undefined, resetSequence);

expect(result?.sequenceHash).toBe(resetSequence);
expect(result?.children).toHaveLength(0);
});

it('correctly merges atomic and reset debug trees', () => {
const atomicClasses = makeStyles({
block: { display: 'block' },
})(options);

const resetClasses = makeResetStyles({
margin: 0,
padding: 0,
})(options);

const atomicSequence = findSequenceHash(atomicClasses.block);
const resetSequence = findResetHash(resetClasses);

const result = mergeDebugSequence(atomicSequence, resetSequence);

const debugClassname = getDebugClassNames(DEFINITION_LOOKUP_TABLE[atomicSequence!]);
expect(result).toMatchObject({
sequenceHash: expect.stringContaining(atomicSequence! + resetSequence!),
direction: 'ltr',
children: expect.arrayContaining([
expect.objectContaining({
sequenceHash: atomicSequence,
debugClassNames: expect.any(Array),
}),
expect.objectContaining({
sequenceHash: resetSequence,
debugClassNames: expect.any(Array),
}),
]),
debugClassNames: expect.arrayContaining([...debugClassname, { className: resetSequence }]),
});
});
});
32 changes: 32 additions & 0 deletions packages/core/src/devtools/mergeDebugSequence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getAtomicDebugSequenceTree } from './getAtomicDebugSequenceTree';
import { getResetDebugSequence } from './getResetDebugSequence';
import type { DebugSequence } from './types';

export function mergeDebugSequence(
atomicClases: string | undefined,
resetClassName: string | undefined,
): DebugSequence | undefined {
const debugResultRootAtomic = atomicClases ? getAtomicDebugSequenceTree(atomicClases) : undefined;
const debugResultRootReset = resetClassName ? getResetDebugSequence(resetClassName) : undefined;

if (!debugResultRootAtomic && !debugResultRootReset) {
return undefined;
}

if (!debugResultRootAtomic) {
return debugResultRootReset;
}

if (!debugResultRootReset) {
return debugResultRootAtomic;
}

const debugResultRoot: DebugSequence = {
sequenceHash: debugResultRootAtomic.sequenceHash + debugResultRootReset.sequenceHash,
direction: debugResultRootAtomic.direction,
children: [debugResultRootAtomic, debugResultRootReset],
debugClassNames: [...debugResultRootAtomic.debugClassNames, ...debugResultRootReset.debugClassNames],
};

return debugResultRoot;
}
2 changes: 1 addition & 1 deletion packages/devtools/src/getMonolithicCSSRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as stylis from 'stylis';
import type { AtomicRules, MonolithicAtRules, MonolithicRules, RuleDetail } from './types';

// match className like '.fvnxzrg' or '.fvnxzrg.fui-focus-visible'
const griffelClassNameRegex = /^\.(f[0-9a-z]+)(\..*|$)/;
const griffelClassNameRegex = /^\.([fr][0-9a-z]+)(\..*|$)/;

function parseRuleElement(element: stylis.Element, overriddenBy?: string) {
// example of `value`: `.f3xbvq9:hover`
Expand Down
20 changes: 19 additions & 1 deletion packages/devtools/src/stories/FlattenView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { StoryObj } from '@storybook/react';
import { FlattenView } from '../FlattenView';
import { darkTheme, lightTheme } from '../themes';

const debugResultRoot: DebugResult = {
const debugResultRootAtomic: DebugResult = {
sequenceHash: '___29aowz0_1rg0tlg',
direction: 'ltr',
children: [
Expand Down Expand Up @@ -96,6 +96,24 @@ const debugResultRoot: DebugResult = {
],
};

const debugResultRootReset: DebugResult = {
sequenceHash: 'r8e9yta',
direction: 'ltr',
children: [],
debugClassNames: [{ className: 'r8e9yta' }],
rules: {
r8e9yta: '.r8e9yta{background-color:yellow; width: 100%;}.r8e9yta:hover{background-color:red;}',
},
slot: 'RESET STYLES',
};

const debugResultRoot: DebugResult = {
sequenceHash: debugResultRootAtomic.sequenceHash + debugResultRootReset.sequenceHash,
direction: 'ltr',
children: [debugResultRootAtomic, debugResultRootReset],
debugClassNames: [...debugResultRootAtomic.debugClassNames, ...debugResultRootReset.debugClassNames],
};

export const Default: StoryObj<{ theme: 'dark' | 'light' }> = {
render: ({ theme }) => {
const tokens = theme === 'dark' ? darkTheme : lightTheme;
Expand Down

0 comments on commit 6bac9a8

Please sign in to comment.