diff --git a/README.md b/README.md index 5ec18bf..faae14c 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ What this plugin does is: - Collect all `eufemia-theme` files (`{scss,css}`) – also check if they are located in `/src` or needs to be collected from `/build`. Both are used by the Eufemia repo/portal. - After we have collected all available theme files, we create or update a static import `load-eufemia-styles.js`, which is git-ignored. - Split theme styles into separate CSS files (Webpack chunks) inside `gatsby-node.js` -- Inserts some JavaScript in the HTML head in order to handle what theme file should be shown (`inlineScriptProd` and `inlineScriptDev`) +- Inserts some JavaScript in the HTML head in order to handle what theme file should be shown (`inlineScript` and `inlineScriptDev`) - Load these inline scripts via Webpack inline module loaders: `!raw-loader!terser-loader!` - By using localStorage, we block the HTML rendering, this way we do avoid flickering of a default theme @@ -147,6 +147,10 @@ During dev, we do not get any inline styles from Gatsby – they are handled by - Use `uniqueId` to reload css files as there is not unique build hash, unlike we get during production - Use `MutationObserver` to reload the current theme file, because Webpack uses hot module replacement, so we need to reload as well +### Sorting order + +The order of the extracted styles can influence CSS specificity. Therefore, the extracted theme styles (`/ui.css`) should always be placed below the `/commons.css`. + ## Releases Releases are made with [semantic-release](https://github.com/semantic-release/semantic-release). diff --git a/examples/shared/ChangeStyleTheme.tsx b/examples/shared/ChangeStyleTheme.tsx index c287966..6e98b6b 100644 --- a/examples/shared/ChangeStyleTheme.tsx +++ b/examples/shared/ChangeStyleTheme.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Dropdown, Switch } from '@dnb/eufemia' +import { Dropdown } from '@dnb/eufemia' import { Context } from '@dnb/eufemia/shared' import { getThemes, diff --git a/examples/ui-theme-example/src/pages/index.tsx b/examples/ui-theme-example/src/pages/index.tsx index b08ec27..72eb60b 100644 --- a/examples/ui-theme-example/src/pages/index.tsx +++ b/examples/ui-theme-example/src/pages/index.tsx @@ -3,6 +3,7 @@ import { Anchor, Button } from '@dnb/eufemia' import { Layout } from '@dnb/eufemia/extensions/forms' import ChangeStyleTheme from '../../../shared/ChangeStyleTheme' import { Link } from 'gatsby' +import './style.css' const App = () => { return ( diff --git a/examples/ui-theme-example/src/pages/style.css b/examples/ui-theme-example/src/pages/style.css new file mode 100644 index 0000000..5b6976f --- /dev/null +++ b/examples/ui-theme-example/src/pages/style.css @@ -0,0 +1,3 @@ +body { + background: white; +} diff --git a/package.json b/package.json index fbb7fb9..7ff797d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ ], "scripts": { "test": "yarn workspace e2e test", - "watch": "yarn workspace gatsby-plugin-eufemia-theme-handler watch", + "watch": "yarn workspace gatsby-plugin-eufemia-theme-handler build:watch", "watch:all": "yarn watch & yarn workspace sb-theme-example watch & yarn workspace ui-theme-example watch & yarn serve", "start": "yarn watch & yarn workspace sb-theme-example start & yarn workspace ui-theme-example start", "build": "yarn workspace gatsby-plugin-eufemia-theme-handler build && yarn workspace ui-theme-example build && yarn workspace ui-theme-example serve & yarn workspace sb-theme-example build", diff --git a/packages/e2e/tests/change-theme.spec.ts b/packages/e2e/tests/change-theme.spec.ts index 4be0cb0..072312e 100644 --- a/packages/e2e/tests/change-theme.spec.ts +++ b/packages/e2e/tests/change-theme.spec.ts @@ -74,20 +74,22 @@ test.describe('change theme', () => { expect(uiStyleElementExists).toBeNull() }) - test('should load css file after template', async ({ page }) => { + test('should place css file after commons.css', async ({ page }) => { await page.click('#change-theme') await page.click('#change-theme-portal ul li:nth-child(2)') + const sbanken = 'link[href^="/sbanken."][rel="stylesheet"]' const sbankenCssAfterTemplateExists = await page.$( - '#eufemia-style-theme + link[href^="/sbanken."][rel="stylesheet"]' + `style[data-href^="/commons."] ~ ${sbanken}, link[href^="/commons."] ~ ${sbanken}` ) expect(sbankenCssAfterTemplateExists).toBeTruthy() await page.click('#change-theme') await page.click('#change-theme-portal ul li:first-child') + const ui = 'link[href^="/ui."][rel="stylesheet"]' const uiCssAfterTemplateExists = await page.$( - '#eufemia-style-theme + link[href^="/ui."][rel="stylesheet"]' + `style[data-href^="/commons."] ~ ${ui}, link[href^="/commons."] ~ ${ui}` ) expect(uiCssAfterTemplateExists).toBeTruthy() }) diff --git a/packages/gatsby-plugin-eufemia-theme-handler/package.json b/packages/gatsby-plugin-eufemia-theme-handler/package.json index 70f9110..4f9d42a 100644 --- a/packages/gatsby-plugin-eufemia-theme-handler/package.json +++ b/packages/gatsby-plugin-eufemia-theme-handler/package.json @@ -31,7 +31,7 @@ "./themeHandler.js": "./themeHandler.js", "./themeHandler.d.ts": "./themeHandler.d.ts", "./inlineScriptDev.js": "./inlineScriptDev.js", - "./inlineScriptProd.js": "./inlineScriptProd.js", + "./inlineScript.js": "./inlineScript.js", "./gatsby-node.js": "./gatsby-node.js", "./gatsby-browser.js": "./gatsby-browser.js", "./gatsby-ssr.js": "./gatsby-ssr.js", @@ -44,8 +44,8 @@ "build": "yarn build:cmd && GATSBY_FILES=true yarn build:cmd && yarn build:types", "build:cmd": "yarn babel src --extensions .js,.ts,.tsx,.mjs --out-dir .", "build:types": "tsc --project tsconfig.json", + "build:watch": "yarn build:cmd --watch", "clean": "rm !(src|babel.config.js|.gitignore|.npmignore|LICENSE|.env|*.md|*.json)", - "watch": "yarn build --watch", "test:types": "tsc --noEmit" }, "dependencies": { diff --git a/packages/gatsby-plugin-eufemia-theme-handler/src/inlineScriptProd.mjs b/packages/gatsby-plugin-eufemia-theme-handler/src/inlineScript.mjs similarity index 77% rename from packages/gatsby-plugin-eufemia-theme-handler/src/inlineScriptProd.mjs rename to packages/gatsby-plugin-eufemia-theme-handler/src/inlineScript.mjs index 4f2925d..3da6a1c 100644 --- a/packages/gatsby-plugin-eufemia-theme-handler/src/inlineScriptProd.mjs +++ b/packages/gatsby-plugin-eufemia-theme-handler/src/inlineScript.mjs @@ -5,17 +5,18 @@ if (typeof window !== 'undefined') { callback = null ) => { try { + const headElement = document.head const styleElement = document.getElementById('eufemia-style-theme') - const headElement = document.querySelector('html head') const themes = globalThis.availableThemes const theme = themes[themeName] - const href = theme.file + (reload ? '?' + Date.now() : '') if (!theme) { console.error('No theme found:', themeName) return // stop here } + const href = theme.file + (reload ? '?' + Date.now() : '') + /** * To avoid flicker during change */ @@ -70,6 +71,8 @@ if (typeof window !== 'undefined') { if (previousElem) { headElement.removeChild(previousElem) } + + reorderStyles() } catch (e) { console.error(e) } @@ -107,5 +110,34 @@ if (typeof window !== 'undefined') { } const themeName = globalThis.__getEufemiaThemeName() - globalThis.__updateEufemiaThemeFile(themeName) + const isDefaultTheme = themeName === globalThis.defaultTheme + if (!isDefaultTheme) { + globalThis.__updateEufemiaThemeFile(themeName) + } +} + +function reorderStyles() { + const commonsElement = getCommonsElement() + if (commonsElement) { + moveElementBelow( + document.getElementById('current-theme'), + commonsElement + ) + moveElementBelow( + document.getElementById('eufemia-style-theme'), + commonsElement + ) + } +} + +function getCommonsElement() { + const elements = Array.from(document.head.querySelectorAll('link[href]')) + return elements.find(({ href }) => { + return href.includes('commons') + }) +} + +function moveElementBelow(elementToMove, targetElement) { + const parentElement = targetElement.parentNode + parentElement.insertBefore(elementToMove, targetElement.nextSibling) } diff --git a/packages/gatsby-plugin-eufemia-theme-handler/src/inlineScriptDev.mjs b/packages/gatsby-plugin-eufemia-theme-handler/src/inlineScriptDev.mjs index 7d8bf6d..194b05b 100644 --- a/packages/gatsby-plugin-eufemia-theme-handler/src/inlineScriptDev.mjs +++ b/packages/gatsby-plugin-eufemia-theme-handler/src/inlineScriptDev.mjs @@ -1,23 +1,34 @@ if (typeof window !== 'undefined') { - try { - const headElement = document.querySelector('html head') - const logMutations = (mutations) => { - for (const mutation of mutations) { - const element = mutation.nextSibling - if ( - element && - (element.src || element.href || '').includes('/commons.') - ) { - const themeName = globalThis.__getEufemiaThemeName() - globalThis.__updateEufemiaThemeFile(themeName, true) - break - } + if (!window.__hasEufemiaObserver) { + window.__hasEufemiaObserver = true + onElementInsertion('[href="/commons.css"]', () => { + const themeName = globalThis.__getEufemiaThemeName() + globalThis.__updateEufemiaThemeFile(themeName, true) + }) + } +} + +function onElementInsertion(targetSelector, callback) { + const headElement = document.head + + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + const addedNodes = Array.from(mutation.addedNodes) + + const targetElementAdded = addedNodes.find((node) => { + return ( + node.nodeType === Node.ELEMENT_NODE && + node.matches(targetSelector) + ) + }) + + if (targetElementAdded) { + callback(targetElementAdded, observer) } - } + }) + }) - const observer = new MutationObserver(logMutations) - observer.observe(headElement, { childList: true }) - } catch (e) { - console.error(e) - } + observer.observe(headElement, { + childList: true, + }) } diff --git a/packages/gatsby-plugin-eufemia-theme-handler/src/themeHandler.tsx b/packages/gatsby-plugin-eufemia-theme-handler/src/themeHandler.tsx index e976483..eca26f5 100644 --- a/packages/gatsby-plugin-eufemia-theme-handler/src/themeHandler.tsx +++ b/packages/gatsby-plugin-eufemia-theme-handler/src/themeHandler.tsx @@ -1,8 +1,8 @@ import React from 'react' import { Theme } from '@dnb/eufemia/shared' -import inlineScriptProd from '!raw-loader!terser-loader!./inlineScriptProd' +import inlineScript from '!raw-loader!terser-loader!./inlineScript' import inlineScriptDev from '!raw-loader!terser-loader!./inlineScriptDev' -import EventEmitter from './EventEmitter.js' +import EventEmitter from './EventEmitter' import type { ThemeNames, ThemeProps } from '@dnb/eufemia/shared/Theme' @@ -109,9 +109,9 @@ export const onPreRenderHTML = ( // Make themes to not be embedded, but rather load as css files if (!isDev) { let defaultElement - for (const element of headComponents) { - const href = element.props['data-href'] - if (href && href.includes('.css')) { + for (const item of headComponents) { + const href = item?.props?.['data-href'] + if (href?.includes('.css')) { if ( availableThemesArray.some((name) => { return href.includes(`/${name}.`) @@ -129,14 +129,14 @@ export const onPreRenderHTML = ( pluginOptions?.inlineDefaultTheme && themeName === defaultTheme ) { - defaultElement = element - headComponents[element] = null + defaultElement = item + headComponents[item] = null } else { // Remove the inline style // but not when its the default theme - delete element.props['data-href'] - delete element.props['data-identity'] - delete element.props.dangerouslySetInnerHTML + delete item.props['data-href'] + delete item.props['data-identity'] + delete item.props.dangerouslySetInnerHTML } } } @@ -206,7 +206,7 @@ export const onPreRenderHTML = (