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 = (
)
@@ -236,14 +236,20 @@ export const onPreRenderHTML = (
headComponents.forEach((item) => {
const exists = uniqueHeadComponents.some((i) => {
const href = i?.props?.['data-href']
- return href && item?.props?.['data-href'] === href
+ return href && href === item?.props?.['data-href']
})
if (!exists) {
uniqueHeadComponents.push(item)
}
})
- replaceHeadComponents(uniqueHeadComponents)
+ const orderedComponents = changeOrderOfComponents(
+ uniqueHeadComponents,
+ 'props.data-href',
+ ['commons', ...availableThemesArray]
+ )
+
+ replaceHeadComponents(orderedComponents)
}
}
@@ -260,3 +266,70 @@ export function wrapRootElement({ element }, pluginOptions) {
return element
}
+
+function changeOrderOfComponents(
+ arr: Array>,
+ propertyPath: string,
+ order: Array
+) {
+ const copyArr = [...arr]
+
+ const findIndexByMatch = (obj, path, value) => {
+ const keys = path.split('.')
+ let currentObj = obj
+ for (const key of keys) {
+ if (currentObj[key] === undefined) {
+ return -1 // Property doesn't exist in the object
+ }
+ currentObj = currentObj[key]
+ }
+
+ if (Array.isArray(currentObj)) {
+ return currentObj.findIndex((item) => item.includes(value))
+ } else if (typeof currentObj === 'string') {
+ return currentObj.includes(value) ? 0 : -1
+ }
+
+ return -1
+ }
+
+ const validOrder = order.filter((value) =>
+ copyArr.some(
+ (obj) => findIndexByMatch(obj, propertyPath, value) !== -1
+ )
+ )
+
+ // Check if the order is already correct
+ if (
+ validOrder.every(
+ (value, index) =>
+ copyArr[index] &&
+ findIndexByMatch(copyArr[index], propertyPath, value) !== -1
+ )
+ ) {
+ return copyArr
+ }
+
+ const validIndexes = validOrder.map((value) =>
+ copyArr.findIndex(
+ (obj) => findIndexByMatch(obj, propertyPath, value) !== -1
+ )
+ )
+
+ // Check if the order needs to be switched
+ if (
+ validIndexes.length === validOrder.length &&
+ !validIndexes.every((value, index) => value === index)
+ ) {
+ validIndexes.forEach((value, index) => {
+ if (value !== index) {
+ ;[copyArr[index], copyArr[value]] = [
+ copyArr[value],
+ copyArr[index],
+ ]
+ }
+ })
+ }
+
+ return copyArr
+}