integration/styles: better style scopingwhen using embedded dsfr

- the CSS that included the DSFR excerpt we needed styled some base html
tags directly. It might collide with a service CSS already there. Now a
postcss plugin applies a CSS prefix to everything
- we output in the lib two DSFR file versions: one with prefixed
selectors, one raw.
This commit is contained in:
Emmanuel Pelletier
2024-05-07 17:17:18 +02:00
parent 3c2a6fd0cf
commit 2a12f312c2
13 changed files with 133 additions and 35 deletions

View File

@@ -20,6 +20,7 @@
"import-single-ts": "^1.0.3", "import-single-ts": "^1.0.3",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-prefix-selector": "^1.16.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -3217,6 +3218,15 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postcss-prefix-selector": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-1.16.1.tgz",
"integrity": "sha512-Umxu+FvKMwlY6TyDzGFoSUnzW+NOfMBLyC1tAkIjgX+Z/qGspJeRjVC903D7mx7TuBpJlwti2ibXtWuA7fKMeQ==",
"dev": true,
"peerDependencies": {
"postcss": ">4 <9"
}
},
"node_modules/postcss-selector-parser": { "node_modules/postcss-selector-parser": {
"version": "6.0.16", "version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",

View File

@@ -50,6 +50,7 @@
"import-single-ts": "^1.0.3", "import-single-ts": "^1.0.3",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-prefix-selector": "^1.16.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@@ -1,4 +1,5 @@
import purgecss from "@fullhuman/postcss-purgecss" import purgecss from "@fullhuman/postcss-purgecss"
import prefixSelector from "postcss-prefix-selector"
import autoprefixer from "autoprefixer" import autoprefixer from "autoprefixer"
export default { export default {
@@ -9,5 +10,20 @@ export default {
variables: true, variables: true,
}), }),
autoprefixer(), autoprefixer(),
prefixSelector({
prefix: ":where(.lasuite)",
transform: function (prefix, selector, prefixedSelector, filePath, rule) {
if (filePath.includes("dev.css") || filePath.includes("raw-dsfr.css")) {
return selector
}
if (selector.includes(".lasuite") || selector === "html" || selector === ":root") {
return selector
}
if (selector === "body") {
return `.lasuite`
}
return prefixedSelector
},
}),
], ],
} }

View File

@@ -0,0 +1,20 @@
import { ReactNode, useEffect } from "react"
import fullStyles from "../styles/homepage-full.css?inline"
import devStyles from "../styles/dev.css?inline"
export const StylesFull = ({ children }: { children: ReactNode }) => {
useEffect(() => {
if (document.querySelector("#styles-full")) {
return
}
const style = document.createElement("style")
style.id = "styles-full"
style.innerHTML = fullStyles + devStyles
document.head.appendChild(style)
return () => {
document.querySelector("#styles-full")?.remove()
}
}, [])
return children
}

View File

@@ -0,0 +1,21 @@
import { ReactNode, useEffect } from "react"
import dsfrStyles from "../styles/raw-dsfr.css?inline"
import homepageStyles from "../styles/homepage-gaufre.css?inline"
import devStyles from "../styles/dev.css?inline"
export const StylesStandalone = ({ children }: { children: ReactNode }) => {
useEffect(() => {
if (document.querySelector("#styles-standalone")) {
return
}
const style = document.createElement("style")
style.id = "styles-standalone"
style.innerHTML = dsfrStyles + homepageStyles + devStyles
document.head.appendChild(style)
return () => {
document.querySelector("#styles-standalone")?.remove()
}
}, [])
return children
}

View File

