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);