From d09aa9ac9296de2e133fe7aebe01576468ddeaf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliz=C3=A9=20Debray?= <33580481+alizedebray@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:43:33 +0200 Subject: [PATCH] fix(docs): add back post styles to storybook iframes (#3580) Co-authored-by: Philipp Gfeller <1659006+gfellerph@users.noreply.github.com> --- .../post-accordion/post-accordion.tsx | 2 + .../.storybook/addons/addons.scss | 2 +- .../addons/styles-switcher/StylesSwitcher.tsx | 153 +++++++++++------- .../.storybook/preview-head.html | 1 + .../.storybook/styles/preview.scss | 4 + packages/styles/src/components/card.scss | 1 + .../src/variables/components/_card.scss | 1 + pnpm-lock.yaml | 45 +++++- 8 files changed, 147 insertions(+), 62 deletions(-) diff --git a/packages/components/src/components/post-accordion/post-accordion.tsx b/packages/components/src/components/post-accordion/post-accordion.tsx index 04a597916e..407c5cf324 100644 --- a/packages/components/src/components/post-accordion/post-accordion.tsx +++ b/packages/components/src/components/post-accordion/post-accordion.tsx @@ -50,6 +50,8 @@ export class PostAccordion { @Listen('postToggle') collapseToggleHandler(event: CustomEvent) { + event.stopPropagation(); + const toggledItem = event.target as HTMLElement; const closestParentAccordion = toggledItem.closest('post-accordion'); diff --git a/packages/documentation/.storybook/addons/addons.scss b/packages/documentation/.storybook/addons/addons.scss index b34b9b2c90..eb90a8a8dc 100644 --- a/packages/documentation/.storybook/addons/addons.scss +++ b/packages/documentation/.storybook/addons/addons.scss @@ -9,7 +9,7 @@ top: -5px; right: 0; padding: post.$size-mini; - background-color: var(--post-light); + background-color: post.$white; border: post.$border-width solid post.$border-color; border-radius: post.$border-radius; font-size: post.$font-size-sm; diff --git a/packages/documentation/.storybook/addons/styles-switcher/StylesSwitcher.tsx b/packages/documentation/.storybook/addons/styles-switcher/StylesSwitcher.tsx index 666550feac..722fd13277 100644 --- a/packages/documentation/.storybook/addons/styles-switcher/StylesSwitcher.tsx +++ b/packages/documentation/.storybook/addons/styles-switcher/StylesSwitcher.tsx @@ -1,101 +1,146 @@ import React, { useEffect, useState } from 'react'; import { IconButton, WithTooltip } from '@storybook/components'; -const STYLESHEET_ID = 'preview-stylesheet'; +const THEMES = ['Post'] as const; +const CHANNELS = ['External', 'Internal'] as const; +const MODES = ['Light', 'Dark'] as const; + +/* + * Stylesheets + */ +const getStylesheetUrl = (theme: string, channel: string) => { + return `/styles/${theme.toLowerCase()}-${channel.toLowerCase()}.css`; +}; +const possibleStylesheets = THEMES.flatMap(theme => { + return CHANNELS.map(channel => getStylesheetUrl(theme, channel)); +}); + +/* + * Backgrounds + */ +const backgroundClasses: { [key in (typeof MODES)[number]]: string } = { + Light: 'bg-white', + Dark: 'bg-dark', +}; +const getBackgroundClass = (mode: string) => { + return mode in backgroundClasses ? backgroundClasses[mode] : ''; +}; +const possibleBackgrounds = MODES.map(mode => getBackgroundClass(mode)); + +/* + * Local storage access + */ const STORAGE_KEY_PREFIX = 'swisspost-documentation'; -const THEMES = ['Post']; -const CHANNELS = ['External', 'Internal']; -const MODES = ['Light', 'Dark']; +const store = (key: string, value: string) => { + return localStorage.setItem(`${STORAGE_KEY_PREFIX}-${key}`, value); +}; +const stored = (key: string): string => { + return localStorage.getItem(`${STORAGE_KEY_PREFIX}-${key}`); +}; + +/* + * Helpers + */ +const debounce = (callback: (...args: T) => void, timeout: number) => { + let timer; + return (...args: T) => { + clearTimeout(timer); + timer = setTimeout(() => { + callback(...args); + }, timeout); + }; +}; function StylesSwitcher() { - const [currentTheme, setCurrentTheme] = useState( - localStorage.getItem(`${STORAGE_KEY_PREFIX}-theme`) || THEMES[0], - ); - const [currentChannel, setCurrentChannel] = useState( - localStorage.getItem(`${STORAGE_KEY_PREFIX}-channel`) || CHANNELS[0], - ); - const [currentMode, setCurrentMode] = useState( - localStorage.getItem(`${STORAGE_KEY_PREFIX}-mode`) || MODES[0], - ); + let observer: MutationObserver; - /** - * Sets the 'data-color-mode' attribute and preview stylesheet when the addon initializes - */ - useEffect(() => { - setPreviewStylesheet(); - setDataColorModeAttribute(); - }); + const [currentTheme, setCurrentTheme] = useState(stored('theme') || THEMES[0]); + const [currentChannel, setCurrentChannel] = useState(stored('channel') || CHANNELS[0]); + const [currentMode, setCurrentMode] = useState(stored('mode') || MODES[0]); + + const [preview, setPreview] = useState(); + const [stories, setStories] = useState>(); /** - * Sets the stylesheet matching the selected theme and channel in the preview document head + * Retrieves the preview document after the first rendering */ - const setPreviewStylesheet = () => { - const preview = getPreviewDocument(); - const previewHead = preview && preview.querySelector('head'); + useEffect(() => { + const previewIFrame = document.querySelector('iframe#storybook-preview-iframe'); - if (!previewHead) return; + if (!previewIFrame) return; - let stylesheetLink = previewHead.querySelector(`#${STYLESHEET_ID}`); + previewIFrame.addEventListener('load', () => { + setPreview((previewIFrame as HTMLIFrameElement).contentWindow.document); + }); + }, []); - if (!stylesheetLink) { - stylesheetLink = document.createElement('link'); - stylesheetLink.setAttribute('rel', 'stylesheet'); - stylesheetLink.setAttribute('id', STYLESHEET_ID); - previewHead.appendChild(stylesheetLink); - } + /** + * Retrieves all the stories when the preview content changes + */ + useEffect(() => { + if (!preview || observer) return; - stylesheetLink.setAttribute( - 'href', - `/styles/${currentTheme.toLowerCase()}-${currentChannel.toLowerCase()}.css`, + observer = new MutationObserver( + debounce(() => { + setStories(preview.querySelectorAll('.sbdocs-preview, .sb-main-padded')); + }, 200), ); - }; + + observer.observe(preview.body, { childList: true, subtree: true }); + }, [preview]); /** - * Sets the 'data-color-mode' attribute of the preview body to match the selected mode + * Sets the expected stylesheet in the preview head when the theme or channel changes */ - const setDataColorModeAttribute = () => { - const preview = getPreviewDocument(); + useEffect(() => { if (!preview) return; - const mode = currentMode.toLowerCase(); - const storyContainers = preview.querySelectorAll('.sbdocs-preview, .sb-main-padded'); - storyContainers.forEach(storyContainer => { - storyContainer.classList.remove('bg-light', 'bg-dark'); - storyContainer.classList.add(`bg-${mode}`); - storyContainer.setAttribute('data-color-mode', mode); + possibleStylesheets.forEach(stylesheet => { + const stylesheetLink = preview.head.querySelector(`link[href="${stylesheet}"]`); + if (stylesheetLink) stylesheetLink.remove(); }); - }; + + preview.head.insertAdjacentHTML( + 'beforeend', + ``, + ); + }, [preview, currentTheme, currentChannel]); /** - * Returns the Document contained in the preview iframe + * Sets the expected 'data-color-mode' attribute on all story containers when the mode changes */ - const getPreviewDocument = (): Document | undefined => { - const preview = document.querySelector('#storybook-preview-iframe'); - return preview && (preview as HTMLIFrameElement).contentWindow.document; - }; + useEffect(() => { + if (!stories) return; + + stories.forEach(story => { + story.classList.remove(...possibleBackgrounds); + story.classList.add(getBackgroundClass(currentMode)); + story.setAttribute('data-color-mode', currentMode.toLowerCase()); + }); + }, [stories, currentMode]); /** * Applies selected theme and registers it to the local storage */ const applyTheme = (theme: string) => { + store('theme', theme); setCurrentTheme(theme); - localStorage.setItem(`${STORAGE_KEY_PREFIX}-theme`, theme); }; /** * Applies selected channel and registers it to the local storage */ const applyChannel = (channel: string) => { + store('channel', channel); setCurrentChannel(channel); - localStorage.setItem(`${STORAGE_KEY_PREFIX}-channel`, channel); }; /** * Applies selected mode and registers it to the local storage */ const applyMode = (mode: string) => { + store('mode', mode); setCurrentMode(mode); - localStorage.setItem(`${STORAGE_KEY_PREFIX}-mode`, mode); }; return ( diff --git a/packages/documentation/.storybook/preview-head.html b/packages/documentation/.storybook/preview-head.html index 6b39f036b6..de5cd42496 100644 --- a/packages/documentation/.storybook/preview-head.html +++ b/packages/documentation/.storybook/preview-head.html @@ -8,6 +8,7 @@ + diff --git a/packages/documentation/.storybook/styles/preview.scss b/packages/documentation/.storybook/styles/preview.scss index e8e8a9784f..b768d3e6ea 100644 --- a/packages/documentation/.storybook/styles/preview.scss +++ b/packages/documentation/.storybook/styles/preview.scss @@ -177,6 +177,10 @@ overflow-x: hidden; } +.sbdocs-preview:not([data-color-mode]) { + display: none; +} + .fake-content { position: relative; min-height: calc(1.6rem * 8); diff --git a/packages/styles/src/components/card.scss b/packages/styles/src/components/card.scss index 35172d8052..af1f6907e2 100644 --- a/packages/styles/src/components/card.scss +++ b/packages/styles/src/components/card.scss @@ -24,6 +24,7 @@ // Outline with card-group margin trick prevents double/jumping (on hover) borders outline: 1px solid card.$card-border-color; box-shadow: commons.$box-shadow-lg; + color: card.$card-color; &.product-card, &.card-button { diff --git a/packages/styles/src/variables/components/_card.scss b/packages/styles/src/variables/components/_card.scss index f6d8f96729..e8836a87ab 100644 --- a/packages/styles/src/variables/components/_card.scss +++ b/packages/styles/src/variables/components/_card.scss @@ -24,6 +24,7 @@ $card-inner-border-radius: calc( ) !default; // Design System $card-cap-bg: color.$light !default; $card-bg: color.$white !default; +$card-color: color.$black !default; $card-img-overlay-padding: $card-spacer-x !default; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90a6686f28..82c233d84b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -763,7 +763,7 @@ importers: version: 14.0.2 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.14.14)(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4)) + version: 29.7.0(@types/node@20.14.14) jest-environment-jsdom: specifier: 29.7.0 version: 29.7.0 @@ -1072,7 +1072,7 @@ importers: version: 5.1.0 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.14.14)(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4)) + version: 29.7.0(@types/node@20.14.14) postcss: specifier: 8.4.40 version: 8.4.40 @@ -11127,7 +11127,7 @@ snapshots: tslib: 2.6.3 typescript: 5.5.4 undici: 6.19.2 - vite: 5.3.2(@types/node@20.12.7)(less@4.2.0)(sass@1.77.6)(terser@5.29.2) + vite: 5.3.2(@types/node@20.12.7)(less@4.2.0)(sass@1.78.0)(terser@5.29.2) watchpack: 2.4.1 webpack: 5.92.1(esbuild@0.21.5) webpack-dev-middleware: 7.2.1(webpack@5.92.1(esbuild@0.21.5)) @@ -15553,7 +15553,7 @@ snapshots: '@vitejs/plugin-basic-ssl@1.1.0(vite@5.3.2(@types/node@20.12.7)(less@4.2.0)(sass@1.77.6)(terser@5.29.2))': dependencies: - vite: 5.3.2(@types/node@20.12.7)(less@4.2.0)(sass@1.77.6)(terser@5.29.2) + vite: 5.3.2(@types/node@20.12.7)(less@4.2.0)(sass@1.78.0)(terser@5.29.2) '@vitejs/plugin-basic-ssl@1.1.0(vite@5.3.2(@types/node@20.14.14)(less@4.2.0)(sass@1.77.6)(terser@5.29.2))': dependencies: @@ -19281,6 +19281,25 @@ snapshots: - ts-node optional: true + jest-cli@29.7.0(@types/node@20.14.14): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.14.14)(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.14.14)(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@20.14.14)(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4)) @@ -19649,7 +19668,7 @@ snapshots: '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.14.14)(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4)) + jest-cli: 29.7.0(@types/node@20.14.14) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -23320,7 +23339,7 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.3.10(esbuild@0.21.5)(webpack@5.92.1): + terser-webpack-plugin@5.3.10(esbuild@0.21.5)(webpack@5.92.1(esbuild@0.21.5)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 @@ -23962,6 +23981,18 @@ snapshots: sass: 1.77.6 terser: 5.29.2 + vite@5.3.2(@types/node@20.12.7)(less@4.2.0)(sass@1.78.0)(terser@5.29.2): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.40 + rollup: 4.18.1 + optionalDependencies: + '@types/node': 20.12.7 + fsevents: 2.3.3 + less: 4.2.0 + sass: 1.78.0 + terser: 5.29.2 + vite@5.3.2(@types/node@20.14.14)(less@4.2.0)(sass@1.77.6)(terser@5.29.2): dependencies: esbuild: 0.21.5 @@ -24125,7 +24156,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(esbuild@0.21.5)(webpack@5.92.1) + terser-webpack-plugin: 5.3.10(esbuild@0.21.5)(webpack@5.92.1(esbuild@0.21.5)) watchpack: 2.4.1 webpack-sources: 3.2.3 transitivePeerDependencies: