Skip to content

Commit

Permalink
Merge pull request #47 from chromaui/ghengeveld/ap-2628-pseudo-states…
Browse files Browse the repository at this point in the history
…-addon-storybook-7-support

Convert to TypeScript and upgrade to Storybook 7
  • Loading branch information
ghengeveld committed Dec 15, 2022
2 parents 27a4103 + bc9d22a commit 5bff30b
Show file tree
Hide file tree
Showing 26 changed files with 10,705 additions and 178 deletions.
14 changes: 2 additions & 12 deletions .babelrc.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
const modules = process.env.BABEL_ESM === 'true' ? false : 'auto';

module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: 'defaults',
modules
}
],
"@babel/preset-react"
],
targets: "defaults",
presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
}
1 change: 1 addition & 0 deletions .github/workflows/chromatic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: 'build:storybook'
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ dist/
node_modules/
storybook-static/
build-storybook.log
package-lock.json
yarn.lock
.env
17 changes: 13 additions & 4 deletions .storybook/main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
module.exports = {
stories: ["../stories/**/*.stories.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"],
addons: ["@storybook/addon-docs", "../preset.js"],
core: {
builder: "webpack5",
stories: ["../stories/**/*.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"../",
],
framework: {
name: "@storybook/react-webpack5",
options: {},
},
docs: {
docsPage: true,
},
}
9 changes: 9 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}
1 change: 1 addition & 0 deletions manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./dist/manager.mjs"
68 changes: 45 additions & 23 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,71 @@
},
"author": "ghengeveld",
"license": "MIT",
"main": "dist/cjs/preset.js",
"module": "dist/esm/preset.js",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
"./manager": {
"require": "./dist/manager.js",
"import": "./dist/manager.mjs",
"types": "./dist/manager.d.ts"
},
"./preview": {
"require": "./dist/preview.js",
"import": "./dist/preview.mjs",
"types": "./dist/preview.d.ts"
},
"./package.json": "./package.json"
},
"files": [
"dist/**/*",
"README.md",
"*.js"
"*.js",
"dist/**/*"
],
"scripts": {
"clean": "rimraf ./dist",
"start": "concurrently \"yarn storybook --no-manager-cache --quiet\" \"yarn build:dist --watch\"",
"storybook": "start-storybook -p 6006",
"storybook": "storybook dev -p 6006",
"test": "jest src",
"chromatic": "chromatic",
"build:dist": "babel ./src --out-dir ./dist/cjs && BABEL_ESM=\"true\" babel ./src --out-dir ./dist/esm",
"build:storybook": "build-storybook",
"chromatic": "npx chromatic",
"build:dist": "tsup",
"build:storybook": "storybook build",
"prepublish": "yarn clean && yarn build:dist",
"release": "auto shipit --base-branch=main"
},
"dependencies": {},
"devDependencies": {
"@babel/cli": "^7.19.3",
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@storybook/addon-docs": "^6.5.14",
"@storybook/builder-webpack5": "^6.5.14",
"@storybook/manager-webpack5": "^6.5.14",
"@storybook/react": "^6.5.14",
"@babel/preset-typescript": "^7.18.6",
"@storybook/addon-essentials": "^7.0.0-beta.3",
"@storybook/addon-interactions": "^7.0.0-beta.3",
"@storybook/addon-links": "^7.0.0-beta.3",
"@storybook/react": "^7.0.0-beta.3",
"@storybook/react-webpack5": "^7.0.0-beta.3",
"@storybook/testing-library": "^0.0.13",
"@storybook/types": "^7.0.0-beta.3",
"@types/jest": "^29.2.4",
"auto": "^10.16.8",
"babel-loader": "^9.1.0",
"chromatic": "^6.12.0",
"concurrently": "^5.3.0",
"jest": "^27.5.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"rimraf": "^3.0.2"
"rimraf": "^3.0.2",
"storybook": "^7.0.0-beta.3",
"tsup": "^6.5.0",
"typescript": "^4.9.4"
},
"peerDependencies": {
"@storybook/addons": "^6.5.0",
"@storybook/api": "^6.5.0",
"@storybook/components": "^6.5.0",
"@storybook/core-events": "^6.5.0",
"@storybook/theming": "^6.5.0",
"@storybook/components": "next",
"@storybook/core-events": "next",
"@storybook/manager-api": "next",
"@storybook/preview-api": "next",
"@storybook/theming": "next",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
Expand Down
9 changes: 0 additions & 9 deletions preset.js

This file was deleted.

1 change: 1 addition & 0 deletions preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./dist/preview.mjs"
4 changes: 3 additions & 1 deletion src/constants.js → src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export const PSEUDO_STATES = {
visited: "visited",
link: "link",
target: "target",
}
} as const

