Skip to content

Commit

Permalink
feat: insertionFactory API for @griffel/core (#407)
Browse files Browse the repository at this point in the history
* feat: insertionFactory API

* add tests
  • Loading branch information
layershifter committed Jul 25, 2023
1 parent 9e29013 commit 9f7710e
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 57 deletions.
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add API for styles insertion",
"packageName": "@griffel/core",
"email": "olfedias@microsoft.com",
"dependentChangeType": "patch"
}
23 changes: 11 additions & 12 deletions packages/core/src/__resetStyles.ts
@@ -1,25 +1,24 @@
import { DEBUG_RESET_CLASSES } from './constants';
import { insertionFactory } from './insertionFactory';
import type { MakeResetStylesOptions } from './makeResetStyles';
import type { GriffelInsertionFactory } from './types';

/**
* @internal
*/
export function __resetStyles(ltrClassName: string, rtlClassName: string | null, cssRules: string[]) {
const insertionCache: Record<string, boolean> = {};
export function __resetStyles(
ltrClassName: string,
rtlClassName: string | null,
cssRules: string[],
factory: GriffelInsertionFactory = insertionFactory,
) {
const insertStyles = factory();

function computeClassName(options: MakeResetStylesOptions): string {
const { dir, renderer } = options;
const className = dir === 'ltr' ? ltrClassName : rtlClassName || ltrClassName;

const isLTR = dir === 'ltr';
// As RTL classes are different they should have a different cache key for insertion
const rendererId = isLTR ? renderer.id : renderer.id + 'r';

if (insertionCache[rendererId] === undefined) {
renderer.insertCSSRules({ r: cssRules! });
insertionCache[rendererId] = true;
}

const className = isLTR ? ltrClassName : rtlClassName || ltrClassName;
insertStyles(renderer, { r: cssRules });

if (process.env.NODE_ENV !== 'production') {
DEBUG_RESET_CLASSES[className] = 1;
Expand Down
14 changes: 5 additions & 9 deletions packages/core/src/__styles.ts
@@ -1,6 +1,7 @@
import { debugData, isDevToolsEnabled, getSourceURLfromError } from './devtools';
import { insertionFactory } from './insertionFactory';
import { reduceToClassNameForSlots } from './runtime/reduceToClassNameForSlots';
import type { CSSClassesMapBySlot, CSSRulesByBucket } from './types';
import type { CSSClassesMapBySlot, CSSRulesByBucket, GriffelInsertionFactory } from './types';
import type { MakeStylesOptions } from './makeStyles';

/**
Expand All @@ -11,8 +12,9 @@ import type { MakeStylesOptions } from './makeStyles';
export function __styles<Slots extends string>(
classesMapBySlot: CSSClassesMapBySlot<Slots>,
cssRules: CSSRulesByBucket,
factory: GriffelInsertionFactory = insertionFactory,
) {
const insertionCache: Record<string, boolean> = {};
const insertStyles = factory();

let ltrClassNamesForSlots: Record<Slots, string> | null = null;
let rtlClassNamesForSlots: Record<Slots, string> | null = null;
Expand All @@ -24,10 +26,7 @@ export function __styles<Slots extends string>(

function computeClasses(options: Pick<MakeStylesOptions, 'dir' | 'renderer'>): Record<Slots, string> {
const { dir, renderer } = options;

const isLTR = dir === 'ltr';
// As RTL classes are different they should have a different cache key for insertion
const rendererId = isLTR ? renderer.id : renderer.id + 'r';

if (isLTR) {
if (ltrClassNamesForSlots === null) {
Expand All @@ -39,10 +38,7 @@ export function __styles<Slots extends string>(
}
}

if (insertionCache[rendererId] === undefined) {
renderer.insertCSSRules(cssRules!);
insertionCache[rendererId] = true;
}
insertStyles(renderer, cssRules);

const classNamesForSlots = isLTR
? (ltrClassNamesForSlots as Record<Slots, string>)
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Expand Up @@ -102,6 +102,7 @@ export {
StyleBucketName,
// Util
GriffelRenderer,
GriffelInsertionFactory,
} from './types';

// Private exports, are used by devtools
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/insertionFactory.test.ts
@@ -0,0 +1,25 @@
import { insertionFactory } from './insertionFactory';
import type { GriffelRenderer } from './types';

describe('insertionFactory', () => {
it('should return a function', () => {
expect(insertionFactory()).toBeInstanceOf(Function);
});

it('inserts CSS rules only once per renderer', () => {
const rendererA: Partial<GriffelRenderer> = { id: 'a', insertCSSRules: jest.fn() };
const rendererB: Partial<GriffelRenderer> = { id: 'b', insertCSSRules: jest.fn() };

const insertStyles = insertionFactory();

insertStyles(rendererA as GriffelRenderer, { d: ['a'] });
insertStyles(rendererA as GriffelRenderer, { d: ['a'] });

expect(rendererA.insertCSSRules).toHaveBeenCalledTimes(1);

insertStyles(rendererB as GriffelRenderer, { d: ['a'] });
insertStyles(rendererB as GriffelRenderer, { d: ['a'] });

expect(rendererB.insertCSSRules).toHaveBeenCalledTimes(1);
});
});
18 changes: 18 additions & 0 deletions packages/core/src/insertionFactory.ts
@@ -0,0 +1,18 @@
import type { CSSRulesByBucket, GriffelInsertionFactory, GriffelRenderer } from './types';

/**
* Default implementation of insertion factory. Inserts styles only once per renderer and performs
* insertion immediately after styles computation.
*
* @internal
*/
export const insertionFactory: GriffelInsertionFactory = () => {
const insertionCache: Record<string, boolean> = {};

return function insertStyles(renderer: GriffelRenderer, cssRules: CSSRulesByBucket) {
if (insertionCache[renderer.id] === undefined) {
renderer.insertCSSRules(cssRules!);
insertionCache[renderer.id] = true;
}
};
};
17 changes: 6 additions & 11 deletions packages/core/src/makeResetStyles.ts
@@ -1,16 +1,18 @@
import type { GriffelResetStyle } from '@griffel/style-types';

import { DEBUG_RESET_CLASSES } from './constants';
import { insertionFactory } from './insertionFactory';
import { resolveResetStyleRules } from './runtime/resolveResetStyleRules';
import type { GriffelRenderer } from './types';
import type { GriffelInsertionFactory } from './types';

export interface MakeResetStylesOptions {
dir: 'ltr' | 'rtl';
renderer: GriffelRenderer;
}

export function makeResetStyles(styles: GriffelResetStyle) {
const insertionCache: Record<string, boolean> = {};
export function makeResetStyles(styles: GriffelResetStyle, factory: GriffelInsertionFactory = insertionFactory) {
const insertStyles = factory();

let ltrClassName: string | null = null;
let rtlClassName: string | null = null;
Expand All @@ -24,16 +26,9 @@ export function makeResetStyles(styles: GriffelResetStyle) {
[ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules(styles);
}

const isLTR = dir === 'ltr';
// As RTL classes are different they should have a different cache key for insertion
const rendererId = isLTR ? renderer.id : renderer.id + 'r';
insertStyles(renderer, { r: cssRules! });

if (insertionCache[rendererId] === undefined) {
renderer.insertCSSRules({ r: cssRules! });
insertionCache[rendererId] = true;
}

const className = isLTR ? ltrClassName : rtlClassName || ltrClassName;
const className = dir === 'ltr' ? ltrClassName : rtlClassName || ltrClassName;

if (process.env.NODE_ENV !== 'production') {
DEBUG_RESET_CLASSES[className] = 1;
Expand Down
28 changes: 12 additions & 16 deletions packages/core/src/makeStaticStyles.ts
@@ -1,31 +1,27 @@
import type { GriffelStaticStyles } from '@griffel/style-types';

import { insertionFactory } from './insertionFactory';
import { resolveStaticStyleRules } from './runtime/resolveStaticStyleRules';
import type { GriffelRenderer } from './types';
import type { GriffelInsertionFactory } from './types';

export interface MakeStaticStylesOptions {
renderer: GriffelRenderer;
}

/**
* Register static css.
* @param styles - styles object or string.
*/
export function makeStaticStyles(styles: GriffelStaticStyles | GriffelStaticStyles[]) {
const styleCache: Record<string, true> = {};
export function makeStaticStyles(
styles: GriffelStaticStyles | GriffelStaticStyles[],
factory: GriffelInsertionFactory = insertionFactory,
) {
const insertStyles = factory();
const stylesSet: GriffelStaticStyles[] = Array.isArray(styles) ? styles : [styles];

function useStaticStyles(options: MakeStaticStylesOptions): void {
const { renderer } = options;
const cacheKey = renderer.id;

if (!styleCache[cacheKey]) {
renderer.insertCSSRules({
// 👇 static rules should be inserted into default bucket
d: resolveStaticStyleRules(stylesSet),
});
styleCache[cacheKey] = true;
}
insertStyles(
options.renderer,
// 👇 static rules should be inserted into default bucket
{ d: resolveStaticStyleRules(stylesSet) },
);
}

return useStaticStyles;
Expand Down
26 changes: 25 additions & 1 deletion packages/core/src/makeStyles.test.ts
@@ -1,7 +1,7 @@
import { createDOMRenderer } from './renderer/createDOMRenderer';
import { griffelRendererSerializer } from './common/snapshotSerializers';
import { makeStyles } from './makeStyles';
import { GriffelRenderer } from './types';
import type { GriffelInsertionFactory, GriffelRenderer } from './types';

expect.addSnapshotSerializer(griffelRendererSerializer);

Expand Down Expand Up @@ -177,6 +177,30 @@ describe('makeStyles', () => {
`);
});

it('works with insertionFactory', () => {
const insertionFactory: GriffelInsertionFactory = () => {
return function (renderer, cssRulesByBucket) {
renderer.insertCSSRules(cssRulesByBucket);
};
};
const renderer: Partial<GriffelRenderer> = { insertCSSRules: jest.fn() };

const computeClasses = makeStyles(
{
root: { display: 'flex', paddingLeft: '10px' },
},
insertionFactory,
);
const classes = computeClasses({ dir: 'ltr', renderer: renderer as GriffelRenderer }).root;

expect(classes).toMatchInlineSnapshot(`"___qs05so0 f22iagw frdkuqy"`);

expect(renderer.insertCSSRules).toHaveBeenCalledTimes(1);
expect(renderer.insertCSSRules).toHaveBeenCalledWith({
d: ['.f22iagw{display:flex;}', '.frdkuqy{padding-left:10px;}', '.f81rol6{padding-right:10px;}'],
});
});

it('handles numeric slot names', () => {
const computeClasses = makeStyles({
42: {
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/makeStyles.ts
@@ -1,15 +1,20 @@
import { debugData, isDevToolsEnabled, getSourceURLfromError } from './devtools';
import { insertionFactory } from './insertionFactory';
import { resolveStyleRulesForSlots } from './resolveStyleRulesForSlots';
import { reduceToClassNameForSlots } from './runtime/reduceToClassNameForSlots';
import type { CSSClassesMapBySlot, CSSRulesByBucket, GriffelRenderer, StylesBySlots } from './types';
import type { GriffelInsertionFactory } from './types';

export interface MakeStylesOptions {
dir: 'ltr' | 'rtl';
renderer: GriffelRenderer;
}

export function makeStyles<Slots extends string | number>(stylesBySlots: StylesBySlots<Slots>) {
const insertionCache: Record<string, boolean> = {};
export function makeStyles<Slots extends string | number>(
stylesBySlots: StylesBySlots<Slots>,
factory: GriffelInsertionFactory = insertionFactory,
) {
const insertStyles = factory();

let classesMapBySlot: CSSClassesMapBySlot<Slots> | null = null;
let cssRules: CSSRulesByBucket | null = null;
Expand All @@ -30,8 +35,6 @@ export function makeStyles<Slots extends string | number>(stylesBySlots: StylesB
}

const isLTR = dir === 'ltr';
// As RTL classes are different they should have a different cache key for insertion
const rendererId = isLTR ? renderer.id : renderer.id + 'r';

if (isLTR) {
if (ltrClassNamesForSlots === null) {
Expand All @@ -43,10 +46,7 @@ export function makeStyles<Slots extends string | number>(stylesBySlots: StylesB
}
}

if (insertionCache[rendererId] === undefined) {
renderer.insertCSSRules(cssRules!);
insertionCache[rendererId] = true;
}
insertStyles(renderer, cssRules!);

const classNamesForSlots = isLTR
? (ltrClassNamesForSlots as Record<Slots, string>)
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types.ts
Expand Up @@ -87,6 +87,8 @@ export type CSSRulesByBucket = {
c?: CSSBucketEntry[];
};

export type GriffelInsertionFactory = () => (renderer: GriffelRenderer, cssRules: CSSRulesByBucket) => void;

/** @internal */
export type CSSBucketEntry = string | [string, Record<string, unknown>];

Expand Down

0 comments on commit 9f7710e

Please sign in to comment.