From 896c9d329ae3206d95aa7c47ad8c4481a8e9961e Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Thu, 27 Jun 2024 16:41:17 -0700 Subject: [PATCH] refactor(web-components): use adopted style sheets to set theme tokens (#31713) Co-authored-by: Chris Holt <13071055+chrisdholt@users.noreply.github.com> --- ...-22e3e36e-c30c-4866-85ad-9e28421977d6.json | 7 +++ packages/web-components/.eslintrc.json | 6 ++ packages/web-components/docs/api-report.md | 4 ++ packages/web-components/package.json | 2 +- .../src/theme/set-theme.bench.ts | 30 ++++++++++ .../src/theme/set-theme.stories.ts | 16 ++++++ .../web-components/src/theme/set-theme.ts | 56 ++++++++++++------- yarn.lock | 8 +-- 8 files changed, 105 insertions(+), 24 deletions(-) create mode 100644 change/@fluentui-web-components-22e3e36e-c30c-4866-85ad-9e28421977d6.json create mode 100644 packages/web-components/src/theme/set-theme.bench.ts create mode 100644 packages/web-components/src/theme/set-theme.stories.ts diff --git a/change/@fluentui-web-components-22e3e36e-c30c-4866-85ad-9e28421977d6.json b/change/@fluentui-web-components-22e3e36e-c30c-4866-85ad-9e28421977d6.json new file mode 100644 index 0000000000000..a13aba16cffb7 --- /dev/null +++ b/change/@fluentui-web-components-22e3e36e-c30c-4866-85ad-9e28421977d6.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "refactor setTheme() to use adoptedStyleSheets", + "packageName": "@fluentui/web-components", + "email": "machi@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/.eslintrc.json b/packages/web-components/.eslintrc.json index 0b1511be1817a..5fd2fd28e79aa 100644 --- a/packages/web-components/.eslintrc.json +++ b/packages/web-components/.eslintrc.json @@ -15,6 +15,12 @@ } }, "rules": { + "no-empty": [ + "error", + { + "allowEmptyCatch": true + } + ], "no-extra-boolean-cast": "off", "no-prototype-builtins": "off", "no-fallthrough": "off", diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index a70ded09f57b8..bbeaa020f1259 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -4,6 +4,8 @@ ```ts +/// + import type { Constructable } from '@microsoft/fast-element'; import { CSSDirective } from '@microsoft/fast-element'; import { Direction } from '@microsoft/fast-web-utilities'; @@ -3152,6 +3154,8 @@ class Text_2 extends FASTElement { block: boolean; // (undocumented) connectedCallback(): void; + // (undocumented) + disconnectedCallback(): void; // @internal elementInternals: ElementInternals; font?: TextFont; diff --git a/packages/web-components/package.json b/packages/web-components/package.json index a39ef4039725c..b594145bc865e 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -230,9 +230,9 @@ }, "devDependencies": { "@microsoft/fast-element": "2.0.0-beta.26", + "@tensile-perf/web-components": "~0.2.0", "@types/web": "^0.0.142", "@storybook/html": "6.5.15", - "@tensile-perf/web-components": "~0.1.15", "chromedriver": "^125.0.0" }, "dependencies": { diff --git a/packages/web-components/src/theme/set-theme.bench.ts b/packages/web-components/src/theme/set-theme.bench.ts new file mode 100644 index 0000000000000..9edcc959fc33b --- /dev/null +++ b/packages/web-components/src/theme/set-theme.bench.ts @@ -0,0 +1,30 @@ +import { measurePerformance, type TestRenderFunction } from '@tensile-perf/web-components'; +import { teamsDarkTheme, teamsLightTheme, webDarkTheme, webLightTheme } from '@fluentui/tokens'; + +import { setTheme } from './set-theme.js'; + +const tests: Record = { + mount: ({ onComplete }) => { + const { startMeasure, endMeasure } = measurePerformance(); + + startMeasure(); + + // Newly set themes + setTheme(webLightTheme); + setTheme(webDarkTheme); + setTheme(teamsDarkTheme); + setTheme(teamsLightTheme); + + // Cached themes + setTheme(webLightTheme); + setTheme(webDarkTheme); + setTheme(teamsDarkTheme); + setTheme(teamsLightTheme); + + endMeasure(); + + onComplete(); + }, +}; + +export { tests }; diff --git a/packages/web-components/src/theme/set-theme.stories.ts b/packages/web-components/src/theme/set-theme.stories.ts new file mode 100644 index 0000000000000..2ce35466847d2 --- /dev/null +++ b/packages/web-components/src/theme/set-theme.stories.ts @@ -0,0 +1,16 @@ +import { html } from '@microsoft/fast-element'; +import { teamsDarkTheme, teamsLightTheme, webDarkTheme, webLightTheme } from '@fluentui/tokens'; + +import { renderComponent } from '../helpers.stories.js'; +import { setTheme } from './set-theme.js'; + +export default { + title: 'Theme/SetTheme', +}; + +export const SetTheme = renderComponent(html` + webLightTheme + webDarkTheme + teamsLightTheme + teamsDarkTheme +`); diff --git a/packages/web-components/src/theme/set-theme.ts b/packages/web-components/src/theme/set-theme.ts index bbd2c5d969dc6..252975dd5cf5a 100644 --- a/packages/web-components/src/theme/set-theme.ts +++ b/packages/web-components/src/theme/set-theme.ts @@ -3,34 +3,52 @@ import * as tokens from './design-tokens.js'; const tokenNames = Object.keys(tokens) as (keyof Theme)[]; +const SUPPORTS_REGISTER_PROPERTY = 'registerProperty' in CSS; +const SUPPORTS_ADOPTED_STYLE_SHEETS = 'adoptedStyleSheets' in document; +const themeStyleSheet = new CSSStyleSheet(); +const themeStyleTextMap = new Map(); + /** * Sets the theme tokens on defaultNode. * @param theme - Flat object of theme token values. * @internal */ export const setTheme = (theme: Theme) => { - for (const t of tokenNames) { - let registered = false; - - if ('registerProperty' in CSS) { - try { - CSS.registerProperty({ - name: `--${t}`, - inherits: true, - initialValue: theme[t] as string, - }); - registered = true; - } catch { - // Do nothing. + // Fallback to setting token custom properties on `` element’s `style` + // attribute, only checking the support of `document.adoptedStyleSheets` + // here because it has broader support than `CSS.registerProperty()`, which + // is checked later. + if (!SUPPORTS_ADOPTED_STYLE_SHEETS) { + setThemeFor(document.documentElement, theme); + return; + } + + if (!themeStyleTextMap.has(theme)) { + const tokenDeclarations: string[] = []; + + for (const t of tokenNames) { + if (SUPPORTS_REGISTER_PROPERTY) { + try { + CSS.registerProperty({ + name: `--${t}`, + inherits: true, + initialValue: theme[t] as string, + }); + } catch {} } + tokenDeclarations.push(`--${t}: ${theme[t] as string};`); } - if (!registered) { - // TODO: Find a better way to update the values. Current approach adds - // lots of code to the `style` attribute on ``. Maybe look into - // `document.adoptedStyleSheets`. - setThemeFor(document.body, theme); - } + themeStyleTextMap.set(theme, `html{${tokenDeclarations.join('')}}`); + } + + if (!document.adoptedStyleSheets.includes(themeStyleSheet)) { + document.adoptedStyleSheets.push(themeStyleSheet); + } else { + // The very first call to `setTheme()` within a document doesn’t need to + // call `replaceSync()`, because `CSS.registerProperty()` above is + // sufficient to set the tokens. + themeStyleSheet.replaceSync(themeStyleTextMap.get(theme)!); } }; diff --git a/yarn.lock b/yarn.lock index 5ce6a679a24ae..2e2f8468acea7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4640,10 +4640,10 @@ dependencies: "@tensile-perf/tools" "0.1.7" -"@tensile-perf/web-components@~0.1.15": - version "0.1.15" - resolved "https://registry.yarnpkg.com/@tensile-perf/web-components/-/web-components-0.1.15.tgz#95587b220d0b09b79b339da4e4f8694772fab91a" - integrity sha512-gawc20t7uvZyWUZOqBCA6/2Z7wEGECy2ytxe2/FMplDOuUj/CLXKoDY9mtS1dlo+6HjERuL8iP4IjdqURXo2UA== +"@tensile-perf/web-components@~0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@tensile-perf/web-components/-/web-components-0.2.0.tgz#355ac463a121656e68c0567f9c15accf19b61734" + integrity sha512-NEKH6d/2HrOJgW60E/oZ/O2D4vjzBv9xgSQmAz1cQnrEyQ/Odz/l3OWDwfEobcg8VHm6iODVoNAyvYAr4ESTKQ== dependencies: "@tensile-perf/runner" "0.4.0" "@tensile-perf/tools" "0.1.7"