Skip to content

Commit

Permalink
feat: add parameter to configure selector limit
Browse files Browse the repository at this point in the history
  • Loading branch information
chase committed Mar 8, 2023
1 parent 27a4103 commit cc64326
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 6 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
8 changes: 4 additions & 4 deletions src/rewriteStyleSheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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

Expand All @@ -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
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/rewriteStyleSheet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 11 additions & 2 deletions src/withPseudoState.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -87,14 +89,21 @@ 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()
}

// Rewrite CSS rules for pseudo-states on all stylesheets to add an alternative selector
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
Expand Down

0 comments on commit cc64326

Please sign in to comment.