From cc64326ffcdf93ea47e11c68e618e5248a599f4b Mon Sep 17 00:00:00 2001 From: chase Date: Wed, 8 Mar 2023 00:45:19 -0800 Subject: [PATCH] feat: add parameter to configure selector limit --- README.md | 15 +++++++++++++++ src/constants.js | 4 ++++ src/rewriteStyleSheet.js | 8 ++++---- src/rewriteStyleSheet.test.js | 17 +++++++++++++++++ src/withPseudoState.js | 13 +++++++++++-- 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bd4c1cb..37d5af0 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,18 @@ Buttons.parameters = { ``` This accepts a single CSS selector (string), or an array of CSS selectors on which to enable that pseudo style. + +### Selector Limits + +For better performance, this addon will only look for pseudo-selectors in the first 1000 selectors of each stylesheet. However, this can be problematic when using frameworks such as Tailwind which may generate a large number of selectors. If necessary, you can configure the maximum number of selectors that will be checked for each stylesheet using the `selectorLimit` parameter: + +```jsx +// in .storybook/preview.js +export const parameters = { + pseudo: { + selectorLimit: 5000 + } +} +``` + +Use `selectorLimit: Infinity` to parse the entire stylesheet, but note that this may have a negative performance impact. Because modified stylesheets may be reused for multiple stories, this parameter should only be defined once in `.storybook/preview.js` rather than dynamically for individual stories. diff --git a/src/constants.js b/src/constants.js index b8b7a59..a3fe457 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,6 +1,10 @@ export const ADDON_ID = "storybook/pseudo-states" export const TOOL_ID = `${ADDON_ID}/tool` +// Parameter that controls the maximum number of selectors +// that we look at (per sheet) to find pseudo-states +export const SELECTOR_LIMIT_PARAMETER = "selectorLimit" + // Pseudo-elements which are not allowed to have classes applied on them // E.g. ::-webkit-scrollbar-thumb.pseudo-hover is not a valid selector export const EXCLUDED_PSEUDO_ELEMENTS = ["::-webkit-scrollbar-thumb"] diff --git a/src/rewriteStyleSheet.js b/src/rewriteStyleSheet.js index 8988b6b..fb607e5 100644 --- a/src/rewriteStyleSheet.js +++ b/src/rewriteStyleSheet.js @@ -9,7 +9,7 @@ const warnings = new Set() const warnOnce = (message) => { if (warnings.has(message)) return // eslint-disable-next-line no-console - console.warn(message) + console.warn(`[storybook-addon-pseudo-states]: ${message}`) warnings.add(message) } @@ -58,7 +58,7 @@ const rewriteRule = (cssText, selectorText, shadowRoot) => { // Rewrites the style sheet to add alternative selectors for any rule that targets a pseudo state. // A sheet can only be rewritten once, and may carry over between stories. -export const rewriteStyleSheet = (sheet, shadowRoot, shadowHosts) => { +export const rewriteStyleSheet = (sheet, shadowRoot, shadowHosts, limit = 1000) => { if (sheet.__pseudoStatesRewritten) return sheet.__pseudoStatesRewritten = true @@ -72,8 +72,8 @@ export const rewriteStyleSheet = (sheet, shadowRoot, shadowHosts) => { if (shadowRoot) shadowHosts.add(shadowRoot.host) } index++ - if (index > 1000) { - warnOnce("Reached maximum of 1000 pseudo selectors per sheet, skipping the rest.") + if (index >= limit) { + warnOnce(`Reached maximum of ${limit} selectors per sheet, skipping the rest.`) break } } diff --git a/src/rewriteStyleSheet.test.js b/src/rewriteStyleSheet.test.js index 1cbc4b0..0b18fc0 100644 --- a/src/rewriteStyleSheet.test.js +++ b/src/rewriteStyleSheet.test.js @@ -17,6 +17,10 @@ class Sheet { } describe("rewriteStyleSheet", () => { + afterEach(() => { + jest.restoreAllMocks() + }) + it("adds alternative selector targeting the element directly", () => { const sheet = new Sheet("a:hover { color: red }") rewriteStyleSheet(sheet) @@ -59,6 +63,19 @@ describe("rewriteStyleSheet", () => { expect(sheet.cssRules[0]).toContain(".pseudo-hover.pseudo-focus a") }) + it("modifies selectors until the limit is reached", () => { + jest.spyOn(console, "warn").mockImplementation(jest.fn()) + + const sheet = new Sheet("a:hover { color: red }", "button:hover { color: blue }") + rewriteStyleSheet(sheet, null, new Set(), 1) + expect(sheet.cssRules[0]).toContain("a.pseudo-hover") + expect(sheet.cssRules[1]).not.toContain("button.pseudo-hover") + + expect(console.warn).toHaveBeenCalledWith( + expect.stringMatching(/Reached maximum of 1 selector/) + ) + }) + it('supports ":host"', () => { const sheet = new Sheet(":host(:hover) { color: red }") rewriteStyleSheet(sheet) diff --git a/src/withPseudoState.js b/src/withPseudoState.js index a85ee46..a2614fa 100644 --- a/src/withPseudoState.js +++ b/src/withPseudoState.js @@ -7,9 +7,11 @@ import { UPDATE_GLOBALS, } from "@storybook/core-events" -import { PSEUDO_STATES } from "./constants" +import { PSEUDO_STATES, SELECTOR_LIMIT_PARAMETER } from "./constants" import { rewriteStyleSheet } from "./rewriteStyleSheet" +let selectorLimit + const channel = addons.getChannel() const shadowHosts = new Set() @@ -87,6 +89,11 @@ export const withPseudoState = (StoryFn, { viewMode, parameters, id, globals: gl return () => clearTimeout(timeout) }, [globals, parameter, viewMode]) + selectorLimit = + typeof parameter?.[SELECTOR_LIMIT_PARAMETER] === "number" + ? parameter[SELECTOR_LIMIT_PARAMETER] + : undefined + return StoryFn() } @@ -94,7 +101,9 @@ export const withPseudoState = (StoryFn, { viewMode, parameters, id, globals: gl const rewriteStyleSheets = (shadowRoot) => { let styleSheets = shadowRoot ? shadowRoot.styleSheets : document.styleSheets if (shadowRoot?.adoptedStyleSheets?.length) styleSheets = shadowRoot.adoptedStyleSheets - Array.from(styleSheets).forEach((sheet) => rewriteStyleSheet(sheet, shadowRoot, shadowHosts)) + Array.from(styleSheets).forEach((sheet) => + rewriteStyleSheet(sheet, shadowRoot, shadowHosts, selectorLimit) + ) } // Only track shadow hosts for the current story