Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shadow DOM support to @fluentui/react (Fluent v8) #30689

Merged
merged 33 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ee92ede
mergeStyles: initial shadow DOM and constructable stylesheets impleme…
spmonahan Aug 30, 2023
e94272d
Add focus rect support in shadow dom (#29046)
brwai Aug 31, 2023
132177d
Merge styles/styles missing (#29119)
spmonahan Oct 6, 2023
14ecd70
Shadow DOM: more documentation (#29442)
spmonahan Oct 9, 2023
07613e4
Shadow DOM layers (#29468)
spmonahan Oct 9, 2023
daadfb7
Merge styles/cleanup (#29485)
spmonahan Oct 11, 2023
a691cf7
feat: add helper functions and docs (#29506)
spmonahan Oct 12, 2023
d3aae6f
temporarily deploy utilities (#29518)
spmonahan Oct 13, 2023
ad1869e
feat: add support for handling multiple versions of Fluent on the sam…
spmonahan Oct 27, 2023
e858df3
update new pipeline to temporarily trigger for shadow-dom branch
spmonahan Nov 3, 2023
34a962f
Improve shadow DOM focus rects (#29750)
spmonahan Nov 16, 2023
185a6c4
`merge-styles` `adoptedStylesheets` polyfill (#29985)
spmonahan Dec 11, 2023
548d27d
docs: add shadow DOM TeachingBubble examples (#30088)
spmonahan Jan 4, 2024
e9e5d6e
chore: maintain style sheet sort order for all shadow roots (#30093)
spmonahan Jan 4, 2024
2f7517c
Shadow DOM: `FocusZone` and `FocusTrapZone` (#30206)
spmonahan Jan 5, 2024
c8f12b8
chore: performance optimizations for shadow dom feature (#30651)
spmonahan Mar 1, 2024
2205b65
remove pipeline overrides
spmonahan Mar 1, 2024
9dba7a9
update snapshots with shadow DOM changes
spmonahan Mar 8, 2024
d76c871
temporarily update pipelines to target 'shadow-dom' branch
spmonahan Mar 8, 2024
5623c50
Shadow DOM: prune bundle size impact (#30709)
spmonahan Mar 22, 2024
b9aca3a
update API snapshot
spmonahan Mar 23, 2024
88aeaba
revert temporary 'shadow-dom' triggers in pipelines
spmonahan Mar 23, 2024
43654b0
update test snapshots
spmonahan Mar 23, 2024
7363767
update change file
spmonahan Mar 26, 2024
99edc79
remove unnecessary type assertion
spmonahan Mar 28, 2024
8511673
refactor MergeStylesRootContext to provide default hooks.
spmonahan Mar 28, 2024
11a0841
update merge-styles docs
spmonahan Mar 28, 2024
65b5de7
update test snapshot
spmonahan Mar 28, 2024
6456c93
remove commented out code
spmonahan Mar 29, 2024
51b53d9
syncpack
spmonahan Apr 4, 2024
0cc53fe
remove files that were accidentally added
spmonahan Apr 4, 2024
802ffdd
syncpack
spmonahan Apr 15, 2024
684f6aa
update lint-imports to exclude Shadow DOM examples
spmonahan Apr 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions apps/stress-test/src/renderers/wc/btn/wcBasicButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import { DOMSelectorTreeComponentRenderer } from '../../../shared/vanilla/types'

spmonahan marked this conversation as resolved.
Show resolved Hide resolved
declare global {
interface Document {
// eslint-disable-next-line
adoptedStyleSheets: any[];
adoptedStyleSheets: CSSStyleSheet[];
}

interface ShadowRoot {
// eslint-disable-next-line
adoptedStyleSheets: any[];
adoptedStyleSheets: CSSStyleSheet[];
}

interface CSSStyleSheet {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add shadow DOM support for DOM APIs",
"packageName": "@fluentui/dom-utilities",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: add typescript types",
"packageName": "@fluentui/jest-serializer-merge-styles",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: add support for shadow dom and constructable stylesheets",
"packageName": "@fluentui/merge-styles",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: update some components to optionally support shadow dom",
"packageName": "@fluentui/react",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add shadow DOM support when traversing the DOM",
"packageName": "@fluentui/react-focus",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add support for shadow roots",
"packageName": "@fluentui/react-hooks",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: add support for shadow dom and constructable stylesheets",
"packageName": "@fluentui/style-utilities",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: add support for shadow dom and constructable stylesheets",
"packageName": "@fluentui/utilities",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}
6 changes: 6 additions & 0 deletions packages/dom-utilities/etc/dom-utilities.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ export function elementContainsAttribute(element: HTMLElement, attribute: string
// @public
export function findElementRecursive(element: HTMLElement | null, matchFunction: (element: HTMLElement) => boolean, doc?: Document): HTMLElement | null;

// @public (undocumented)
export const getActiveElement: (doc: Document) => Element | null;

// @public
export function getChildren(parent: HTMLElement, allowVirtualChildren?: boolean): HTMLElement[];

// @public (undocumented)
export const getEventTarget: (event: Event) => HTMLElement | null;

// @public
export function getParent(child: HTMLElement, allowVirtualParents?: boolean): HTMLElement | null;

Expand Down
9 changes: 9 additions & 0 deletions packages/dom-utilities/src/getActiveElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const getActiveElement = (doc: Document): Element | null => {
let ae = doc.activeElement;

while (ae?.shadowRoot) {
ae = ae.shadowRoot.activeElement;
}

return ae;
};
8 changes: 8 additions & 0 deletions packages/dom-utilities/src/getEventTarget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const getEventTarget = (event: Event): HTMLElement | null => {
let target = event.target as HTMLElement;
if (target && target.shadowRoot) {
target = event.composedPath()[0] as HTMLElement;
}

return target;
};
25 changes: 21 additions & 4 deletions packages/dom-utilities/src/getParent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,25 @@ import { getVirtualParent } from './getVirtualParent';
* @public
*/
export function getParent(child: HTMLElement, allowVirtualParents: boolean = true): HTMLElement | null {
return (
child &&
((allowVirtualParents && getVirtualParent(child)) || (child.parentNode && (child.parentNode as HTMLElement)))
);
if (!child) {
return null;
}

const parent = allowVirtualParents && getVirtualParent(child);

if (parent) {
return parent;
}

// Support looking for parents in shadow DOM
if (typeof (child as HTMLSlotElement).assignedElements !== 'function' && child.assignedSlot?.parentNode) {
// Element is slotted
return child.assignedSlot as HTMLElement;
} else if (child.parentNode?.nodeType === 11) {
// nodeType 11 is DOCUMENT_FRAGMENT
// Element is in shadow root
return (child.parentNode as ShadowRoot).host as HTMLElement;
} else {
return child.parentNode as HTMLElement;
}
}
24 changes: 13 additions & 11 deletions packages/dom-utilities/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
export * from './IVirtualElement';
export * from './elementContains';
export * from './elementContainsAttribute';
export * from './findElementRecursive';
export * from './getChildren';
export * from './getParent';
export * from './getVirtualParent';
export * from './isVirtualElement';
export * from './portalContainsElement';
export * from './setPortalAttribute';
export * from './setVirtualParent';
export type { IVirtualElement } from './IVirtualElement';
export { elementContains } from './elementContains';
export { elementContainsAttribute } from './elementContainsAttribute';
export { findElementRecursive } from './findElementRecursive';
export { getActiveElement } from './getActiveElement';
export { getChildren } from './getChildren';
export { getEventTarget } from './getEventTarget';
export { getParent } from './getParent';
export { getVirtualParent } from './getVirtualParent';
export { isVirtualElement } from './isVirtualElement';
export { portalContainsElement } from './portalContainsElement';
export { DATA_PORTAL_ATTRIBUTE, setPortalAttribute } from './setPortalAttribute';
export { setVirtualParent } from './setVirtualParent';

import './version';
2 changes: 1 addition & 1 deletion packages/jest-serializer-merge-styles/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"target": "es5",
"outDir": "lib",
"module": "commonjs",
"lib": ["es2017"],
"lib": ["es2017", "dom"],
spmonahan marked this conversation as resolved.
Show resolved Hide resolved
"jsx": "react",
"declaration": true,
"sourceMap": true,
Expand Down
73 changes: 73 additions & 0 deletions packages/merge-styles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,76 @@ window.FabricConfig = {
},
};
```

## Shadow DOM

`merge-styles` has 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):

1. `MergeStylesRootProvider`: acts as the "global" context for your application. You should have one of these per page.
2. `MergeStylesShadowRootProvider`: a context for each shadow root in your application. You should have one of these per shadow root.

`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.

### Shadow DOM example

```tsx
import { PrimaryButton } from '@fluentui/react';
import { MergeStylesRootProvider, MergeStylesShadowRootProvider } from '@fluentui/utilities';
import root from 'react-shadow';

const ShadowRoot = ({ children }) => {
// This is a ref but we're using state to manage it so we can force
// a re-render.
const [shadowRootEl, setShadowRootEl] = React.useState<HTMLElement | null>(null);

return (
<MergeStylesRootProvider>
<root.div className="shadow-root" delegatesFocus ref={setShadowRootEl}>
<MergeStylesShadowRootProvider shadowRoot={shadowRootEl?.shadowRoot}>{children}</MergeStylesShadowRootProvider>
</root.div>
</MergeStylesRootProvider>
);
};

<ShadowRoot>
<PrimaryButton>I'm in the shadow DOM!</PrimaryButton>
</ShadowRoot>
<PrimaryButton>I'm in the light DOM!</PrimaryButton>
```

### Scoping styles for more efficient CSS

You do not _need_ to update your `merge-styles` styles to support shadow DOM but you can make styles more efficient with some updates.

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.

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.

All `@fluentui/react` styles are scoped 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.
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.

#### Style scoping example

```tsx
import { useMergeStylesHooks } from '@fluentui/react';
import { mergeStyles } from '@fluentui/merge-styles';
import type { ShadowConfig } from '@fluentui/merge-styles';

// This must be globally unique for the application
const MY_COMPONENT_STYLESHEET_KEY: string = 'my-unique-key';

const MyComponent = props => {
const { useWindow, useShadowConfig, useAdoptedStylesheet } = useMergeStylesHooks();

// Make sure multi-window scenarios work (e.g., pop outs)
const win: Window = useWindow();
const shadowConfig: ShadowConfig = useShadowConfig(MY_COMPONENT_STYLESHEET_KEY, win);

const styles = React.useMemo(() => {
// shadowConfig must be the first parameter when it is used
return mergeStyles(shadowConfig, myStyles);
}, [shadowConfig, myStyles]);

useAdoptedStylesheet(MY_COMPONENT_STYLESHEET_KEY);
};
```
Loading
Loading