diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 2cd7dc2ea27a1e..b1613a202efde6 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,6 +1,11 @@ { "buildCommand": "build:codesandbox", - "packages": ["packages/react", "packages/react-components/react-components"], + "packages": [ + "packages/react", + "packages/react-components/react-components", + "packages/merge-styles", + "packages/utilities" + ], "sandboxes": ["x5u3t", "spnyu"], "node": "16" } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8f90f7e4737c1d..c5d92274c5c9a4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,5 +1,6 @@ pr: - master + - shadow-dom # Remove before merging to master # There's a separate pipeline for CI which also uses this file, but with a trigger override in the UI # https://dev.azure.com/uifabric/fabricpublic/_apps/hub/ms.vss-ciworkflow.build-ci-hub?_a=edit-build-definition&id=164&view=Tab_Triggers diff --git a/change/@fluentui-jest-serializer-merge-styles-8ad53e9f-3456-4924-b50d-48ffd979b2ce.json b/change/@fluentui-jest-serializer-merge-styles-8ad53e9f-3456-4924-b50d-48ffd979b2ce.json new file mode 100644 index 00000000000000..4248b6568e18b1 --- /dev/null +++ b/change/@fluentui-jest-serializer-merge-styles-8ad53e9f-3456-4924-b50d-48ffd979b2ce.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: add typescript types", + "packageName": "@fluentui/jest-serializer-merge-styles", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-merge-styles-284eeed7-0d8c-4ebf-a44c-1e34467aca9b.json b/change/@fluentui-merge-styles-284eeed7-0d8c-4ebf-a44c-1e34467aca9b.json new file mode 100644 index 00000000000000..abaaa5d8e4c87a --- /dev/null +++ b/change/@fluentui-merge-styles-284eeed7-0d8c-4ebf-a44c-1e34467aca9b.json @@ -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" +} diff --git a/change/@fluentui-react-b73398fa-9e0a-4d05-97a4-7a4820a876c1.json b/change/@fluentui-react-b73398fa-9e0a-4d05-97a4-7a4820a876c1.json new file mode 100644 index 00000000000000..8127128a9fa87f --- /dev/null +++ b/change/@fluentui-react-b73398fa-9e0a-4d05-97a4-7a4820a876c1.json @@ -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" +} diff --git a/change/@fluentui-utilities-27116d94-1a5f-4443-9287-25d1d27f63ca.json b/change/@fluentui-utilities-27116d94-1a5f-4443-9287-25d1d27f63ca.json new file mode 100644 index 00000000000000..36e8196592dcdc --- /dev/null +++ b/change/@fluentui-utilities-27116d94-1a5f-4443-9287-25d1d27f63ca.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix an issue where focus rect styles are not visible for components within shadow doms", + "packageName": "@fluentui/utilities", + "email": "brwai@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-utilities-3b3cdc9c-0e4f-4c68-aee6-9040e041186c.json b/change/@fluentui-utilities-3b3cdc9c-0e4f-4c68-aee6-9040e041186c.json new file mode 100644 index 00000000000000..efdf957b68fb92 --- /dev/null +++ b/change/@fluentui-utilities-3b3cdc9c-0e4f-4c68-aee6-9040e041186c.json @@ -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" +} diff --git a/packages/jest-serializer-merge-styles/tsconfig.json b/packages/jest-serializer-merge-styles/tsconfig.json index ea104793c121e9..9d96846319c431 100644 --- a/packages/jest-serializer-merge-styles/tsconfig.json +++ b/packages/jest-serializer-merge-styles/tsconfig.json @@ -3,7 +3,7 @@ "target": "es5", "outDir": "lib", "module": "commonjs", - "lib": ["es2017"], + "lib": ["es2017", "dom"], "jsx": "react", "declaration": true, "sourceMap": true, diff --git a/packages/merge-styles/etc/merge-styles.api.md b/packages/merge-styles/etc/merge-styles.api.md index bd0e748d69dfc3..f2705209f84fe1 100644 --- a/packages/merge-styles/etc/merge-styles.api.md +++ b/packages/merge-styles/etc/merge-styles.api.md @@ -33,9 +33,38 @@ export type DeepPartial = { [P in keyof T]?: T[P] extends (infer U)[] ? DeepPartial[] : T[P] extends object ? DeepPartial : T[P]; }; +// @public (undocumented) +export class EventMap { + constructor(); + // (undocumented) + forEach(callback: (value: V, key: K, map: Map) => void): void; + // (undocumented) + get(key: K): V | undefined; + // (undocumented) + has(key: K): boolean; + // (undocumented) + off(type: string, callback: EventHandler): void; + // Warning: (ae-forgotten-export) The symbol "EventHandler" needs to be exported by the entry point index.d.ts + // + // (undocumented) + on(type: string, callback: EventHandler): void; + // (undocumented) + raise(type: string, data: { + key: K; + sheet: V; + }): void; + // (undocumented) + set(key: K, value: V): void; +} + // @public export function fontFace(font: IFontFace): void; +// @public (undocumented) +export const GLOBAL_STYLESHEET_KEY = "__global__"; + +// Warning: (ae-forgotten-export) The symbol "IShadowConfig" needs to be exported by the entry point index.d.ts +// // @public export type IConcatenatedStyleSet> = { [P in keyof Omit_2]: IStyle; @@ -43,7 +72,7 @@ export type IConcatenatedStyleSet> = { subComponentStyles?: { [P in keyof TStyleSet['subComponentStyles']]: IStyleFunction; }; -}; +} & IShadowConfig; // @public export interface ICSPSettings { @@ -75,6 +104,9 @@ export const InjectionMode: { none: 0; insertNode: 1; appendChild: 2; + constructableStylesheet: 3; + insertNodeAndConstructableStylesheet: 4; + appedChildAndConstructableStylesheet: 5; }; // @public (undocumented) @@ -87,7 +119,7 @@ export type IProcessedStyleSet> = { subComponentStyles: { [P in keyof TStyleSet['subComponentStyles']]: __MapToFunctionType; }; -}; +} & IShadowConfig; // @public export interface IRawFontStyle { @@ -425,7 +457,7 @@ export interface IStyleBaseArray extends Array { } // @public -export type IStyleFunction> = (props: TStylesProps) => DeepPartial; +export type IStyleFunction> = (props: TStylesProps, __stylesheetKey__?: string) => DeepPartial; // @public export type IStyleFunctionOrObject> = IStyleFunction | DeepPartial; @@ -439,7 +471,7 @@ export type IStyleSet = { subComponentStyles?: { [P in keyof TStyleSet['subComponentStyles']]: IStyleFunctionOrObject; }; -}; +} & IShadowConfig; // @public export interface IStyleSheetConfig { @@ -459,22 +491,23 @@ export interface IStyleSheetConfig { export function keyframes(timeline: IKeyframes): string; // Warning: (ae-forgotten-export) The symbol "IStyleOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ShadowConfig" needs to be exported by the entry point index.d.ts // // @public -export function mergeCss(args: (IStyle | IStyleBaseArray | false | null | undefined) | (IStyle | IStyleBaseArray | false | null | undefined)[], options?: IStyleOptions): string; +export function mergeCss(args: (IStyle | IStyleBaseArray | false | null | undefined) | (IStyle | IStyleBaseArray | false | null | undefined)[], options?: IStyleOptions, shadowConfig?: ShadowConfig): string; // @public -export function mergeCssSets(styleSets: [TStyleSet | false | null | undefined], options?: IStyleOptions): IProcessedStyleSet; +export function mergeCssSets(styleSets: [TStyleSet | false | null | undefined], options?: IStyleOptions, shadowConfig?: ShadowConfig): IProcessedStyleSet; // @public -export function mergeCssSets(styleSets: [TStyleSet1 | false | null | undefined, TStyleSet2 | false | null | undefined], options?: IStyleOptions): IProcessedStyleSet; +export function mergeCssSets(styleSets: [TStyleSet1 | false | null | undefined, TStyleSet2 | false | null | undefined], options?: IStyleOptions, shadowConfig?: ShadowConfig): IProcessedStyleSet; // @public export function mergeCssSets(styleSets: [ TStyleSet1 | false | null | undefined, TStyleSet2 | false | null | undefined, TStyleSet3 | false | null | undefined -], options?: IStyleOptions): IProcessedStyleSet; +], options?: IStyleOptions, shadowConfig?: ShadowConfig): IProcessedStyleSet; // @public export function mergeCssSets(styleSets: [ @@ -482,10 +515,10 @@ TStyleSet1 | false | null | undefined, TStyleSet2 | false | null | undefined, TStyleSet3 | false | null | undefined, TStyleSet4 | false | null | undefined -], options?: IStyleOptions): IProcessedStyleSet & ObjectOnly & ObjectOnly & ObjectOnly>; +], options?: IStyleOptions, shadowConfig?: ShadowConfig): IProcessedStyleSet & ObjectOnly & ObjectOnly & ObjectOnly>; // @public -export function mergeCssSets(styleSet: [TStyleSet | false | null | undefined], options?: IStyleOptions): IProcessedStyleSet; +export function mergeCssSets(styleSet: [TStyleSet | false | null | undefined], options?: IStyleOptions, shadowConfig?: ShadowConfig): IProcessedStyleSet; // @public export function mergeStyles(...args: (IStyle | IStyleBaseArray | false | null | undefined)[]): string; @@ -503,7 +536,10 @@ export function mergeStyleSets(styleSet1: TS export function mergeStyleSets(styleSet1: TStyleSet1 | false | null | undefined, styleSet2: TStyleSet2 | false | null | undefined, styleSet3: TStyleSet3 | false | null | undefined, styleSet4: TStyleSet4 | false | null | undefined): IProcessedStyleSet & ObjectOnly & ObjectOnly & ObjectOnly>; // @public -export function mergeStyleSets(...styleSets: Array): IProcessedStyleSet; +export function mergeStyleSets(...styleSets: Array): IProcessedStyleSet; + +// @public (undocumented) +export function mergeStyleSets(shadowConfig: ShadowConfig, ...styleSets: Array): IProcessedStyleSet; // @public (undocumented) export type ObjectOnly = TArg extends {} ? TArg : {}; @@ -517,32 +553,55 @@ export { Omit_2 as Omit } // @public export function setRTL(isRTL: boolean): void; +// @public (undocumented) +export type ShadowConfig = { + stylesheetKey: string; + inShadow: boolean; + window?: Window; +}; + // @public export class Stylesheet { - constructor(config?: IStyleSheetConfig, serializedStylesheet?: ISerializedStylesheet); + constructor(config?: IStyleSheetConfig, serializedStylesheet?: ISerializedStylesheet, stylesheetKey?: string); argsFromClassName(className: string): IStyle[] | undefined; cacheClassName(className: string, key: string, args: IStyle[], rules: string[]): void; classNameFromKey(key: string): string | undefined; + // (undocumented) + get counter(): number; + // (undocumented) + static forEachAdoptedStyleSheet(callback: (value: Stylesheet, key: string, map: Map) => void, srcWindow?: Window): void; + // (undocumented) + getAdoptableStyleSheet(): CSSStyleSheet | undefined; getClassName(displayName?: string): string; getClassNameCache(): { [key: string]: string; }; - static getInstance(): Stylesheet; + static getInstance(shadowConfig?: ShadowConfig): Stylesheet; getRules(includePreservedRules?: boolean): string; insertedRulesFromClassName(className: string): string[] | undefined; insertRule(rule: string, preserve?: boolean): void; + // (undocumented) + static offAddConstructableStyleSheet(callback: EventHandler, targetWindow?: Window): void; + // Warning: (ae-forgotten-export) The symbol "EventHandler" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static onAddConstructableStyleSheet(callback: EventHandler, targetWindow?: Window): void; onInsertRule(callback: Function): Function; onReset(callback: Function): Function; + // (undocumented) + static projectStylesToWindow(targetWindow: Window, srcWindow?: Window): void; reset(): void; // (undocumented) resetKeys(): void; serialize(): string; + // (undocumented) + setAdoptableStyleSheet(sheet: CSSStyleSheet): void; setConfig(config?: IStyleSheetConfig): void; } // Warnings were encountered during analysis: // -// lib/IStyleSet.d.ts:53:5 - (ae-forgotten-export) The symbol "__MapToFunctionType" needs to be exported by the entry point index.d.ts +// lib/IStyleSet.d.ts:54:5 - (ae-forgotten-export) The symbol "__MapToFunctionType" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/merge-styles/src/EventMap.ts b/packages/merge-styles/src/EventMap.ts new file mode 100644 index 00000000000000..e1337f51fbd725 --- /dev/null +++ b/packages/merge-styles/src/EventMap.ts @@ -0,0 +1,58 @@ +export type EventArgs = { key: string; sheet: T }; +export type EventHandler = (args: EventArgs) => void; + +export class EventMap { + private _events: Map[]>; + private _self: Map; + + constructor() { + this._self = new Map(); + this._events = new Map[]>(); + } + + public get(key: K) { + return this._self.get(key); + } + + public set(key: K, value: V) { + this._self.set(key, value); + } + + public has(key: K) { + return this._self.has(key); + } + + public forEach(callback: (value: V, key: K, map: Map) => void) { + this._self.forEach(callback); + } + + public raise(type: string, data: EventArgs) { + const handlers = this._events.get(type); + if (!handlers) { + return; + } + + for (const handler of handlers) { + handler?.(data); + } + } + + public on(type: string, callback: EventHandler) { + const handlers = this._events.get(type); + if (!handlers) { + this._events.set(type, [callback]); + } else { + handlers.push(callback); + } + } + + public off(type: string, callback: EventHandler) { + const handlers = this._events.get(type); + if (handlers) { + const index = handlers.indexOf(callback); + if (index >= 0) { + handlers.splice(index, 1); + } + } + } +} diff --git a/packages/merge-styles/src/IStyleFunction.ts b/packages/merge-styles/src/IStyleFunction.ts index 8c2d9e8609c870..73d70761815b1b 100644 --- a/packages/merge-styles/src/IStyleFunction.ts +++ b/packages/merge-styles/src/IStyleFunction.ts @@ -7,6 +7,7 @@ import { DeepPartial } from './DeepPartial'; */ export type IStyleFunction> = ( props: TStylesProps, + __stylesheetKey__?: string, ) => DeepPartial; /** diff --git a/packages/merge-styles/src/IStyleOptions.ts b/packages/merge-styles/src/IStyleOptions.ts index cb68444e487cf6..07f82f33d500f4 100644 --- a/packages/merge-styles/src/IStyleOptions.ts +++ b/packages/merge-styles/src/IStyleOptions.ts @@ -1,4 +1,8 @@ +import { ShadowConfig } from './shadowConfig'; + export interface IStyleOptions { rtl?: boolean; specificityMultiplier?: number; + // stylesheetKey?: string; + shadowConfig?: ShadowConfig; } diff --git a/packages/merge-styles/src/IStyleSet.ts b/packages/merge-styles/src/IStyleSet.ts index 64aa0d9e6a81d5..b02c22b98a6cd7 100644 --- a/packages/merge-styles/src/IStyleSet.ts +++ b/packages/merge-styles/src/IStyleSet.ts @@ -1,5 +1,6 @@ import { IStyle } from './IStyle'; import { IStyleFunctionOrObject, IStyleFunction } from './IStyleFunction'; +import { ShadowConfig } from './shadowConfig'; /** * @deprecated Use `Exclude` provided by TypeScript instead. @@ -33,7 +34,7 @@ export type IStyleSet = { [key: string]: [P in keyof Omit]: IStyle; } & { subComponentStyles?: { [P in keyof TStyleSet['subComponentStyles']]: IStyleFunctionOrObject }; -}; +} & IShadowConfig; /** * A concatenated style set differs from `IStyleSet` in that subComponentStyles will always be a style function. @@ -43,7 +44,7 @@ export type IConcatenatedStyleSet> = { [P in keyof Omit]: IStyle; } & { subComponentStyles?: { [P in keyof TStyleSet['subComponentStyles']]: IStyleFunction }; -}; +} & IShadowConfig; /** * A processed style set is one which the set of styles associated with each area has been converted @@ -58,4 +59,8 @@ export type IProcessedStyleSet> = { TStyleSet['subComponentStyles'] extends infer J ? (P extends keyof J ? J[P] : never) : never >; }; +} & IShadowConfig; + +type IShadowConfig = { + __shadowConfig__?: ShadowConfig; }; diff --git a/packages/merge-styles/src/Stylesheet.ts b/packages/merge-styles/src/Stylesheet.ts index 3be6223b3d3119..a7cf216876e61e 100644 --- a/packages/merge-styles/src/Stylesheet.ts +++ b/packages/merge-styles/src/Stylesheet.ts @@ -1,4 +1,6 @@ import { IStyle } from './IStyle'; +import { DEFAULT_SHADOW_CONFIG, GLOBAL_STYLESHEET_KEY, ShadowConfig } from './shadowConfig'; +import { EventHandler, EventMap } from './EventMap'; export const InjectionMode = { /** @@ -15,6 +17,21 @@ export const InjectionMode = { * Appends rules using appendChild. */ appendChild: 2 as 2, + + /** + * Inserts rules into constructable stylesheets. + */ + constructableStylesheet: 3 as 3, + + /** + * Same as `insertNode` and `constructableStylesheet` + */ + insertNodeAndConstructableStylesheet: 4 as 4, + + /** + * Same as `appendChild` and `constructableStylesheet` + */ + appedChildAndConstructableStylesheet: 5 as 5, }; export type InjectionMode = (typeof InjectionMode)[keyof typeof InjectionMode]; @@ -88,18 +105,29 @@ export interface ISerializedStylesheet { } const STYLESHEET_SETTING = '__stylesheet__'; + +const ADOPTED_STYLESHEETS = '__mergeStylesAdoptedStyleSheets__'; + /** * MSIE 11 doesn't cascade styles based on DOM ordering, but rather on the order that each style node * is created. As such, to maintain consistent priority, IE11 should reuse a single style node. */ const REUSE_STYLE_NODE = typeof navigator !== 'undefined' && /rv:11.0/.test(navigator.userAgent); +const SUPPORTS_CONSTRUCTIBLE_STYLESHEETS = typeof window !== 'undefined' && 'CSSStyleSheet' in window; + +export type AdoptableStylesheet = { + fluentSheet: Stylesheet; + adoptedSheet: CSSStyleSheet; +}; + let _global: (Window | {}) & { [STYLESHEET_SETTING]?: Stylesheet; FabricConfig?: { mergeStyles?: IStyleSheetConfig; serializedStylesheet?: ISerializedStylesheet; }; + [ADOPTED_STYLESHEETS]?: EventMap; } = {}; // Grab window. @@ -114,6 +142,8 @@ try { let _stylesheet: Stylesheet | undefined; +let constructableStyleSheetCounter = 0; + /** * Represents the state of styles registered in the page. Abstracts * the surface for adding styles to the stylesheet, exposes helpers @@ -124,10 +154,14 @@ let _stylesheet: Stylesheet | undefined; export class Stylesheet { private _lastStyleElement?: HTMLStyleElement; private _styleElement?: HTMLStyleElement; + + private _constructibleSheet?: CSSStyleSheet; + private _stylesheetKey?: string; + private _rules: string[] = []; private _preservedRules: string[] = []; private _config: IStyleSheetConfig; - private _counter = 0; + private _styleCounter = 0; private _keyToClassName: { [key: string]: string } = {}; private _onInsertRuleCallbacks: Function[] = []; private _onResetCallbacks: Function[] = []; @@ -136,35 +170,158 @@ export class Stylesheet { /** * Gets the singleton instance. */ - public static getInstance(): Stylesheet { - _stylesheet = _global[STYLESHEET_SETTING] as Stylesheet; + public static getInstance(shadowConfig?: ShadowConfig): Stylesheet { + const { stylesheetKey, inShadow, window: win } = shadowConfig ?? DEFAULT_SHADOW_CONFIG; + const global = (win ?? _global) as typeof _global; + + if (stylesheetKey && inShadow) { + _stylesheet = global[ADOPTED_STYLESHEETS]?.get(stylesheetKey); + } else { + _stylesheet = global[STYLESHEET_SETTING] as Stylesheet; + } if (!_stylesheet || (_stylesheet._lastStyleElement && _stylesheet._lastStyleElement.ownerDocument !== document)) { - const fabricConfig = _global?.FabricConfig || {}; + const fabricConfig = global?.FabricConfig || {}; + if (inShadow) { + fabricConfig.mergeStyles = fabricConfig.mergeStyles || {}; + fabricConfig.mergeStyles.injectionMode = InjectionMode.constructableStylesheet; + } - const stylesheet = new Stylesheet(fabricConfig.mergeStyles, fabricConfig.serializedStylesheet); + const stylesheet = new Stylesheet(fabricConfig.mergeStyles, fabricConfig.serializedStylesheet, stylesheetKey); _stylesheet = stylesheet; - _global[STYLESHEET_SETTING] = stylesheet; + if (stylesheetKey) { + if (inShadow || stylesheetKey === GLOBAL_STYLESHEET_KEY) { + if (!global[ADOPTED_STYLESHEETS]) { + global[ADOPTED_STYLESHEETS] = new EventMap(); + } + global[ADOPTED_STYLESHEETS]!.set(stylesheetKey, stylesheet); + (global as Window).requestAnimationFrame?.(() => { + global[ADOPTED_STYLESHEETS]!.raise('add-sheet', { key: stylesheetKey, sheet: stylesheet }); + }); + } + + if (stylesheetKey === GLOBAL_STYLESHEET_KEY) { + global[STYLESHEET_SETTING] = stylesheet; + } + } else { + global[STYLESHEET_SETTING] = stylesheet; + } } return _stylesheet; } - constructor(config?: IStyleSheetConfig, serializedStylesheet?: ISerializedStylesheet) { + public static projectStylesToWindow(targetWindow: Window, srcWindow?: Window): void { + const global = (srcWindow ?? _global) as typeof _global; + const clone = new EventMap(); + + // TODO: add support for