@@ -8,14 +8,19 @@ import { Gaufre } from "./components/Gaufre"
import { EmailOrProconnect } from "./components/Homepage/EmailOrProconnect" import { EmailOrProconnect } from "./components/Homepage/EmailOrProconnect"
import { Email } from "./components/Homepage/Email" import { Email } from "./components/Homepage/Email"
import { Proconnect } from "./components/Homepage/Proconnect" import { Proconnect } from "./components/Homepage/Proconnect"
import { StylesFull } from "./components/StylesFull"
import { StylesStandalone } from "./components/StylesStandalone"
import services from "../../../website/src/data/services.json" import services from "../../../website/src/data/services.json"
import "./styles/dsfr.css" const serviceHomepage = ({
import "./styles/homepage.css" id,
import "./styles/gaufre.css" content,
import "./styles/dev.css" styles = "standalone",
}: {
const serviceHomepage = ({ id, content }: { content?: ReactNode; id: string }) => { content?: ReactNode
id: string
styles: "standalone" | "full"
}) => {
const service = services.find(({ id: itemId }) => itemId === id) const service = services.find(({ id: itemId }) => itemId === id)
if (!service) { if (!service) {
console.log(`Service ${id} not found, exiting`) console.log(`Service ${id} not found, exiting`)
@@ -23,29 +28,33 @@ const serviceHomepage = ({ id, content }: { content?: ReactNode; id: string }) =
} }
const { name, tagline } = service const { name, tagline } = service
const StylesProvider = styles === "standalone" ? StylesStandalone : StylesFull
return { return {
path: `/homepage-template-${id}`, path: `/homepage-template-${id}`,
label: `Homepage ${name}`, label: `Homepage ${name}`,
component: ( component: (
<Homepage <StylesProvider>
lasuiteApiUrl={import.meta.env.VITE_LASUITE_API_URL} <Homepage
entity="Gouvernement" lasuiteApiUrl={import.meta.env.VITE_LASUITE_API_URL}
tagline={tagline} entity="Gouvernement"
serviceName={name} tagline={tagline}
serviceId={id} serviceName={`${name} - styles ${styles}`}
logo={`/logos/${id}.svg`} serviceId={id}
homepageUrl="/" logo={`/logos/${id}.svg`}
footerOptions={{ homepageUrl="/"
description: "Un service de la Direction interministérielle du numérique", footerOptions={{
sitemapUrl: "/sitemap", description: "Un service de la Direction interministérielle du numérique",
a11yUrl: "/accessibilite", sitemapUrl: "/sitemap",
a11yLevel: "non compliant", a11yUrl: "/accessibilite",
termsUrl: "/mentions-legales", a11yLevel: "non compliant",
privacyUrl: "/donnees-personnelles", termsUrl: "/mentions-legales",
}} privacyUrl: "/donnees-personnelles",
> }}
{content || <Proconnect url="#" />} >
</Homepage> {content || <Proconnect url="#" />}
</Homepage>
</StylesProvider>
), ),
} }
} }
@@ -54,14 +63,17 @@ const routes = [
serviceHomepage({ serviceHomepage({
id: "resana", id: "resana",
content: <EmailOrProconnect proconnectUrl="#" />, content: <EmailOrProconnect proconnectUrl="#" />,
styles: "full",
}), }),
serviceHomepage({ serviceHomepage({
id: "messagerie", id: "messagerie",
content: <Email />, content: <Email />,
styles: "full",
}), }),
serviceHomepage({ serviceHomepage({
id: "tchap", id: "tchap",
content: <Email />, content: <Email />,
styles: "standalone",
}), }),
serviceHomepage({ serviceHomepage({
id: "france-transfert", id: "france-transfert",
@@ -75,9 +87,11 @@ const routes = [
</p> </p>
</div> </div>
), ),
styles: "standalone",
}), }),
serviceHomepage({ serviceHomepage({
id: "equipes", id: "equipes",
styles: "standalone",
}), }),
{ {
path: "/gaufre", path: "/gaufre",

View File

@@ -1,3 +1,4 @@
body { body {
margin: 0; margin: 0;
padding: 0;
} }

View File

@@ -1,3 +1,3 @@
@import "./required-dsfr.css"; @import "./prefixed-dsfr.css";
@import "./homepage.css"; @import "./homepage.css";
@import "./gaufre.css"; @import "./gaufre.css";

View File

@@ -2,6 +2,10 @@
--lasuite-primary: #000091; --lasuite-primary: #000091;
} }
.lasuite {
box-sizing: border-box;
}
@media (min-width: 78em) { @media (min-width: 78em) {
.lasuite-container, .lasuite-container,
.fr-container.lasuite-container { .fr-container.lasuite-container {

View File

@@ -0,0 +1,2 @@
@import "./fonts.css";
@import "./dsfr.css";

View File

@@ -15,7 +15,8 @@ export default defineConfig({
emptyOutDir: false, emptyOutDir: false,
rollupOptions: { rollupOptions: {
input: { input: {
"css-required-dsfr": resolve(__dirname, "src/styles/required-dsfr.css"), "css-prefixed-dsfr": resolve(__dirname, "src/styles/prefixed-dsfr.css"),
"css-raw-dsfr": resolve(__dirname, "src/styles/raw-dsfr.css"),
"css-homepage": resolve(__dirname, "src/styles/homepage.css"), "css-homepage": resolve(__dirname, "src/styles/homepage.css"),
"css-gaufre": resolve(__dirname, "src/styles/gaufre.css"), "css-gaufre": resolve(__dirname, "src/styles/gaufre.css"),
"css-homepage-full": resolve(__dirname, "src/styles/homepage-full.css"), "css-homepage-full": resolve(__dirname, "src/styles/homepage-full.css"),

View File

@@ -64,13 +64,21 @@ vous l'utilisiez déjà ou non dans votre projet, un fichier CSS différent est
</TabItem> </TabItem>
</Tabs> </Tabs>
Si vous voulez finement gérer vos styles car vous utilisez le bouton Gaufre sur tout votre site, vous pouvez choisir parmi tous les fichiers CSS présents : Si vous voulez finement gérer vos styles car vous utilisez le bouton Gaufre sur tout votre site,
vous pouvez choisir parmi tous les fichiers CSS présents :
- `css/homepage-full.css` : contient l'extrait de DSFR nécessaire + les styles de la homepage + les styles de la gaufre, - `css/homepage-full.css` : contient l'extrait de DSFR nécessaire + les styles de la homepage + les
- `css/homepage-gaufre.css` : contient les styles de la homepage + les styles de la gaufre (sans DSFR), styles de la gaufre,
- `css/homepage-gaufre.css` : contient les styles de la homepage + les styles de la gaufre (sans
DSFR),
- `css/homepage.css` : contient les styles de la homepage (sans DSFR ni gaufre), - `css/homepage.css` : contient les styles de la homepage (sans DSFR ni gaufre),
- `css/gaufre.css` : contient les styles de la gaufre (sans DSFR ni homepage), - `css/gaufre.css` : contient les styles de la gaufre (sans DSFR ni homepage),
- `css/required-dsfr.css` : contient l'extrait de DSFR nécessaire (rien d'autre), - `css/prefixed-dsfr.css` : contient l'extrait de DSFR nécessaire (rien d'autre),
Note : l'extrait de DSFR est toujours transformé pour que tous les sélecteurs soient derrière un
sélecteur `:where(.lasuite)`. Ceci permet d'intégrer le DSFR sans risque de conflit avec d'autres
styles, et sans impacter la spécifité du sélecteur. Si vous préférez utiliser un extrait de DSFR non
transformé, utilisez `css/raw-dsfr.css`.
### 2. JS ### 2. JS
@@ -124,10 +132,10 @@ savoir plus sur chaque composant.
##### Traduction ##### Traduction
Si vous utilisez les composants React et que vous avez besoin de traduire votre page d'accueil, Si vous utilisez les composants React et que vous avez besoin de traduire votre page d'accueil, vous
vous pouvez envelopper les composants avec le provider `LaSuiteTranslationsProvider` provenant du pouvez envelopper les composants avec le provider `LaSuiteTranslationsProvider` provenant du paquet,
paquet, et lui passer en props vos `translations`. Les traductions françaises sont exportées en et lui passer en props vos `translations`. Les traductions françaises sont exportées en tant que
tant que `frTranslations`. `frTranslations`.
#### Sans React #### Sans React