export type PseudoState = keyof typeof PSEUDO_STATES
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
if (module && module.hot && module.hot.decline) {
module.hot.decline()
}

// make it work with --isolatedModules
export default {}
6 changes: 3 additions & 3 deletions src/preset/manager.js → src/manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { addons, types } from "@storybook/addons"
import { addons, types } from "@storybook/manager-api"

import { ADDON_ID, TOOL_ID } from "../constants"
import { PseudoStateTool } from "../PseudoStateTool"
import { ADDON_ID, TOOL_ID } from "./constants"
import { PseudoStateTool } from "./manager/PseudoStateTool"

addons.register(ADDON_ID, () => {
addons.add(TOOL_ID, {
Expand Down
6 changes: 3 additions & 3 deletions src/PseudoStateTool.js → src/manager/PseudoStateTool.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useCallback, useMemo } from "react"
import { useGlobals } from "@storybook/api"
import React, { useCallback } from "react"
import { Icons, IconButton, WithTooltip, TooltipLinkList } from "@storybook/components"
import { useGlobals } from "@storybook/manager-api"
import { styled, color } from "@storybook/theming"

import { PSEUDO_STATES } from "./constants"
import { PSEUDO_STATES } from "../constants"

const LinkTitle = styled.span(({ active }) => ({
color: active ? color.secondary : "inherit",
Expand Down
3 changes: 0 additions & 3 deletions src/preset/preview.js

This file was deleted.

3 changes: 3 additions & 0 deletions src/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { withPseudoState } from "./preview/withPseudoState"

export const decorators = [withPseudoState]
84 changes: 84 additions & 0 deletions src/preview/rewriteStyleSheet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { rewriteStyleSheet } from "./rewriteStyleSheet"

class Rule {
cssText: any
selectorText: any
constructor(cssText: string) {
this.cssText = cssText
this.selectorText = cssText.slice(0, cssText.indexOf(" {"))
}
toString() {
return this.cssText
}
}

class Sheet {
__pseudoStatesRewritten: boolean
cssRules: CSSStyleRule[]

constructor(...rules: string[]) {
this.__pseudoStatesRewritten = false
this.cssRules = rules.map((cssText) => new Rule(cssText) as CSSStyleRule)
}
deleteRule(index: number) {
this.cssRules.splice(index, 1)
}
insertRule(cssText: string, index: number) {
this.cssRules.splice(index, 0, new Rule(cssText) as CSSStyleRule)
}
}

describe("rewriteStyleSheet", () => {
it("adds alternative selector targeting the element directly", () => {
const sheet = new Sheet("a:hover { color: red }")
rewriteStyleSheet(sheet as any)
expect(sheet.cssRules[0].selectorText).toContain("a.pseudo-hover")
})

it("adds alternative selector targeting an ancestor", () => {
const sheet = new Sheet("a:hover { color: red }")
rewriteStyleSheet(sheet as any)
expect(sheet.cssRules[0].selectorText).toContain(".pseudo-hover a")
})

it("does not add .pseudo-<class> to pseudo-class, which does not support classes", () => {
const sheet = new Sheet("::-webkit-scrollbar-thumb:hover { border-color: transparent; }")
rewriteStyleSheet(sheet as any)
expect(sheet.cssRules[0].selectorText).not.toContain("::-webkit-scrollbar-thumb.pseudo-hover")
})

it("adds alternative selector for each pseudo selector", () => {
const sheet = new Sheet("a:hover, a:focus { color: red }")
rewriteStyleSheet(sheet as any)
expect(sheet.cssRules[0].selectorText).toContain("a.pseudo-hover")
expect(sheet.cssRules[0].selectorText).toContain("a.pseudo-focus")
expect(sheet.cssRules[0].selectorText).toContain(".pseudo-hover a")
expect(sheet.cssRules[0].selectorText).toContain(".pseudo-focus a")
})

it("keeps non-pseudo selectors as-is", () => {
const sheet = new Sheet("a.class, a:hover, a:focus, a#id { color: red }")
rewriteStyleSheet(sheet as any)
expect(sheet.cssRules[0].selectorText).toContain("a.class")
expect(sheet.cssRules[0].selectorText).toContain("a#id")
})

it("supports combined pseudo selectors", () => {
const sheet = new Sheet("a:hover:focus { color: red }")
rewriteStyleSheet(sheet as any)
expect(sheet.cssRules[0].selectorText).toContain("a.pseudo-hover.pseudo-focus")
expect(sheet.cssRules[0].selectorText).toContain(".pseudo-hover.pseudo-focus a")
})

it('supports ":host"', () => {
const sheet = new Sheet(":host(:hover) { color: red }")
rewriteStyleSheet(sheet as any)
expect(sheet.cssRules[0].cssText).toEqual(":host(:hover), :host(.pseudo-hover) { color: red }")
})

it('supports ":not"', () => {
const sheet = new Sheet(":not(:hover) { color: red }")
rewriteStyleSheet(sheet as any)
expect(sheet.cssRules[0].cssText).toEqual(":not(:hover), :not(.pseudo-hover) { color: red }")
})
})
40 changes: 23 additions & 17 deletions src/rewriteStyleSheet.js → src/preview/rewriteStyleSheet.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { PSEUDO_STATES, EXCLUDED_PSEUDO_ELEMENTS } from "./constants"
import { PSEUDO_STATES, EXCLUDED_PSEUDO_ELEMENTS } from "../constants"
import { splitSelectors } from "./splitSelectors"

const pseudoStates = Object.values(PSEUDO_STATES)
const matchOne = new RegExp(`:(${pseudoStates.join("|")})`)
const matchAll = new RegExp(`:(${pseudoStates.join("|")})`, "g")

const warnings = new Set()
const warnOnce = (message) => {
const warnOnce = (message: string) => {
if (warnings.has(message)) return
// eslint-disable-next-line no-console
console.warn(message)
warnings.add(message)
}

const isExcludedPseudoElement = (selector, pseudoState) =>
const isExcludedPseudoElement = (selector: string, pseudoState: string) =>
EXCLUDED_PSEUDO_ELEMENTS.some((element) => selector.endsWith(`${element}:${pseudoState}`))

const rewriteRule = (cssText, selectorText, shadowRoot) => {
const rewriteRule = ({ cssText, selectorText }: CSSStyleRule, shadowRoot?: ShadowRoot) => {
return cssText.replace(
selectorText,
splitSelectors(selectorText)
Expand All @@ -28,17 +28,15 @@ const rewriteRule = (cssText, selectorText, shadowRoot) => {
return [selector]
}

const states = []
const states: string[] = []
const plainSelector = selector.replace(matchAll, (_, state) => {
states.push(state)
return ""
})
const classSelector = states.reduce(
(acc, state) =>
!isExcludedPseudoElement(selector, state) &&
acc.replace(new RegExp(`(?<!Y):${state}`, "g"), `.pseudo-${state}`),
selector
)
const classSelector = states.reduce((acc, state) => {
if (isExcludedPseudoElement(selector, state)) return ""
return acc.replace(new RegExp(`(?<!Y):${state}`, "g"), `.pseudo-${state}`)
}, selector)

if (selector.startsWith(":host(") || selector.startsWith("::slotted(")) {
return [selector, classSelector].filter(Boolean)
Expand All @@ -58,18 +56,26 @@ 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: CSSStyleSheet,
shadowRoot?: ShadowRoot,
shadowHosts?: Set<Element>
) => {
// @ts-expect-error
if (sheet.__pseudoStatesRewritten) return
// @ts-expect-error
sheet.__pseudoStatesRewritten = true

try {
let index = 0
for (const { cssText, selectorText } of sheet.cssRules) {
if (matchOne.test(selectorText)) {
const newRule = rewriteRule(cssText, selectorText, shadowRoot)
for (const cssRule of sheet.cssRules) {
if (!("selectorText" in cssRule)) continue
const styleRule = cssRule as CSSStyleRule
if (matchOne.test(styleRule.selectorText)) {
const newRule = rewriteRule(styleRule, shadowRoot)
sheet.deleteRule(index)
sheet.insertRule(newRule, index)
if (shadowRoot) shadowHosts.add(shadowRoot.host)
if (shadowRoot && shadowHosts) shadowHosts.add(shadowRoot.host)
}
index++
if (index > 1000) {
Expand All @@ -78,7 +84,7 @@ export const rewriteStyleSheet = (sheet, shadowRoot, shadowHosts) => {
}
}
} catch (e) {
if (e.toString().includes("cssRules")) {
if (String(e).includes("cssRules")) {
warnOnce(`Can't access cssRules, likely due to CORS restrictions: ${sheet.href}`)
} else {
// eslint-disable-next-line no-console
Expand Down
File renamed without changes.
Loading

0 comments on commit 5bff30b

Please sign in to comment.