Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
57 changes: 57 additions & 0 deletions storybook/decorators/utils/use-shared-style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useLayoutEffect } from '@wordpress/element';

interface StyleEntry {
element: HTMLStyleElement;
refCount: number;
}

const styleRefs = new Map< string, StyleEntry >();

/**
* Injects a `<style>` element into the document head, ref-counted by `key`.
*
* When multiple Storybook story instances need the same stylesheet (e.g. on
* the Docs tab where several stories render simultaneously), this hook ensures
* only a single `<style>` element is created. It is removed from the DOM when
* the last consumer unmounts.
*
* @param options
* @param options.key A unique identifier for the stylesheet. Callers with
* the same key share one `<style>` element. Pass an
* empty string to skip injection.
* @param options.cssText The CSS text to inject. Pass an empty string to skip
* injection.
*/
export function useSharedStyle( {
key,
cssText,
}: {
key: string;
cssText: string;
} ): void {
useLayoutEffect( () => {
if ( ! key || ! cssText ) {
return;
}

let entry = styleRefs.get( key );

if ( entry ) {
entry.refCount++;
} else {
const style = document.createElement( 'style' );
style.textContent = cssText;
document.head.appendChild( style );
entry = { element: style, refCount: 1 };
styleRefs.set( key, entry );
}

return () => {
entry.refCount--;
if ( entry.refCount === 0 ) {
entry.element.remove();
styleRefs.delete( key );
}
};
}, [ key, cssText ] );
}
12 changes: 5 additions & 7 deletions storybook/decorators/with-global-css.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import clsx from 'clsx';
import { useEffect } from '@wordpress/element';
import basicStyles from '../global-basic.scss?inline';
import wordPressStyles from '../global-wordpress.scss?inline';
import { useSharedStyle } from './utils/use-shared-style';

/**
* A Storybook decorator to inject global CSS.
Expand Down Expand Up @@ -42,12 +42,10 @@ export const WithGlobalCSS = ( Story, context ) => {
const { lazyStyles, externalStyles, classes } =
config[ context.globals.css ];

useEffect( () => {
const style = document.createElement( 'style' );
style.textContent = lazyStyles.join( '\n' );
document.head.appendChild( style );
return () => document.head.removeChild( style );
}, [ context.globals.css, lazyStyles ] );
useSharedStyle( {
key: 'global:' + context.globals.css,
cssText: lazyStyles.join( '\n' ),
} );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Curious: Do we need this key at all. Could this just be ... ?

Suggested change
useSharedStyle( {
key: 'global:' + context.globals.css,
cssText: lazyStyles.join( '\n' ),
} );
useSharedStyle( lazyStyles.join( '\n' ) );

Granted, these strings could be pretty long, but we're holding them in memory regardless, and I dunno if that would really impact the map lookup performance vs. shorter keys.

Or is there some reason the key needs to be stable independent from the CSS text?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Granted, these strings could be pretty long, but we're holding them in memory regardless, and I dunno if that would really impact the map lookup performance vs. shorter keys.

It doesn't initially seem that it should have a significant impact:

short key x 300,047,730 ops/sec ±5.03% (79 runs sampled)
long key x 288,352,484 ops/sec ±6.24% (76 runs sampled)
Benchmarking code
import Benchmark from "benchmark";

const suite = new Benchmark.Suite();

const shortKey = "a";
const longKey = "a".repeat(500);

const mapShortKeys = new Map([[shortKey, true]]);
const mapLongKeys = new Map([[longKey, true]]);

suite.add("short key", () => {
  mapShortKeys.get(shortKey);
});

suite.add("long key", () => {
  mapLongKeys.get(longKey);
});

suite
  .run({ async: true })
  .on("cycle", (event) => {
    console.log(String(event.target));
  })
  .on("complete", () => {
    console.log("Fastest is " + suite.filter("fastest").map("name"));
  });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, simplified in 88aba00


return (
<div className={ clsx( classes ) }>
Expand Down
41 changes: 18 additions & 23 deletions storybook/decorators/with-rtl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@
* WordPress dependencies
*/
import { addFilter, removeFilter } from '@wordpress/hooks';
import {
useEffect,
useLayoutEffect,
useRef,
useState,
} from '@wordpress/element';
import { useEffect, useRef, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import CONFIG from '../package-styles/config';
import { useSharedStyle } from './utils/use-shared-style';

export const WithRTL = ( Story, context ) => {
const [ rerenderKey, setRerenderKey ] = useState( 0 );
Expand Down Expand Up @@ -41,23 +37,22 @@ export const WithRTL = ( Story, context ) => {
return () => removeFilter( 'i18n.gettext_with_context', 'storybook' );
}, [ context.globals.direction ] );

useLayoutEffect( () => {
const stylesToUse = [];

CONFIG.forEach( ( item ) => {
if ( item.componentIdMatcher.test( context.componentId ) ) {
stylesToUse.push( ...item[ context.globals.direction ] );
}
} );

const style = document.createElement( 'style' );
style.textContent = stylesToUse.join( '\n' );
document.head.appendChild( style );

return () => {
document.head.removeChild( style );
};
}, [ context.componentId, context.globals.direction ] );
const matchedIndices = [];
const stylesToUse = [];

CONFIG.forEach( ( item, index ) => {
if ( item.componentIdMatcher.test( context.componentId ) ) {
matchedIndices.push( index );
stylesToUse.push( ...item[ context.globals.direction ] );
}
} );

useSharedStyle( {
key: matchedIndices.length
? matchedIndices.join( ',' ) + ':' + context.globals.direction
: '',
cssText: stylesToUse.join( '\n' ),
} );

return (
<div ref={ ref } key={ rerenderKey }>
Expand Down
Loading