Skip to content

Commit 3fd170c

Browse files
committed
feat: add helper functions and docs
Adds helper functions to simplify working with `merge-styles` shadow DOM APIs. Adds documentation explaining basic usage of shadow DOM features.
1 parent 2890dcd commit 3fd170c

8 files changed

Lines changed: 114 additions & 34 deletions

File tree

packages/merge-styles/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,3 +504,76 @@ window.FabricConfig = {
504504
},
505505
};
506506
```
507+
508+
## Shadow DOM
509+
510+
`merge-styles` has experimental support for [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). This feature is opt-in and incrementally adoptable. To enable the feature you need to include two [React Providers](https://react.dev/reference/react/createContext#provider):
511+
512+
1. `MergeStylesRootProvider`: acts as the "global" context for your application. You should have one of these per page.
513+
2. `MergeStylesShadowRootProvider`: a context for each shadow root in your application. You should have one of these per shadow root.
514+
515+
`merge-styles` does not provide an option for creating shadow roots in React as how you get a shadow root doesn't matter, just that you have a reference to one. [`react-shadow`](https://www.npmjs.com/package/react-shadow) is one library that can create shadow roots in React and will be used in examples.
516+
517+
### Shadow DOM example
518+
519+
```tsx
520+
import { PrimaryButton } from '@fluentui/react';
521+
import { MergeStylesRootProvider, MergeStylesShadowRootProvider } from '@fluentui/utilities';
522+
import root from 'react-shadow';
523+
524+
const ShadowRoot = ({ children }) => {
525+
// This is a ref but we're using state to manage it so we can force
526+
// a re-render.
527+
const [shadowRootEl, setShadowRootEl] = React.useState<HTMLElement | null>(null);
528+
529+
return (
530+
<MergeStylesRootProvider>
531+
<root.div className="shadow-root" delegatesFocus ref={setShadowRootEl}>
532+
<MergeStylesShadowRootProvider shadowRoot={shadowRootEl?.shadowRoot}>{children}</MergeStylesShadowRootProvider>
533+
</root.div>
534+
</MergeStylesRootProvider>
535+
);
536+
};
537+
538+
<ShadowRoot>
539+
<PrimaryButton>I'm in the shadow DOM!</PrimaryButton>
540+
</ShadowRoot>
541+
<PrimaryButton>I'm in the light DOM!</PrimaryButton>
542+
```
543+
544+
### Scoping styles for more efficient CSS
545+
546+
You do not _need_ to update your `merge-styles` styles to support shadow DOM but you can make styles more efficient with some updates.
547+
548+
Shadow DOM support is achieved in `merge-styles` by using [constructable stylesheets](https://web.dev/articles/constructable-stylesheets) and is scoped by "stylesheet keys". `merge-styles` creates one stylesheet per key and in Fluent this means each component has its own stylesheet. Each `MergeStylesShadowRootProvider` will only adopt styles for components it contains plus the global sheet (we cannot be certain whether we need this sheet or not so we always adopt it). This means a `MergeStylesShadowRootProvider` that contains a button will only adopt button styles (plus the global styles) but not checkbox styles, making styling within the shadow root more efficient.
549+
550+
If you use `customizable` or `styled` the existing "scope" value provided to these functions is used a unique key. If no key is provided `merge-styles` falls back to a "global" key. This global key is a catch-all and allows us to support code that was written before shadow DOM support was added or code that is called outside of React context.
551+
552+
All `@fluentui/react` styles are scoped to via `customizable` and `styled` (and some updates to specific component styles where needed). If your components use these functions and you set the "scope" property your components will automatically be scoped.
553+
If you're using `mergeStyles()` (and other `merge-styles` APIs) directly, your styles will be placed in the global scope and still be available in shadow roots, just not as optimally as possible.
554+
555+
#### Style scoping example
556+
557+
```tsx
558+
import { useWindow } from '@fluentui/react';
559+
import { mergeStyles } from '@fluentui/merge-styles';
560+
import { useShadowConfig } from '@fluentui/utilities';
561+
import type { ShadowConfig } from '@fluentui/merge-styles';
562+
563+
// This must be globally unique for the application
564+
const MY_COMPONENT_STYLESHEET_KEY: string = 'my-unique-key';
565+
566+
const MyComponent = props => {
567+
// Make sure multi-window scenarios work (e.g., pop outs)
568+
const win: Window = useWindow();
569+
// Check if the component is rendered in a shadow context
570+
const inShadow: boolean = useHasMergeStylesShadowRootContext();
571+
572+
const shadowConfig: ShadowConfig = useShadowConfig(MY_COMPONENT_STYLESHEET_KEY, win);
573+
574+
const styles = React.useMemo(() => {
575+
// shadowConfig must be the first parameter when it is used
576+
return mergeStyles(shadowConfig, myStyles);
577+
}, [shadowConfig, myStyles]);
578+
};
579+
```

packages/merge-styles/etc/merge-styles.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,9 @@ export interface IStyleSheetConfig {
469469
// @public
470470
export function keyframes(timeline: IKeyframes): string;
471471

472+
// @public (undocumented)
473+
export const makeShadowConfig: (stylesheetKey: string, inShadow: boolean, window?: Window) => ShadowConfig;
474+
472475
// Warning: (ae-forgotten-export) The symbol "IStyleOptions" needs to be exported by the entry point index.d.ts
473476
//
474477
// @public

packages/merge-styles/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export { setRTL } from './StyleOptionsState';
3939

4040
export type { ObjectOnly } from './ObjectOnly';
4141

42-
export { GLOBAL_STYLESHEET_KEY } from './shadowConfig';
42+
export { GLOBAL_STYLESHEET_KEY, makeShadowConfig } from './shadowConfig';
4343
export type { ShadowConfig } from './shadowConfig';
4444

4545
import './version';

packages/merge-styles/src/shadowConfig.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ export const DEFAULT_SHADOW_CONFIG: ShadowConfig = {
1414
__isShadowConfig__: true,
1515
} as const;
1616

17+
export const makeShadowConfig = (stylesheetKey: string, inShadow: boolean, window?: Window): ShadowConfig => {
18+
return {
19+
stylesheetKey,
20+
inShadow,
21+
window,
22+
__isShadowConfig__: true,
23+
};
24+
};
25+
1726
export const isShadowConfig = (obj: unknown): obj is ShadowConfig => {
1827
if (!obj) {
1928
return false;

packages/utilities/src/customizations/customizable.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { Customizations } from './Customizations';
33
import { hoistStatics } from '../hoistStatics';
44
import { CustomizerContext } from './CustomizerContext';
5-
import { concatStyleSets } from '@fluentui/merge-styles';
5+
import { concatStyleSets, makeShadowConfig } from '@fluentui/merge-styles';
66
import type { ICustomizerContext } from './CustomizerContext';
77
import { MergeStylesShadowRootConsumer } from '../shadowDom/MergeStylesShadowRootContext';
88
import type { ShadowConfig } from '@fluentui/merge-styles';
@@ -56,14 +56,8 @@ export function customizable(
5656
this._shadowConfig.stylesheetKey !== scope ||
5757
this._shadowConfig.inShadow !== inShadow ||
5858
this._shadowConfig.window !== win
59-
// false
6059
) {
61-
this._shadowConfig = {
62-
stylesheetKey: scope,
63-
inShadow,
64-
window: win,
65-
__isShadowConfig__: true,
66-
};
60+
this._shadowConfig = makeShadowConfig(scope, inShadow, win);
6761
}
6862

6963
// eslint-disable-next-line @typescript-eslint/no-explicit-any

packages/utilities/src/shadowDom/MergeStylesRootContext.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { GLOBAL_STYLESHEET_KEY, Stylesheet } from '@fluentui/merge-styles';
2+
import { GLOBAL_STYLESHEET_KEY, Stylesheet, makeShadowConfig } from '@fluentui/merge-styles';
33
import { getWindow } from '../dom';
44

55
declare global {
@@ -82,12 +82,14 @@ export const MergeStylesRootProvider: React.FC<MergeStylesRootProviderProps> = (
8282

8383
let changed = false;
8484
const next = new Map<string, CSSStyleSheet>(stylesheets);
85-
const sheet = Stylesheet.getInstance({
86-
window: win,
87-
inShadow: false,
88-
stylesheetKey: GLOBAL_STYLESHEET_KEY,
89-
__isShadowConfig__: true,
90-
});
85+
const sheet = Stylesheet.getInstance(makeShadowConfig(GLOBAL_STYLESHEET_KEY, false, win));
86+
87+
// {
88+
// window: win,
89+
// inShadow: false,
90+
// stylesheetKey: GLOBAL_STYLESHEET_KEY,
91+
// __isShadowConfig__: true,
92+
// });
9193
sheet.forEachAdoptedStyleSheet((adoptedSheet, key) => {
9294
next.set(key, adoptedSheet);
9395
changed = true;

packages/utilities/src/shadowDom/MergeStylesShadowRootContext.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { GLOBAL_STYLESHEET_KEY } from '@fluentui/merge-styles';
2+
import { GLOBAL_STYLESHEET_KEY, makeShadowConfig } from '@fluentui/merge-styles';
33
import { FocusRectsProvider } from '../FocusRectsProvider';
44
import { useMergeStylesRootStylesheets } from './MergeStylesRootContext';
55

@@ -112,3 +112,16 @@ export const useHasMergeStylesShadowRootContext = () => {
112112
export const useMergeStylesShadowRootContext = () => {
113113
return React.useContext(MergeStylesShadowRootContext);
114114
};
115+
116+
/**
117+
* Get a shadow config.
118+
* @param stylesheetKey - Globally unique key
119+
* @param win - Reference to the `window` global.
120+
* @returns ShadowConfig
121+
*/
122+
export const useShadowConfig = (stylesheetKey: string, win?: Window) => {
123+
const inShadow = useHasMergeStylesShadowRootContext();
124+
return React.useMemo(() => {
125+
return makeShadowConfig(stylesheetKey, inShadow, win);
126+
}, [stylesheetKey, inShadow, win]);
127+
};

packages/utilities/src/styled.tsx

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { concatStyleSetsWithProps } from '@fluentui/merge-styles';
3-
import { useAdoptedStylesheet, useHasMergeStylesShadowRootContext } from './shadowDom/MergeStylesShadowRootContext';
3+
import { useAdoptedStylesheet, useShadowConfig } from './shadowDom/MergeStylesShadowRootContext';
44
import { useCustomizationSettings } from './customizations/useCustomizationSettings';
55
import type { IStyleSet, IStyleFunctionOrObject, ShadowConfig } from '@fluentui/merge-styles';
66
import { getWindow } from './dom/getWindow';
@@ -103,21 +103,7 @@ export function styled<
103103
const additionalProps = getProps ? getProps(props) : undefined;
104104

105105
const win = useWindow() ?? getWindow();
106-
107-
const inShadow = useHasMergeStylesShadowRootContext();
108-
const shadowConfig = React.useRef<ShadowConfig>({ stylesheetKey: scope, inShadow, __isShadowConfig__: true });
109-
if (
110-
shadowConfig.current.stylesheetKey !== scope ||
111-
shadowConfig.current.inShadow !== inShadow ||
112-
shadowConfig.current.window !== win
113-
) {
114-
shadowConfig.current = {
115-
stylesheetKey: scope,
116-
inShadow,
117-
window: win,
118-
__isShadowConfig__: true,
119-
};
120-
}
106+
const shadowConfig = useShadowConfig(scope, win);
121107

122108
// eslint-disable-next-line @typescript-eslint/no-explicit-any
123109
const cache = (styles.current && (styles.current as any).__cachedInputs__) || [];
@@ -143,7 +129,7 @@ export function styled<
143129
styles.current = concatenatedStyles as StyleFunction<TStyleProps, TStyleSet>;
144130
}
145131

146-
styles.current.__shadowConfig__ = shadowConfig.current;
132+
styles.current.__shadowConfig__ = shadowConfig;
147133

148134
useAdoptedStylesheet(scope);
149135

0 commit comments

Comments
 (0)