diff --git a/apps/perf-test/.eslintrc.json b/apps/perf-test/.eslintrc.json index 6c6d649f517582..b77a03e1dea94e 100644 --- a/apps/perf-test/.eslintrc.json +++ b/apps/perf-test/.eslintrc.json @@ -2,6 +2,7 @@ "extends": ["plugin:@fluentui/eslint-plugin/react"], "root": true, "rules": { - "no-console": "off" + "no-console": "off", + "no-restricted-globals": "off" } } diff --git a/apps/pr-deploy-site/.eslintrc.json b/apps/pr-deploy-site/.eslintrc.json index ac81a128fc7689..75b0d9e79f26dc 100644 --- a/apps/pr-deploy-site/.eslintrc.json +++ b/apps/pr-deploy-site/.eslintrc.json @@ -13,7 +13,8 @@ "curly": "off", "no-var": "off", "vars-on-top": "off", - "prefer-arrow-callback": "off" + "prefer-arrow-callback": "off", + "no-restricted-globals": "off" } } ] diff --git a/apps/public-docsite-resources/.eslintrc.json b/apps/public-docsite-resources/.eslintrc.json index 8e090ba63188fa..b9cc959897e9b2 100644 --- a/apps/public-docsite-resources/.eslintrc.json +++ b/apps/public-docsite-resources/.eslintrc.json @@ -5,6 +5,7 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/explicit-member-accessibility": "off", "@typescript-eslint/member-ordering": "off", - "deprecation/deprecation": "off" + "deprecation/deprecation": "off", + "no-restricted-globals": "off" } } diff --git a/apps/public-docsite/.eslintrc.json b/apps/public-docsite/.eslintrc.json index 80f89945c1a8dc..4a1010d7c11d98 100644 --- a/apps/public-docsite/.eslintrc.json +++ b/apps/public-docsite/.eslintrc.json @@ -6,6 +6,7 @@ "deprecation/deprecation": "off", "import/no-webpack-loader-syntax": "off", // ok in this project "prefer-const": "off", - "react/jsx-no-bind": "off" + "react/jsx-no-bind": "off", + "no-restricted-globals": "off" } } diff --git a/apps/theming-designer/.eslintrc.json b/apps/theming-designer/.eslintrc.json index f7aed51167d6cb..0d9ca4c10f823d 100644 --- a/apps/theming-designer/.eslintrc.json +++ b/apps/theming-designer/.eslintrc.json @@ -4,6 +4,7 @@ "rules": { "@typescript-eslint/no-explicit-any": "off", "deprecation/deprecation": "off", - "prefer-const": "off" + "prefer-const": "off", + "no-restricted-globals": "off" } } diff --git a/apps/vr-tests/.eslintrc.json b/apps/vr-tests/.eslintrc.json index b3f394f7cbaca5..54ac641d7b3bfc 100644 --- a/apps/vr-tests/.eslintrc.json +++ b/apps/vr-tests/.eslintrc.json @@ -5,6 +5,7 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/jsx-no-bind": "off", "deprecation/deprecation": "off", - "import/no-extraneous-dependencies": ["error", { "packageDir": [".", "../.."] }] + "import/no-extraneous-dependencies": ["error", { "packageDir": [".", "../.."] }], + "no-restricted-globals": "off" } } diff --git a/change/@fluentui-dom-utilities-3c9941d3-fd1e-4a07-85f3-e239836f2cfc.json b/change/@fluentui-dom-utilities-3c9941d3-fd1e-4a07-85f3-e239836f2cfc.json new file mode 100644 index 00000000000000..0f5ed9e946deca --- /dev/null +++ b/change/@fluentui-dom-utilities-3c9941d3-fd1e-4a07-85f3-e239836f2cfc.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disallow document and window access", + "packageName": "@fluentui/dom-utilities", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-eslint-plugin-7df50bae-6d4c-4022-ba1c-ff08e41f8c68.json b/change/@fluentui-eslint-plugin-7df50bae-6d4c-4022-ba1c-ff08e41f8c68.json new file mode 100644 index 00000000000000..ed5cf01918be36 --- /dev/null +++ b/change/@fluentui-eslint-plugin-7df50bae-6d4c-4022-ba1c-ff08e41f8c68.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: add restricted globals for @fluentui/react and related packages", + "packageName": "@fluentui/eslint-plugin", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-merge-styles-bb6fa0b4-0bfd-45c1-8ab9-140b968c7f9b.json b/change/@fluentui-merge-styles-bb6fa0b4-0bfd-45c1-8ab9-140b968c7f9b.json new file mode 100644 index 00000000000000..db9f69ab4c30a1 --- /dev/null +++ b/change/@fluentui-merge-styles-bb6fa0b4-0bfd-45c1-8ab9-140b968c7f9b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disallow document and window access", + "packageName": "@fluentui/merge-styles", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-21dccde6-b184-46e2-8427-18a9d856443c.json b/change/@fluentui-react-21dccde6-b184-46e2-8427-18a9d856443c.json new file mode 100644 index 00000000000000..f9f4f48f593359 --- /dev/null +++ b/change/@fluentui-react-21dccde6-b184-46e2-8427-18a9d856443c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disallow document and window access", + "packageName": "@fluentui/react", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-file-type-icons-2fbf5c3f-d2bf-43cf-942b-64122c56a603.json b/change/@fluentui-react-file-type-icons-2fbf5c3f-d2bf-43cf-942b-64122c56a603.json new file mode 100644 index 00000000000000..a032e4c2bb3be2 --- /dev/null +++ b/change/@fluentui-react-file-type-icons-2fbf5c3f-d2bf-43cf-942b-64122c56a603.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disallow document and window access", + "packageName": "@fluentui/react-file-type-icons", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-focus-7e69e339-9791-4ebc-8191-6bace7a0f0cb.json b/change/@fluentui-react-focus-7e69e339-9791-4ebc-8191-6bace7a0f0cb.json new file mode 100644 index 00000000000000..e4a95713e70307 --- /dev/null +++ b/change/@fluentui-react-focus-7e69e339-9791-4ebc-8191-6bace7a0f0cb.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disallow document and window access", + "packageName": "@fluentui/react-focus", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-window-provider-e664cb1c-eaa1-4854-997f-0b08b0feecda.json b/change/@fluentui-react-window-provider-e664cb1c-eaa1-4854-997f-0b08b0feecda.json new file mode 100644 index 00000000000000..2a49c94426968a --- /dev/null +++ b/change/@fluentui-react-window-provider-e664cb1c-eaa1-4854-997f-0b08b0feecda.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: clean up exports", + "packageName": "@fluentui/react-window-provider", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-utilities-c5fe63ad-e579-4a80-b09d-ed584717a112.json b/change/@fluentui-utilities-c5fe63ad-e579-4a80-b09d-ed584717a112.json new file mode 100644 index 00000000000000..3cc90880d59436 --- /dev/null +++ b/change/@fluentui-utilities-c5fe63ad-e579-4a80-b09d-ed584717a112.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disallow document and window access", + "packageName": "@fluentui/utilities", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/dom-utilities/etc/dom-utilities.api.md b/packages/dom-utilities/etc/dom-utilities.api.md index 24373f59c4fcc0..2e27fa6ecb1977 100644 --- a/packages/dom-utilities/etc/dom-utilities.api.md +++ b/packages/dom-utilities/etc/dom-utilities.api.md @@ -11,10 +11,10 @@ export const DATA_PORTAL_ATTRIBUTE = "data-portal-element"; export function elementContains(parent: HTMLElement | null, child: HTMLElement | null, allowVirtualParents?: boolean): boolean; // @public -export function elementContainsAttribute(element: HTMLElement, attribute: string): string | null; +export function elementContainsAttribute(element: HTMLElement, attribute: string, doc?: Document): string | null; // @public -export function findElementRecursive(element: HTMLElement | null, matchFunction: (element: HTMLElement) => boolean): HTMLElement | null; +export function findElementRecursive(element: HTMLElement | null, matchFunction: (element: HTMLElement) => boolean, doc?: Document): HTMLElement | null; // @public export function getChildren(parent: HTMLElement, allowVirtualChildren?: boolean): HTMLElement[]; @@ -38,7 +38,7 @@ export interface IVirtualElement extends HTMLElement { } // @public -export function portalContainsElement(target: HTMLElement, parent?: HTMLElement): boolean; +export function portalContainsElement(target: HTMLElement, parent?: HTMLElement, doc?: Document): boolean; // @public export function setPortalAttribute(element: HTMLElement): void; @@ -46,7 +46,6 @@ export function setPortalAttribute(element: HTMLElement): void; // @public export function setVirtualParent(child: HTMLElement, parent: HTMLElement | null): void; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/dom-utilities/src/elementContainsAttribute.ts b/packages/dom-utilities/src/elementContainsAttribute.ts index 04a53bc19884ca..8a88beeb4a90d8 100644 --- a/packages/dom-utilities/src/elementContainsAttribute.ts +++ b/packages/dom-utilities/src/elementContainsAttribute.ts @@ -6,8 +6,12 @@ import { findElementRecursive } from './findElementRecursive'; * @param attribute - the attribute to search for * @returns the value of the first instance found */ -export function elementContainsAttribute(element: HTMLElement, attribute: string): string | null { - const elementMatch = findElementRecursive(element, (testElement: HTMLElement) => testElement.hasAttribute(attribute)); +export function elementContainsAttribute(element: HTMLElement, attribute: string, doc?: Document): string | null { + const elementMatch = findElementRecursive( + element, + (testElement: HTMLElement) => testElement.hasAttribute(attribute), + doc, + ); return elementMatch && elementMatch.getAttribute(attribute); } diff --git a/packages/dom-utilities/src/findElementRecursive.ts b/packages/dom-utilities/src/findElementRecursive.ts index 9dc8118575595f..afac46bc46f256 100644 --- a/packages/dom-utilities/src/findElementRecursive.ts +++ b/packages/dom-utilities/src/findElementRecursive.ts @@ -8,8 +8,11 @@ import { getParent } from './getParent'; export function findElementRecursive( element: HTMLElement | null, matchFunction: (element: HTMLElement) => boolean, + doc?: Document, ): HTMLElement | null { - if (!element || element === document.body) { + // eslint-disable-next-line no-restricted-globals + doc ??= document; + if (!element || element === doc.body) { return null; } return matchFunction(element) ? element : findElementRecursive(getParent(element), matchFunction); diff --git a/packages/dom-utilities/src/portalContainsElement.ts b/packages/dom-utilities/src/portalContainsElement.ts index a5b9391aa576af..979b364d3e1a7d 100644 --- a/packages/dom-utilities/src/portalContainsElement.ts +++ b/packages/dom-utilities/src/portalContainsElement.ts @@ -9,10 +9,11 @@ import { DATA_PORTAL_ATTRIBUTE } from './setPortalAttribute'; * @param parent - Optional parent perspective. Search for containing portal stops at parent * (or root if parent is undefined or invalid.) */ -export function portalContainsElement(target: HTMLElement, parent?: HTMLElement): boolean { +export function portalContainsElement(target: HTMLElement, parent?: HTMLElement, doc?: Document): boolean { const elementMatch = findElementRecursive( target, (testElement: HTMLElement) => parent === testElement || testElement.hasAttribute(DATA_PORTAL_ATTRIBUTE), + doc, ); return elementMatch !== null && elementMatch.hasAttribute(DATA_PORTAL_ATTRIBUTE); } diff --git a/packages/eslint-plugin/src/configs/react-legacy.js b/packages/eslint-plugin/src/configs/react-legacy.js index 108f202b0d9a7e..b267a6610b4083 100644 --- a/packages/eslint-plugin/src/configs/react-legacy.js +++ b/packages/eslint-plugin/src/configs/react-legacy.js @@ -1,6 +1,7 @@ // @ts-check - +const configHelpers = require('../utils/configHelpers'); const path = require('path'); +const { reactLegacy: restrictedGlobals } = require('./restricted-globals'); /** @type {import("eslint").Linter.Config} */ module.exports = { @@ -9,7 +10,16 @@ module.exports = { rules: { 'jsdoc/check-tag-names': 'off', '@griffel/no-shorthands': 'off', - 'no-restricted-globals': 'off', + 'no-restricted-globals': restrictedGlobals, }, - overrides: [], + overrides: [ + { + // Test overrides + files: [...configHelpers.testFiles, '**/*.stories.tsx'], + rules: { + 'no-restricted-globals': 'off', + 'react/jsx-no-bind': 'off', + }, + }, + ], }; diff --git a/packages/eslint-plugin/src/configs/restricted-globals.js b/packages/eslint-plugin/src/configs/restricted-globals.js index b8870e2883ecc0..5cc7632b4002cf 100644 --- a/packages/eslint-plugin/src/configs/restricted-globals.js +++ b/packages/eslint-plugin/src/configs/restricted-globals.js @@ -239,6 +239,18 @@ // 'onwheel', // ]; +const reactLegacy = [ + 'error', + { + name: 'window', + message: 'Get a reference to `window` via context.', + }, + { + name: 'document', + message: 'Get a reference to `document` via context.', + }, +]; + const react = [ 'error', { @@ -258,5 +270,6 @@ const react = [ ]; module.exports = { + reactLegacy, react, }; diff --git a/packages/merge-styles/src/StyleOptionsState.ts b/packages/merge-styles/src/StyleOptionsState.ts index c59ad4bba7ca9c..d1eda55135b622 100644 --- a/packages/merge-styles/src/StyleOptionsState.ts +++ b/packages/merge-styles/src/StyleOptionsState.ts @@ -15,8 +15,11 @@ export function setRTL(isRTL: boolean): void { export function getRTL(): boolean { if (_rtl === undefined) { _rtl = + // eslint-disable-next-line no-restricted-globals typeof document !== 'undefined' && + // eslint-disable-next-line no-restricted-globals !!document.documentElement && + // eslint-disable-next-line no-restricted-globals document.documentElement.getAttribute('dir') === 'rtl'; } return _rtl; diff --git a/packages/merge-styles/src/Stylesheet.ts b/packages/merge-styles/src/Stylesheet.ts index 3be6223b3d3119..d0702e2b4dea72 100644 --- a/packages/merge-styles/src/Stylesheet.ts +++ b/packages/merge-styles/src/Stylesheet.ts @@ -1,3 +1,6 @@ +/* eslint no-restricted-globals: 0 */ +// globals in stylesheets will be addressed as part of shadow DOM work. +// See: https://github.com/microsoft/fluentui/issues/28058 import { IStyle } from './IStyle'; export const InjectionMode = { diff --git a/packages/merge-styles/src/getVendorSettings.ts b/packages/merge-styles/src/getVendorSettings.ts index dfdcbdba55797b..41ba34307a3981 100644 --- a/packages/merge-styles/src/getVendorSettings.ts +++ b/packages/merge-styles/src/getVendorSettings.ts @@ -9,6 +9,7 @@ let _vendorSettings: IVendorSettings | undefined; export function getVendorSettings(): IVendorSettings { if (!_vendorSettings) { + // eslint-disable-next-line no-restricted-globals const doc = typeof document !== 'undefined' ? document : undefined; const nav = typeof navigator !== 'undefined' ? navigator : undefined; const userAgent = nav?.userAgent?.toLowerCase(); diff --git a/packages/react-charting/.eslintrc.json b/packages/react-charting/.eslintrc.json index 20576f82da4223..478004905dea85 100644 --- a/packages/react-charting/.eslintrc.json +++ b/packages/react-charting/.eslintrc.json @@ -1,4 +1,7 @@ { "extends": ["plugin:@fluentui/eslint-plugin/react--legacy"], - "root": true + "root": true, + "rules": { + "no-restricted-globals": "off" + } } diff --git a/packages/react-docsite-components/.eslintrc.json b/packages/react-docsite-components/.eslintrc.json index a6ecfa7038e6be..ec2a316fd41b8f 100644 --- a/packages/react-docsite-components/.eslintrc.json +++ b/packages/react-docsite-components/.eslintrc.json @@ -2,6 +2,7 @@ "extends": ["plugin:@fluentui/eslint-plugin/react--legacy"], "root": true, "rules": { - "deprecation/deprecation": "off" + "deprecation/deprecation": "off", + "no-restricted-globals": "off" } } diff --git a/packages/react-examples/.eslintrc.json b/packages/react-examples/.eslintrc.json index d4c9b90997c5ee..4a59b1205ddc66 100644 --- a/packages/react-examples/.eslintrc.json +++ b/packages/react-examples/.eslintrc.json @@ -4,6 +4,7 @@ "rules": { "import/no-webpack-loader-syntax": "off", "@typescript-eslint/no-explicit-any": "off", - "no-alert": "off" + "no-alert": "off", + "no-restricted-globals": "off" } } diff --git a/packages/react-experiments/.eslintrc.json b/packages/react-experiments/.eslintrc.json index 10a55ade409374..94cce37333d4c9 100644 --- a/packages/react-experiments/.eslintrc.json +++ b/packages/react-experiments/.eslintrc.json @@ -2,6 +2,7 @@ "extends": ["plugin:@fluentui/eslint-plugin/react--legacy"], "root": true, "rules": { - "@typescript-eslint/no-explicit-any": "off" + "@typescript-eslint/no-explicit-any": "off", + "no-restricted-globals": "off" } } diff --git a/packages/react-file-type-icons/src/getFileTypeIconProps.ts b/packages/react-file-type-icons/src/getFileTypeIconProps.ts index 83943838385ac1..2bf8d49d07809e 100644 --- a/packages/react-file-type-icons/src/getFileTypeIconProps.ts +++ b/packages/react-file-type-icons/src/getFileTypeIconProps.ts @@ -160,8 +160,14 @@ export function getFileTypeIconNameFromExtensionOrType( return iconBaseName || GENERIC_FILE; } -export function getFileTypeIconSuffix(size: FileTypeIconSize, imageFileType: ImageFileType = 'svg'): string { - let devicePixelRatio: number = window.devicePixelRatio; +export function getFileTypeIconSuffix( + size: FileTypeIconSize, + imageFileType: ImageFileType = 'svg', + win?: Window, +): string { + // eslint-disable-next-line no-restricted-globals + win ??= window; + let devicePixelRatio: number = win.devicePixelRatio; let devicePixelRatioSuffix = ''; // Default is 1x // SVGs scale well, so you can generally use the default image. diff --git a/packages/react-focus/src/components/FocusZone/FocusZone.tsx b/packages/react-focus/src/components/FocusZone/FocusZone.tsx index a8ae33009abcc4..9d1f1f5e6057d7 100644 --- a/packages/react-focus/src/components/FocusZone/FocusZone.tsx +++ b/packages/react-focus/src/components/FocusZone/FocusZone.tsx @@ -59,12 +59,14 @@ function raiseClickFromKeyboardEvent(target: Element, ev?: React.KeyboardEvent> extends React_2.Compon // (undocumented) componentWillUnmount(): void; // (undocumented) + static contextType: React_2.Context; + // (undocumented) protected currentPromise: PromiseLike | undefined; // (undocumented) dismissSuggestions: (ev?: any) => void; @@ -797,6 +799,8 @@ export class BaseSelectedItemsList> // (undocumented) componentDidUpdate(oldProps: P, oldState: IBaseSelectedItemsListState): void; // (undocumented) + static contextType: React_2.Context; + // (undocumented) protected copyItems(items: T[]): void; // (undocumented) static getDerivedStateFromProps(newProps: IBaseSelectedItemsListProps): { @@ -1173,7 +1177,7 @@ export { createTheme } export { css } // @public -export function cssColor(color?: string): IRGB | undefined; +export function cssColor(color?: string, doc?: Document): IRGB | undefined; export { customizable } @@ -1306,6 +1310,8 @@ export class DetailsListBase extends React_2.Component; + // (undocumented) static defaultProps: { layoutMode: DetailsListLayoutMode; selectionMode: SelectionMode_2; @@ -1746,7 +1752,7 @@ export function getColorFromHSV(hsv: IHSV, a?: number): IColor; export function getColorFromRGBA(rgba: IRGB): IColor; // @public -export function getColorFromString(inputColor: string): IColor | undefined; +export function getColorFromString(inputColor: string, doc?: Document): IColor | undefined; // @public (undocumented) export const getCommandBarStyles: (props: ICommandBarStyleProps) => ICommandBarStyles; @@ -1856,7 +1862,7 @@ export function getLayerHostSelector(): string | undefined; export const getLayerStyles: (props: ILayerStyleProps) => ILayerStyles; // @public -export function getMaxHeight(target: Element | MouseEvent | Point | Rectangle, targetEdge: DirectionalHint, gapSpace?: number, bounds?: IRectangle, coverTarget?: boolean): number; +export function getMaxHeight(target: Element | MouseEvent | Point | Rectangle, targetEdge: DirectionalHint, gapSpace?: number, bounds?: IRectangle, coverTarget?: boolean, win?: Window): number; // @public export const getMeasurementCache: () => { @@ -8126,6 +8132,8 @@ export interface IScrollablePaneContext { notifySubscribers: (sort?: boolean) => void; syncScrollSticky: (sticky: Sticky) => void; }; + // (undocumented) + window: Window | undefined; } // @public (undocumented) @@ -9772,6 +9780,8 @@ export class KeytipLayerBase extends React_2.Component; + // (undocumented) static defaultProps: IKeytipLayerProps; // (undocumented) getCurrentSequence(): string; @@ -9880,6 +9890,8 @@ export class List extends React_2.Component, IListState; + // (undocumented) static defaultProps: { startIndex: number; onRenderCell: (item: any, index: number, containsFocus: boolean) => JSX.Element; @@ -10043,6 +10055,8 @@ export const Nav: React_2.FunctionComponent; export class NavBase extends React_2.Component implements INav { constructor(props: INavProps); // (undocumented) + static contextType: React_2.Context; + // (undocumented) static defaultProps: INavProps; focus(forceIntoFirstElement?: boolean): boolean; // (undocumented) @@ -10135,6 +10149,8 @@ export class PanelBase extends React_2.Component imple // (undocumented) componentWillUnmount(): void; // (undocumented) + static contextType: React_2.Context; + // (undocumented) static defaultProps: IPanelProps; // (undocumented) dismiss: (ev?: React_2.SyntheticEvent | KeyboardEvent) => void; @@ -10398,13 +10414,13 @@ export enum Position { } // @public (undocumented) -export function positionCallout(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo, shouldScroll?: boolean, minimumScrollResizeHeight?: number): ICalloutPositionedInfo; +export function positionCallout(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo, shouldScroll?: boolean, minimumScrollResizeHeight?: number, win?: Window): ICalloutPositionedInfo; // @public (undocumented) -export function positionCard(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo): ICalloutPositionedInfo; +export function positionCard(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo, win?: Window): ICalloutPositionedInfo; // @public -export function positionElement(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: IPositionedData): IPositionedData; +export function positionElement(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: IPositionedData, win?: Window): IPositionedData; // @public (undocumented) export const PositioningContainer: React_2.FunctionComponent; @@ -10585,6 +10601,8 @@ export class ScrollablePaneBase extends React_2.Component; + // (undocumented) forceLayoutUpdate(): void; // (undocumented) getScrollPosition: () => number; @@ -11351,6 +11369,8 @@ export class TooltipHostBase extends React_2.Component; + // (undocumented) static defaultProps: { delay: TooltipDelay; }; diff --git a/packages/react/src/components/Callout/CalloutContent.base.tsx b/packages/react/src/components/Callout/CalloutContent.base.tsx index 4357e0aa68542e..2240614960778d 100644 --- a/packages/react/src/components/Callout/CalloutContent.base.tsx +++ b/packages/react/src/components/Callout/CalloutContent.base.tsx @@ -21,7 +21,7 @@ import type { ICalloutProps, ICalloutContentStyleProps, ICalloutContentStyles } import type { Point, IRectangle } from '../../Utilities'; import type { ICalloutPositionedInfo, IPositionProps, IPosition } from '../../Positioning'; import type { Target } from '@fluentui/react-hooks'; -import { useWindow } from '@fluentui/react-window-provider'; +import { useWindowEx } from '../../utilities/dom'; const COMPONENT_NAME = 'CalloutContentBase'; @@ -208,7 +208,7 @@ function usePositions( preferScrollResizePositioning, } = props; - const win = useWindow(); + const win = useWindowEx(); const localRef = React.useRef(); let popupStyles: CSSStyleDeclaration | undefined; if (localRef.current !== popupRef.current) { @@ -243,8 +243,16 @@ function usePositions( // If there is a finalHeight given then we assume that the user knows and will handle // additional positioning adjustments so we should call positionCard const newPositions: ICalloutPositionedInfo = finalHeight - ? positionCard(currentProps, hostElement.current, dupeCalloutElement, previousPositions) - : positionCallout(currentProps, hostElement.current, dupeCalloutElement, previousPositions, shouldScroll); + ? positionCard(currentProps, hostElement.current, dupeCalloutElement, previousPositions, win) + : positionCallout( + currentProps, + hostElement.current, + dupeCalloutElement, + previousPositions, + shouldScroll, + undefined, + win, + ); // clean up duplicate calloutElement calloutElement.parentElement?.removeChild(dupeCalloutElement); @@ -295,6 +303,7 @@ function usePositions( hideOverflow, preferScrollResizePositioning, popupOverflowY, + win, ]); return positions; diff --git a/packages/react/src/components/ChoiceGroup/ChoiceGroup.base.tsx b/packages/react/src/components/ChoiceGroup/ChoiceGroup.base.tsx index 32d982585eee84..8dfa8dccca951d 100644 --- a/packages/react/src/components/ChoiceGroup/ChoiceGroup.base.tsx +++ b/packages/react/src/components/ChoiceGroup/ChoiceGroup.base.tsx @@ -20,6 +20,7 @@ import type { IChoiceGroup, } from './ChoiceGroup.types'; import type { IChoiceGroupOptionProps } from './ChoiceGroupOption/ChoiceGroupOption.types'; +import { useDocumentEx } from '../../utilities/dom'; const getClassNames = classNamesFunction(); @@ -36,9 +37,10 @@ const focusSelectedOption = ( keyChecked: IChoiceGroupProps['selectedKey'], id: string, focusProviders?: React.RefObject[], + doc?: Document, ) => { const optionToFocus = findOption(options, keyChecked) || options.filter(option => !option.disabled)[0]; - const elementToFocus = optionToFocus && document.getElementById(getOptionId(optionToFocus, id)); + const elementToFocus = optionToFocus && doc?.getElementById(getOptionId(optionToFocus, id)); if (elementToFocus) { elementToFocus.focus(); @@ -57,6 +59,7 @@ const useComponentRef = ( componentRef?: IRefObject, focusProviders?: React.RefObject[], ) => { + const doc = useDocumentEx(); React.useImperativeHandle( componentRef, () => ({ @@ -64,10 +67,10 @@ const useComponentRef = ( return findOption(options, keyChecked); }, focus() { - focusSelectedOption(options, keyChecked, id, focusProviders); + focusSelectedOption(options, keyChecked, id, focusProviders, doc); }, }), - [options, keyChecked, id, focusProviders], + [options, keyChecked, id, focusProviders, doc], ); }; diff --git a/packages/react/src/components/Coachmark/Coachmark.base.tsx b/packages/react/src/components/Coachmark/Coachmark.base.tsx index ce307961c3c059..bd685fb6ebba3e 100644 --- a/packages/react/src/components/Coachmark/Coachmark.base.tsx +++ b/packages/react/src/components/Coachmark/Coachmark.base.tsx @@ -26,6 +26,7 @@ import type { IPositionedData } from '../../Positioning'; import type { IPositioningContainerProps } from './PositioningContainer/PositioningContainer.types'; import type { ICoachmarkProps, ICoachmarkStyles, ICoachmarkStyleProps } from './Coachmark.types'; import type { IBeakProps } from './Beak/Beak.types'; +import { useDocumentEx, useWindowEx } from '../../utilities/dom'; const getClassNames = classNamesFunction(); @@ -253,6 +254,8 @@ function useProximityHandlers( /** The target element the mouse would be in proximity to */ const targetElementRect = React.useRef(); + const win = useWindowEx(); + const doc = useDocumentEx(); React.useEffect(() => { const setTargetElementRect = (): void => { @@ -277,7 +280,7 @@ function useProximityHandlers( // When the window resizes we want to async get the bounding client rectangle. // Every time the event is triggered we want to setTimeout and then clear any previous // instances of setTimeout. - events.on(window, 'resize', (): void => { + events.on(win, 'resize', (): void => { timeoutIds.forEach((value: number): void => { clearTimeout(value); }); @@ -286,7 +289,7 @@ function useProximityHandlers( timeoutIds.push( setTimeout((): void => { setTargetElementRect(); - setBounds(getBounds(props.isPositionForced, props.positioningContainerProps)); + setBounds(getBounds(props.isPositionForced, props.positioningContainerProps, win)); }, 100), ); }); @@ -294,7 +297,7 @@ function useProximityHandlers( // Every time the document's mouse move is triggered, we want to check if inside of an element // and set the state with the result. - events.on(document, 'mousemove', (e: MouseEvent) => { + events.on(doc, 'mousemove', (e: MouseEvent) => { const mouseY = e.clientY; const mouseX = e.clientX; setTargetElementRect(); @@ -412,6 +415,7 @@ export const CoachmarkBase: React.FunctionComponent = React.for >((propsWithoutDefaults, forwardedRef) => { const props = getPropsWithDefaults(DEFAULT_PROPS, propsWithoutDefaults); + const win = useWindowEx(); const entityInnerHostElementRef = React.useRef(null); const translateAnimationContainer = React.useRef(null); @@ -420,7 +424,7 @@ export const CoachmarkBase: React.FunctionComponent = React.for const [beakPositioningProps, transformOrigin] = useBeakPosition(props, targetAlignment, targetPosition); const [isMeasuring, entityInnerHostRect] = useEntityHostMeasurements(props, entityInnerHostElementRef); const [bounds, setBounds] = React.useState( - getBounds(props.isPositionForced, props.positioningContainerProps), + getBounds(props.isPositionForced, props.positioningContainerProps, win), ); const alertText = useAriaAlert(props); const entityHost = useAutoFocus(props); @@ -431,8 +435,8 @@ export const CoachmarkBase: React.FunctionComponent = React.for useDeprecationWarning(props); React.useEffect(() => { - setBounds(getBounds(props.isPositionForced, props.positioningContainerProps)); - }, [props.isPositionForced, props.positioningContainerProps]); + setBounds(getBounds(props.isPositionForced, props.positioningContainerProps, win)); + }, [props.isPositionForced, props.positioningContainerProps, win]); const { beaconColorOne, @@ -539,6 +543,7 @@ CoachmarkBase.displayName = COMPONENT_NAME; function getBounds( isPositionForced?: boolean, positioningContainerProps?: IPositioningContainerProps, + win?: Window, ): IRectangle | undefined { if (isPositionForced) { // If directionalHint direction is the top or bottom auto edge, then we want to set the left/right bounds @@ -552,8 +557,8 @@ function getBounds( left: 0, top: -Infinity, bottom: Infinity, - right: window.innerWidth, - width: window.innerWidth, + right: win?.innerWidth ?? 0, + width: win?.innerWidth ?? 0, height: Infinity, }; } else { diff --git a/packages/react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx b/packages/react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx index 0bce232488344d..759f6990affde2 100644 --- a/packages/react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx +++ b/packages/react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx @@ -14,6 +14,7 @@ import { useMergedRefs, useAsync, useTarget } from '@fluentui/react-hooks'; import type { IPositioningContainerProps } from './PositioningContainer.types'; import type { Point, IRectangle } from '../../../Utilities'; import type { IPositionedData, IPositionProps, IPosition } from '../../../Positioning'; +import { useDocumentEx, useWindowEx } from '../../../utilities/dom'; const OFF_SCREEN_STYLE = { opacity: 0 }; @@ -64,6 +65,8 @@ function usePositionState( getCachedBounds: () => IRectangle, ) { const async = useAsync(); + const doc = useDocumentEx(); + const win = useWindowEx(); /** * Current set of calculated positions for the outermost parent container. */ @@ -90,13 +93,15 @@ function usePositionState( // or don't check anything else if the target is a Point or Rectangle if ( (!(target as Element).getBoundingClientRect && !(target as MouseEvent).preventDefault) || - document.body.contains(target as Node) + doc?.body.contains(target as Node) ) { currentProps!.gapSpace = offsetFromTarget; const newPositions: IPositionedData = positionElement( currentProps!, hostElement, positioningContainerElement, + undefined, + win, ); // Set the new position only when the positions are not exists or one of the new positioningContainer // positions are different. The position should not change if the position is within 2 decimal places. @@ -152,6 +157,7 @@ function useMaxHeight( * without going beyond the window or target bounds */ const maxHeight = React.useRef(); + const win = useWindowEx(); // If the target element changed, reset the max height. If we are tracking // target with class name, always reset because we do not know if @@ -171,7 +177,14 @@ function useMaxHeight( if (!maxHeight.current) { if (directionalHintFixed && targetRef.current) { const gapSpace = offsetFromTarget ? offsetFromTarget : 0; - maxHeight.current = getMaxHeight(targetRef.current, directionalHint!, gapSpace, getCachedBounds()); + maxHeight.current = getMaxHeight( + targetRef.current, + directionalHint!, + gapSpace, + getCachedBounds(), + undefined, + win, + ); } else { maxHeight.current = getCachedBounds().height! - BORDER_WIDTH * 2; } diff --git a/packages/react/src/components/ColorPicker/ColorRectangle/ColorRectangle.base.tsx b/packages/react/src/components/ColorPicker/ColorRectangle/ColorRectangle.base.tsx index 1666f4076356e6..d8ef8bbed0d9c0 100644 --- a/packages/react/src/components/ColorPicker/ColorRectangle/ColorRectangle.base.tsx +++ b/packages/react/src/components/ColorPicker/ColorRectangle/ColorRectangle.base.tsx @@ -5,6 +5,8 @@ import { MAX_COLOR_SATURATION, MAX_COLOR_VALUE } from '../../../utilities/color/ import { getFullColorString } from '../../../utilities/color/getFullColorString'; import { updateSV } from '../../../utilities/color/updateSV'; import { clamp } from '../../../utilities/color/clamp'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getWindowEx } from '../../../utilities/dom'; import type { IColorRectangleProps, IColorRectangleStyleProps, @@ -26,6 +28,7 @@ export class ColorRectangleBase extends React.Component implements IColorRectangle { + public static contextType = WindowContext; public static defaultProps: Partial = { minSize: 220, ariaLabel: 'Saturation and brightness', @@ -183,9 +186,10 @@ export class ColorRectangleBase } private _onMouseDown = (ev: React.MouseEvent): void => { + const win = getWindowEx(this.context)!; // Can only be called on the client this._disposables.push( - on(window, 'mousemove', this._onMouseMove as (ev: MouseEvent) => void, true), - on(window, 'mouseup', this._disposeListeners, true), + on(win, 'mousemove', this._onMouseMove as (ev: MouseEvent) => void, true), + on(win, 'mouseup', this._disposeListeners, true), ); this._onMouseMove(ev); diff --git a/packages/react/src/components/ComboBox/ComboBox.tsx b/packages/react/src/components/ComboBox/ComboBox.tsx index 255f1533e4de49..d4efab6f0de0f0 100644 --- a/packages/react/src/components/ComboBox/ComboBox.tsx +++ b/packages/react/src/components/ComboBox/ComboBox.tsx @@ -41,6 +41,8 @@ import type { import type { IButtonStyles } from '../../Button'; import type { ICalloutProps } from '../../Callout'; import { getChildren } from '@fluentui/utilities'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getDocumentEx } from '../../utilities/dom'; export interface IComboBoxState { /** The open state */ @@ -243,6 +245,8 @@ function findFirstDescendant(element: HTMLElement, match: (element: HTMLElement) @customizable('ComboBox', ['theme', 'styles'], true) class ComboBoxInternal extends React.Component implements IComboBox { + public static contextType = WindowContext; + /** The input aspect of the combo box */ private _autofill = React.createRef(); @@ -368,6 +372,7 @@ class ComboBoxInternal extends React.Component this._scrollIntoView(), 0); } + const doc = getDocumentEx(this.context); // if an action is taken that put focus in the ComboBox // and If we are open or we are just closed, shouldFocusAfterClose is set, // but we are not the activeElement set focus on the input @@ -378,7 +383,7 @@ class ComboBoxInternal extends React.Component): void => { + const doc = getDocumentEx(this.context); // Do nothing if the blur is coming from something // inside the comboBox root or the comboBox menu since // it we are not really blurring from the whole comboBox @@ -1231,7 +1237,7 @@ class ComboBoxInternal extends React.Component element === relatedTarget); + findElementRecursive(this._comboBoxMenu.current, (element: HTMLElement) => element === relatedTarget, doc); if (isBlurFromComboBoxTitle || isBlurFromComboBoxMenu || isBlurFromComboBoxMenuAncestor) { if ( diff --git a/packages/react/src/components/DetailsList/DetailsList.base.tsx b/packages/react/src/components/DetailsList/DetailsList.base.tsx index 5bab87d397cddf..71173295bba17d 100644 --- a/packages/react/src/components/DetailsList/DetailsList.base.tsx +++ b/packages/react/src/components/DetailsList/DetailsList.base.tsx @@ -57,6 +57,8 @@ import type { IFocusZone, IFocusZoneProps } from '../../FocusZone'; import type { IObjectWithKey, ISelection } from '../../Selection'; import type { IGroupedList, IGroupDividerProps, IGroupRenderProps, IGroup } from '../../GroupedList'; import type { IListProps } from '../../List'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getDocumentEx } from '../../utilities/dom'; const getClassNames = classNamesFunction(); const COMPONENT_NAME = 'DetailsList'; @@ -779,6 +781,8 @@ export class DetailsListBase extends React.Component(); @@ -934,6 +938,8 @@ export class DetailsListBase extends React.Component 0 && this.state.focusedItemIndex !== -1 && - !elementContains(this._root.current, document.activeElement as HTMLElement, false) + !elementContains(this._root.current, doc?.activeElement as HTMLElement, false) ) { // Item set has changed and previously-focused item is gone. // Set focus to item at index of previously-focused item if it is in range, diff --git a/packages/react/src/components/DocumentCard/DocumentCard.base.tsx b/packages/react/src/components/DocumentCard/DocumentCard.base.tsx index cac8816f5241e0..78bbf79439ba1e 100644 --- a/packages/react/src/components/DocumentCard/DocumentCard.base.tsx +++ b/packages/react/src/components/DocumentCard/DocumentCard.base.tsx @@ -16,6 +16,8 @@ import type { IDocumentCardStyleProps, IDocumentCardStyles, } from './DocumentCard.types'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getWindowEx } from '../../utilities/dom'; const getClassNames = classNamesFunction(); @@ -31,6 +33,8 @@ export class DocumentCardBase extends React.Component i type: DocumentCardType.normal, }; + public static contextType = WindowContext; + private _rootElement = React.createRef(); private _classNames: IProcessedStyleSet; @@ -109,14 +113,16 @@ export class DocumentCardBase extends React.Component i private _onAction = (ev: React.SyntheticEvent): void => { const { onClick, onClickHref, onClickTarget } = this.props; + const win = getWindowEx(this.context)!; // can only be called on the client + if (onClick) { onClick(ev); } else if (!onClick && onClickHref) { // If no onClick Function was provided and we do have an onClickHref, redirect to the onClickHref if (onClickTarget) { - window.open(onClickHref, onClickTarget, 'noreferrer noopener nofollow'); + win.open(onClickHref, onClickTarget, 'noreferrer noopener nofollow'); } else { - window.location.href = onClickHref; + win.location.href = onClickHref; } ev.preventDefault(); diff --git a/packages/react/src/components/DocumentCard/DocumentCardTitle.base.tsx b/packages/react/src/components/DocumentCard/DocumentCardTitle.base.tsx index 0dbaa541322973..3abe764a832b77 100644 --- a/packages/react/src/components/DocumentCard/DocumentCardTitle.base.tsx +++ b/packages/react/src/components/DocumentCard/DocumentCardTitle.base.tsx @@ -9,6 +9,8 @@ import type { } from './DocumentCardTitle.types'; import type { IProcessedStyleSet } from '../../Styling'; import { DocumentCardContext } from './DocumentCard.base'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getWindowEx } from '../../utilities/dom'; const getClassNames = classNamesFunction(); @@ -23,6 +25,8 @@ const TRUNCATION_VERTICAL_OVERFLOW_THRESHOLD = 5; * {@docCategory DocumentCard} */ export class DocumentCardTitleBase extends React.Component { + public static contextType = WindowContext; + private _titleElement = React.createRef(); private _classNames: IProcessedStyleSet; private _async: Async; @@ -53,12 +57,13 @@ export class DocumentCardTitleBase extends React.Component { @@ -71,7 +76,8 @@ export class DocumentCardTitleBase extends React.Component(); @@ -187,6 +189,8 @@ class DropdownInternal extends React.Component(); private _focusZone = React.createRef(); private _dropDown = React.createRef(); @@ -937,14 +941,15 @@ class DropdownInternal extends React.Component { + const win = getWindowEx(this.context)!; // can only be called on the client if (!this._isScrollIdle && this._scrollIdleTimeoutId !== undefined) { - clearTimeout(this._scrollIdleTimeoutId); + win.clearTimeout(this._scrollIdleTimeoutId); this._scrollIdleTimeoutId = undefined; } else { this._isScrollIdle = false; } - this._scrollIdleTimeoutId = window.setTimeout(() => { + this._scrollIdleTimeoutId = win.setTimeout(() => { this._isScrollIdle = true; }, this._scrollIdleDelay); }; @@ -959,10 +964,11 @@ class DropdownInternal extends React.Component): void { + const doc = getDocumentEx(this.context)!; // can only be called on the client const targetElement = ev.currentTarget as HTMLElement; this._gotMouseMove = true; - if (!this._isScrollIdle || document.activeElement === targetElement) { + if (!this._isScrollIdle || doc.activeElement === targetElement) { return; } diff --git a/packages/react/src/components/FocusTrapZone/FocusTrapZone.tsx b/packages/react/src/components/FocusTrapZone/FocusTrapZone.tsx index b4b6c6e9a75ef6..1e64090bae5a24 100644 --- a/packages/react/src/components/FocusTrapZone/FocusTrapZone.tsx +++ b/packages/react/src/components/FocusTrapZone/FocusTrapZone.tsx @@ -15,6 +15,7 @@ import { useId, useConst, useMergedRefs, useEventCallback, usePrevious, useUnmou import { useDocument } from '../../WindowProvider'; import type { IRefObject } from '../../Utilities'; import type { IFocusTrapZoneProps, IFocusTrapZone } from './FocusTrapZone.types'; +import { useWindowEx } from '../../utilities/dom'; interface IFocusTrapZoneInternalState { previouslyFocusedElementInTrapZone?: HTMLElement; @@ -62,6 +63,7 @@ export const FocusTrapZone: React.FunctionComponent & { const lastBumper = React.useRef(null); const mergedRootRef = useMergedRefs(root, ref) as React.Ref; const doc = useDocument(); + const win = useWindowEx()!; const isFirstRender = usePrevious(false) ?? true; @@ -263,17 +265,17 @@ export const FocusTrapZone: React.FunctionComponent & { const disposables: Array<() => void> = []; if (forceFocusInsideTrap) { - disposables.push(on(window, 'focus', forceFocusOrClickInTrap, true)); + disposables.push(on(win, 'focus', forceFocusOrClickInTrap, true)); } if (!isClickableOutsideFocusTrap) { - disposables.push(on(window, 'click', forceFocusOrClickInTrap, true)); + disposables.push(on(win, 'click', forceFocusOrClickInTrap, true)); } return () => { disposables.forEach(dispose => dispose()); }; // eslint-disable-next-line react-hooks/exhaustive-deps -- should only run when these two props change - }, [forceFocusInsideTrap, isClickableOutsideFocusTrap]); + }, [forceFocusInsideTrap, isClickableOutsideFocusTrap, win]); // On prop change or first render, focus the FTZ and update focusStack if appropriate React.useEffect(() => { diff --git a/packages/react/src/components/KeytipLayer/KeytipLayer.base.tsx b/packages/react/src/components/KeytipLayer/KeytipLayer.base.tsx index 6f539afcfd1d7b..339bf05435512f 100644 --- a/packages/react/src/components/KeytipLayer/KeytipLayer.base.tsx +++ b/packages/react/src/components/KeytipLayer/KeytipLayer.base.tsx @@ -28,6 +28,8 @@ import type { IKeytipLayerProps, IKeytipLayerStyles, IKeytipLayerStyleProps } fr import type { IKeytipProps } from '../../Keytip'; import type { IKeytipTreeNode } from './IKeytipTreeNode'; import type { KeytipTransitionModifier, IKeytipTransitionKey } from '../../utilities/keytips/IKeytipTransitionKey'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getDocumentEx, getWindowEx } from '../../utilities/dom'; export interface IKeytipLayerState { inKeytipMode: boolean; @@ -63,6 +65,8 @@ export class KeytipLayerBase extends React.Component { + const doc = getDocumentEx(this.context); + const win = getWindowEx(this.context); const targetSelector = ktpTargetFromSequences(keySequences); - const matchingElements = document.querySelectorAll(targetSelector); + const matchingElements = doc?.querySelectorAll(targetSelector) ?? []; // If there are multiple elements for the keytip sequence, return true if the element instance // that corresponds to the keytip instance is visible, otherwise return if there is only one instance return matchingElements.length > 1 && instanceCount <= matchingElements.length - ? isElementVisibleAndNotHidden(matchingElements[instanceCount - 1] as HTMLElement) + ? isElementVisibleAndNotHidden(matchingElements[instanceCount - 1] as HTMLElement, win ?? undefined) : instanceCount === 1; }; diff --git a/packages/react/src/components/KeytipLayer/KeytipTree.ts b/packages/react/src/components/KeytipLayer/KeytipTree.ts index 42d8f922df505f..e1268041db4466 100644 --- a/packages/react/src/components/KeytipLayer/KeytipTree.ts +++ b/packages/react/src/components/KeytipLayer/KeytipTree.ts @@ -1,4 +1,4 @@ -import { find, isElementVisibleAndNotHidden, values } from '../../Utilities'; +import { find, getDocument, isElementVisibleAndNotHidden, values } from '../../Utilities'; import { ktpTargetFromSequences, mergeOverflows, sequencesToID } from '../../utilities/keytips/KeytipUtils'; import { KTP_LAYER_ID } from '../../utilities/keytips/KeytipConstants'; import type { IKeytipProps } from '../../Keytip'; @@ -122,9 +122,15 @@ export class KeytipTree { * * @param keySequence - string to match * @param currentKeytip - The keytip whose children will try to match + * @param doc - The document for DOM operations * @returns The node that exactly matched the keySequence, or undefined if none matched */ - public getExactMatchedNode(keySequence: string, currentKeytip: IKeytipTreeNode): IKeytipTreeNode | undefined { + public getExactMatchedNode( + keySequence: string, + currentKeytip: IKeytipTreeNode, + doc?: Document, + ): IKeytipTreeNode | undefined { + const theDoc = doc ?? getDocument()!; const possibleNodes = this.getNodes(currentKeytip.children); const matchingNodes = possibleNodes.filter((node: IKeytipTreeNode) => { return this._getNodeSequence(node) === keySequence && !node.disabled; @@ -149,7 +155,7 @@ export class KeytipTree { const overflowSetSequence = node.overflowSetSequence; const fullKeySequences = overflowSetSequence ? mergeOverflows(keySequences, overflowSetSequence) : keySequences; const keytipTargetSelector = ktpTargetFromSequences(fullKeySequences); - const potentialTargetElements = document.querySelectorAll(keytipTargetSelector); + const potentialTargetElements = theDoc.querySelectorAll(keytipTargetSelector); // If we have less nodes than the potential target elements, // we won't be able to map element to node, return the first node. @@ -161,7 +167,7 @@ export class KeytipTree { // Attempt to find the node that corresponds to the first visible/non-hidden element const matchingIndex = Array.from(potentialTargetElements).findIndex((element: HTMLElement) => - isElementVisibleAndNotHidden(element), + isElementVisibleAndNotHidden(element, theDoc.defaultView ?? undefined), ); if (matchingIndex !== -1) { return matchingNodes[matchingIndex]; diff --git a/packages/react/src/components/List/List.tsx b/packages/react/src/components/List/List.tsx index 6efe0e51e38dc4..612058540ae64e 100644 --- a/packages/react/src/components/List/List.tsx +++ b/packages/react/src/components/List/List.tsx @@ -24,6 +24,8 @@ import type { IListOnRenderSurfaceProps, IListOnRenderRootProps, } from './List.types'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getWindowEx } from '../../utilities/dom'; const RESIZE_DELAY = 16; const MIN_SCROLL_UPDATE_DELAY = 100; @@ -105,6 +107,8 @@ export class List extends React.Component, IListState> renderedWindowsBehind: DEFAULT_RENDERED_WINDOWS_BEHIND, }; + public static contextType = WindowContext; + private _root = React.createRef(); private _surface = React.createRef(); private _pageRefs: Record = {}; @@ -347,7 +351,9 @@ export class List extends React.Component, IListState> this.setState({ ...this._updatePages(this.props, this.state), hasMounted: true }); this._measureVersion++; - this._events.on(window, 'resize', this._onAsyncResizeDebounced); + const win = getWindowEx(this.context); + + this._events.on(win, 'resize', this._onAsyncResizeDebounced); if (this._root.current) { this._events.on(this._root.current, 'focus', this._onFocus, true); } diff --git a/packages/react/src/components/MarqueeSelection/MarqueeSelection.base.tsx b/packages/react/src/components/MarqueeSelection/MarqueeSelection.base.tsx index 79de834f3dedd8..365e513b5cb55e 100644 --- a/packages/react/src/components/MarqueeSelection/MarqueeSelection.base.tsx +++ b/packages/react/src/components/MarqueeSelection/MarqueeSelection.base.tsx @@ -16,6 +16,8 @@ import type { IMarqueeSelectionStyleProps, IMarqueeSelectionStyles, } from './MarqueeSelection.types'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getDocumentEx, getWindowEx } from '../../utilities/dom'; const getClassNames = classNamesFunction(); @@ -41,6 +43,8 @@ export class MarqueeSelectionBase extends React.Component(); @@ -71,8 +75,11 @@ export class MarqueeSelectionBase extends React.Component implements IN groups: null, }; + public static contextType = WindowContext; + private _focusZone = React.createRef(); constructor(props: INavProps) { super(props); @@ -360,8 +364,9 @@ export class NavBase extends React.Component implements IN // resolve is not supported for ssr return false; } else { + const doc = getDocumentEx(this.context)!; // there is an SSR check above so this is safe // If selectedKey is undefined in props and state, then check URL - _urlResolver = _urlResolver || document.createElement('a'); + _urlResolver = _urlResolver || doc.createElement('a'); _urlResolver.href = link.url || ''; const target: string = _urlResolver.href; diff --git a/packages/react/src/components/OverflowSet/OverflowSet.base.tsx b/packages/react/src/components/OverflowSet/OverflowSet.base.tsx index 32d0ac872e0811..fb94c8fafac49c 100644 --- a/packages/react/src/components/OverflowSet/OverflowSet.base.tsx +++ b/packages/react/src/components/OverflowSet/OverflowSet.base.tsx @@ -4,11 +4,13 @@ import { classNamesFunction, divProperties, elementContains, getNativeProps, foc import { OverflowButton } from './OverflowButton'; import type { IProcessedStyleSet } from '../../Styling'; import type { IOverflowSetProps, IOverflowSetStyles, IOverflowSetStyleProps, IOverflowSet } from './OverflowSet.types'; +import { useDocumentEx } from '../../utilities/dom'; const getClassNames = classNamesFunction(); const COMPONENT_NAME = 'OverflowSet'; const useComponentRef = (props: IOverflowSetProps, divContainer: React.RefObject) => { + const doc = useDocumentEx(); React.useImperativeHandle( props.componentRef, (): IOverflowSet => ({ @@ -26,12 +28,12 @@ const useComponentRef = (props: IOverflowSetProps, divContainer: React.RefObject } if (divContainer.current && elementContains(divContainer.current, childElement)) { childElement.focus(); - focusSucceeded = document.activeElement === childElement; + focusSucceeded = doc?.activeElement === childElement; } return focusSucceeded; }, }), - [divContainer], + [divContainer, doc], ); }; diff --git a/packages/react/src/components/Panel/Panel.base.tsx b/packages/react/src/components/Panel/Panel.base.tsx index 8438e2ad06a36b..329e90467153e8 100644 --- a/packages/react/src/components/Panel/Panel.base.tsx +++ b/packages/react/src/components/Panel/Panel.base.tsx @@ -22,6 +22,8 @@ import { FocusTrapZone } from '../FocusTrapZone/index'; import { PanelType } from './Panel.types'; import type { IProcessedStyleSet } from '../../Styling'; import type { IPanel, IPanelProps, IPanelStyleProps, IPanelStyles } from './Panel.types'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getDocumentEx, getWindowEx } from '../../utilities/dom'; const getClassNames = classNamesFunction(); const COMPONENT_NAME = 'Panel'; @@ -48,6 +50,8 @@ export class PanelBase extends React.Component impleme type: PanelType.smallFixedFar, }; + public static contextType = WindowContext; + private _async: Async; private _events: EventGroup; private _panel = React.createRef(); @@ -107,11 +111,13 @@ export class PanelBase extends React.Component impleme public componentDidMount(): void { this._async = new Async(this); this._events = new EventGroup(this); + const win = getWindowEx(this.context); + const doc = getDocumentEx(this.context); - this._events.on(window, 'resize', this._updateFooterPosition); + this._events.on(win, 'resize', this._updateFooterPosition); if (this._shouldListenForOuterClick(this.props)) { - this._events.on(document.body, 'mousedown', this._dismissOnOuterClick, true); + this._events.on(doc?.body, 'mousedown', this._dismissOnOuterClick, true); } if (this.props.isOpen) { @@ -132,10 +138,11 @@ export class PanelBase extends React.Component impleme } } + const doc = getDocumentEx(this.context); if (shouldListenOnOuterClick && !previousShouldListenOnOuterClick) { - this._events.on(document.body, 'mousedown', this._dismissOnOuterClick, true); + this._events.on(doc?.body, 'mousedown', this._dismissOnOuterClick, true); } else if (!shouldListenOnOuterClick && previousShouldListenOnOuterClick) { - this._events.off(document.body, 'mousedown', this._dismissOnOuterClick, true); + this._events.off(doc?.body, 'mousedown', this._dismissOnOuterClick, true); } } diff --git a/packages/react/src/components/ScrollablePane/ScrollablePane.base.tsx b/packages/react/src/components/ScrollablePane/ScrollablePane.base.tsx index ce2d43c27d7194..620ce3ac9b3e0d 100644 --- a/packages/react/src/components/ScrollablePane/ScrollablePane.base.tsx +++ b/packages/react/src/components/ScrollablePane/ScrollablePane.base.tsx @@ -17,6 +17,8 @@ import type { IScrollablePaneStyleProps, IScrollablePaneStyles, } from './ScrollablePane.types'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getWindowEx } from '../../utilities/dom'; export interface IScrollablePaneState { stickyTopHeight: number; @@ -31,6 +33,8 @@ export class ScrollablePaneBase extends React.Component implements IScrollablePane { + public static contextType = WindowContext; + private _root = React.createRef(); private _stickyAboveRef = React.createRef(); private _stickyBelowRef = React.createRef(); @@ -78,9 +82,10 @@ export class ScrollablePaneBase } public componentDidMount() { + const win = getWindowEx(this.context); const { initialScrollPosition } = this.props; this._events.on(this.contentContainer, 'scroll', this._onScroll); - this._events.on(window, 'resize', this._onWindowResize); + this._events.on(win, 'resize', this._onWindowResize); if (this.contentContainer && initialScrollPosition) { this.contentContainer.scrollTop = initialScrollPosition; } @@ -92,7 +97,7 @@ export class ScrollablePaneBase }); this.notifySubscribers(); - if ('MutationObserver' in window) { + if (win && 'MutationObserver' in win) { this._mutationObserver = new MutationObserver(mutation => { // Function to check if mutation is occuring in stickyAbove or stickyBelow function checkIfMutationIsSticky(mutationRecord: MutationRecord): boolean { @@ -354,6 +359,7 @@ export class ScrollablePaneBase notifySubscribers: this.notifySubscribers, syncScrollSticky: this.syncScrollSticky, }, + window: getWindowEx(this.context), }; }; diff --git a/packages/react/src/components/ScrollablePane/ScrollablePane.types.ts b/packages/react/src/components/ScrollablePane/ScrollablePane.types.ts index fe6a24d16d3c6e..78ab4edfc1a4e1 100644 --- a/packages/react/src/components/ScrollablePane/ScrollablePane.types.ts +++ b/packages/react/src/components/ScrollablePane/ScrollablePane.types.ts @@ -130,6 +130,10 @@ export interface IScrollablePaneContext { notifySubscribers: (sort?: boolean) => void; syncScrollSticky: (sticky: Sticky) => void; }; + window: Window | undefined; } -export const ScrollablePaneContext = React.createContext({ scrollablePane: undefined }); +export const ScrollablePaneContext = React.createContext({ + scrollablePane: undefined, + window: undefined, +}); diff --git a/packages/react/src/components/SelectedItemsList/BaseSelectedItemsList.tsx b/packages/react/src/components/SelectedItemsList/BaseSelectedItemsList.tsx index 5254f6ab1f19da..224ff5ace4ba20 100644 --- a/packages/react/src/components/SelectedItemsList/BaseSelectedItemsList.tsx +++ b/packages/react/src/components/SelectedItemsList/BaseSelectedItemsList.tsx @@ -7,6 +7,8 @@ import type { ISelectedItemProps, } from './BaseSelectedItemsList.types'; import type { IObjectWithKey } from '../../Utilities'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getDocumentEx } from '../../utilities/dom'; export interface IBaseSelectedItemsListState { items: T[]; @@ -16,6 +18,8 @@ export class BaseSelectedItemsList> extends React.Component> implements IBaseSelectedItemsList { + public static contextType = WindowContext; + protected root: HTMLElement; private _defaultSelection: Selection; @@ -213,22 +217,23 @@ export class BaseSelectedItemsList> if (this.props.onCopyItems) { const copyText = (this.props.onCopyItems as any)(items); - const copyInput = document.createElement('input') as HTMLInputElement; - document.body.appendChild(copyInput); + const doc = getDocumentEx(this.context)!; // equivalent to previous behavior of directly using `document` + const copyInput = doc.createElement('input') as HTMLInputElement; + doc.body.appendChild(copyInput); try { // Try to copy the text directly to the clipboard copyInput.value = copyText; copyInput.select(); // eslint-disable-next-line deprecation/deprecation - if (!document.execCommand('copy')) { + if (!doc.execCommand('copy')) { // The command failed. Fallback to the method below. throw new Error(); } } catch (err) { // no op } finally { - document.body.removeChild(copyInput); + doc.body.removeChild(copyInput); } } } diff --git a/packages/react/src/components/Slider/useSlider.ts b/packages/react/src/components/Slider/useSlider.ts index ddfa1015218520..bdb30715087204 100644 --- a/packages/react/src/components/Slider/useSlider.ts +++ b/packages/react/src/components/Slider/useSlider.ts @@ -12,6 +12,7 @@ import { } from '@fluentui/utilities'; import type { ISliderProps, ISliderStyleProps, ISliderStyles } from './Slider.types'; import type { ILabelProps } from '../Label/index'; +import { useWindowEx } from '../../utilities/dom'; export const ONKEYDOWN_TIMEOUT_DURATION = 1000; @@ -101,6 +102,7 @@ export const useSlider = (props: ISliderProps, ref: React.ForwardedRef void)[]>([]); const { setTimeout, clearTimeout } = useSetTimeout(); const sliderLine = React.useRef(null); + const win = useWindowEx(); // Casting here is necessary because useControllableValue expects the event for the change callback // to extend React.SyntheticEvent, when in fact for Slider, the event could be either a React event @@ -303,15 +305,16 @@ export const useSlider = (props: ISliderProps, ref: React.ForwardedRef void, true), - on(window, 'mouseup', onMouseUpOrTouchEnd, true), + on(win!, 'mousemove', onMouseMoveOrTouchMove as (ev: Event) => void, true), + on(win!, 'mouseup', onMouseUpOrTouchEnd, true), ); } else if (event.type === 'touchstart') { disposables.current.push( - on(window, 'touchmove', onMouseMoveOrTouchMove as (ev: Event) => void, true), - on(window, 'touchend', onMouseUpOrTouchEnd, true), + on(win!, 'touchmove', onMouseMoveOrTouchMove as (ev: Event) => void, true), + on(win!, 'touchend', onMouseUpOrTouchEnd, true), ); } onMouseMoveOrTouchMove(event, true); diff --git a/packages/react/src/components/Sticky/Sticky.tsx b/packages/react/src/components/Sticky/Sticky.tsx index fa89e9c5c7b980..197124193dea2a 100644 --- a/packages/react/src/components/Sticky/Sticky.tsx +++ b/packages/react/src/components/Sticky/Sticky.tsx @@ -263,6 +263,8 @@ export class Sticky extends React.Component { const distanceFromTop = this._getNonStickyDistanceFromTop(container); let isStickyTop = false; let isStickyBottom = false; + // eslint-disable-next-line no-restricted-globals + const doc = (this._getContext().window ?? window)?.document; if (this.canStickyTop) { const distanceToStickTop = distanceFromTop - this._getStickyDistanceFromTop(); @@ -279,11 +281,11 @@ export class Sticky extends React.Component { } if ( - document.activeElement && - this.nonStickyContent.contains(document.activeElement) && + doc?.activeElement && + this.nonStickyContent.contains(doc?.activeElement) && (this.state.isStickyTop !== isStickyTop || this.state.isStickyBottom !== isStickyBottom) ) { - this._activeElement = document.activeElement as HTMLElement; + this._activeElement = doc?.activeElement as HTMLElement; } else { this._activeElement = undefined; } @@ -342,10 +344,12 @@ export class Sticky extends React.Component { } let curr: HTMLElement = this.root; + // eslint-disable-next-line no-restricted-globals + const win = this._getContext().window ?? window; while ( - window.getComputedStyle(curr).getPropertyValue('background-color') === 'rgba(0, 0, 0, 0)' || - window.getComputedStyle(curr).getPropertyValue('background-color') === 'transparent' + win.getComputedStyle(curr).getPropertyValue('background-color') === 'rgba(0, 0, 0, 0)' || + win.getComputedStyle(curr).getPropertyValue('background-color') === 'transparent' ) { if (curr.tagName === 'HTML') { // Fallback color if no element has a declared background-color attribute @@ -355,7 +359,7 @@ export class Sticky extends React.Component { curr = curr.parentElement; } } - return window.getComputedStyle(curr).getPropertyValue('background-color'); + return win.getComputedStyle(curr).getPropertyValue('background-color'); } } diff --git a/packages/react/src/components/SwatchColorPicker/SwatchColorPicker.base.tsx b/packages/react/src/components/SwatchColorPicker/SwatchColorPicker.base.tsx index f8b4365120e2a5..d2aaaa6ba45532 100644 --- a/packages/react/src/components/SwatchColorPicker/SwatchColorPicker.base.tsx +++ b/packages/react/src/components/SwatchColorPicker/SwatchColorPicker.base.tsx @@ -10,6 +10,7 @@ import type { } from './SwatchColorPicker.types'; import type { IColorCellProps } from './ColorPickerGridCell.types'; import type { IButtonGridProps } from '../../utilities/ButtonGrid/ButtonGrid.types'; +import { useDocumentEx } from '../../utilities/dom'; interface ISwatchColorPickerInternalState { isNavigationIdle: boolean; @@ -40,6 +41,7 @@ export const SwatchColorPickerBase: React.FunctionComponent((props, ref) => { const defaultId = useId('swatchColorPicker'); const id = props.id || defaultId; + const doc = useDocumentEx(); const internalState = useConst({ isNavigationIdle: true, @@ -161,13 +163,13 @@ export const SwatchColorPickerBase: React.FunctionComponent { // The math here is done to account for the 45 degree rotation of the beak // and sub-pixel rounding that differs across browsers, which is more noticeable when // the device pixel ratio is larger - const tooltipGapSpace = -(Math.sqrt((beakWidth * beakWidth) / 2) + gapSpace) + 1 / window.devicePixelRatio; + const tooltipGapSpace = + -(Math.sqrt((beakWidth * beakWidth) / 2) + gapSpace) + + 1 / + // There isn't really a great way to pass in a `window` reference here so disabling the line rule + // eslint-disable-next-line no-restricted-globals + window.devicePixelRatio; return { root: [ diff --git a/packages/react/src/components/Tooltip/TooltipHost.base.tsx b/packages/react/src/components/Tooltip/TooltipHost.base.tsx index 39175c58acb404..abcc554dfd370e 100644 --- a/packages/react/src/components/Tooltip/TooltipHost.base.tsx +++ b/packages/react/src/components/Tooltip/TooltipHost.base.tsx @@ -16,6 +16,8 @@ import { TooltipOverflowMode } from './TooltipHost.types'; import { Tooltip } from './Tooltip'; import { TooltipDelay } from './Tooltip.types'; import type { ITooltipHostProps, ITooltipHostStyles, ITooltipHostStyleProps, ITooltipHost } from './TooltipHost.types'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getDocumentEx } from '../../utilities/dom'; export interface ITooltipHostState { /** @deprecated No longer used internally */ @@ -30,6 +32,7 @@ export class TooltipHostBase extends React.Component { this._hideTooltip(); @@ -202,6 +205,7 @@ export class TooltipHostBase extends React.Component { const { overflowMode, delay } = this.props; + const doc = getDocumentEx(this.context); if (TooltipHostBase._currentVisibleTooltip && TooltipHostBase._currentVisibleTooltip !== this) { TooltipHostBase._currentVisibleTooltip.dismiss(); @@ -215,7 +219,7 @@ export class TooltipHostBase extends React.Component> extends React.Component> implements IBasePicker { + public static contextType = WindowContext; + // Refs protected root = React.createRef(); protected input = React.createRef(); @@ -505,7 +509,8 @@ export class BasePicker> }); } else { this.setState({ - suggestionsVisible: this.input.current! && this.input.current!.inputElement === document.activeElement, + suggestionsVisible: + this.input.current! && this.input.current!.inputElement === getDocumentEx(this.context)?.activeElement, }); } @@ -599,7 +604,7 @@ export class BasePicker> // even when it's not. Using document.activeElement is another way // for us to be able to get what the relatedTarget without relying // on the event - relatedTarget = document.activeElement; + relatedTarget = getDocumentEx(this.context)!.activeElement; } if (relatedTarget && !elementContains(this.root.current!, relatedTarget as HTMLElement)) { this.setState({ isFocused: false }); @@ -1032,7 +1037,7 @@ export class BasePicker> const areSuggestionsVisible = this.input.current !== undefined && this.input.current !== null && - this.input.current.inputElement === document.activeElement && + this.input.current.inputElement === getDocumentEx(this.context)?.activeElement && this.input.current.value !== ''; return areSuggestionsVisible; diff --git a/packages/react/src/utilities/DraggableZone/DraggableZone.tsx b/packages/react/src/utilities/DraggableZone/DraggableZone.tsx index c9df1913d94781..760e2612fa684e 100644 --- a/packages/react/src/utilities/DraggableZone/DraggableZone.tsx +++ b/packages/react/src/utilities/DraggableZone/DraggableZone.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import { getClassNames } from './DraggableZone.styles'; import { on } from '../../Utilities'; import type { IDraggableZoneProps, ICoordinates, IDragData } from './DraggableZone.types'; +import { WindowContext } from '@fluentui/react-window-provider'; +import { getDocumentEx } from '../dom'; export interface IDraggableZoneState { isDragging: boolean; @@ -27,6 +29,8 @@ const eventMapping = { type MouseTouchEvent = React.MouseEvent & React.TouchEvent & Event; export class DraggableZone extends React.Component { + public static contextType = WindowContext; + private _touchId?: number; private _currentEventType = eventMapping.mouse; private _events: (() => void)[] = []; @@ -153,9 +157,10 @@ export class DraggableZone extends React.Component( private _updateViewport = (withForceUpdate?: boolean) => { const { viewport } = this.state; const viewportElement = this._root.current; + const win = getWindow(viewportElement); const scrollElement = findScrollableParent(viewportElement) as HTMLElement; - const scrollRect = getRect(scrollElement); - const clientRect = getRect(viewportElement); + const scrollRect = getRect(scrollElement, win); + const clientRect = getRect(viewportElement, win); const updateComponent = () => { if (withForceUpdate && this._composedComponentInstance) { this._composedComponentInstance.forceUpdate(); diff --git a/packages/react/src/utilities/dom.ts b/packages/react/src/utilities/dom.ts new file mode 100644 index 00000000000000..623a08b1c71433 --- /dev/null +++ b/packages/react/src/utilities/dom.ts @@ -0,0 +1,55 @@ +import { useDocument, useWindow, WindowProviderProps } from '@fluentui/react-window-provider'; + +/** + * NOTE: the check for `window`/`document` is a bit verbose and perhaps + * overkill but it ensures the prior assumbed behavior of directly + * calling `window`/`document` is preserved. + * + * It is possible to set `window` to undefined on `WindowProvider` so + * we'll fallback to directly accessing the global in that (hopefully unlikely) + * case. + */ + +/** + * Get a reference to the `document` object. + * Use this in place of the global `document` in React function components. + * @returns Document | undefined + */ +export const useDocumentEx = () => { + // eslint-disable-next-line no-restricted-globals + return useDocument() ?? typeof document !== 'undefined' ? document : undefined; +}; + +/** + * Get a reference to the `window` object. + * Use this in place of the global `window` in React function components. + * @returns Window | undefined + */ +export const useWindowEx = () => { + // eslint-disable-next-line no-restricted-globals + return useWindow() ?? typeof window !== 'undefined' ? window : undefined; +}; + +/** + * Get a reference to the `document` object. + * Use this in place of the global `document` in React class components. + * + * @param ctx - Class component WindowContext + * @returns Document | undefined + */ +export const getDocumentEx = (ctx: Pick | undefined) => { + // eslint-disable-next-line no-restricted-globals + return ctx?.window?.document ?? typeof document !== 'undefined' ? document : undefined; +}; + +/** + * Get a reference to the `window` object. + * Use this in place of the global `window` in React class components. + * + * @param ctx - Class component WindowContext + * @returns Window | undefined + */ +export const getWindowEx = (ctx: Pick | undefined) => { + // eslint-disable-next-line no-restricted-globals + return ctx?.window ?? typeof window !== 'undefined' ? window : undefined; +}; diff --git a/packages/react/src/utilities/positioning/positioning.ts b/packages/react/src/utilities/positioning/positioning.ts index 1fba0a3481e1d8..64ca4a3813cf5c 100644 --- a/packages/react/src/utilities/positioning/positioning.ts +++ b/packages/react/src/utilities/positioning/positioning.ts @@ -1,5 +1,5 @@ import { DirectionalHint } from '../../common/DirectionalHint'; -import { getScrollbarWidth, getRTL } from '../../Utilities'; +import { getScrollbarWidth, getRTL, getWindow } from '../../Utilities'; import { RectangleEdge } from './positioning.types'; import { Rectangle } from '../../Utilities'; import type { IRectangle, Point } from '../../Utilities'; @@ -917,10 +917,12 @@ function _positionElement( hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: IPositionedData, + win?: Window, ): IPositionedData { + const theWin = win ?? getWindow()!; const boundingRect: Rectangle = props.bounds ? _getRectangleFromIRect(props.bounds) - : new Rectangle(0, window.innerWidth - getScrollbarWidth(), 0, window.innerHeight); + : new Rectangle(0, theWin.innerWidth - getScrollbarWidth(), 0, theWin.innerHeight); const positionedElement: IElementPosition = _positionElementRelative( props, elementToPosition, @@ -942,14 +944,16 @@ function _positionCallout( shouldScroll = false, minimumScrollResizeHeight?: number, doNotFinalizeReturnEdge?: boolean, + win?: Window, ): ICalloutPositionedInfo { + const theWin = win ?? getWindow()!; const beakWidth: number = props.isBeakVisible ? props.beakWidth || 0 : 0; const gap = _calculateGapSpace(props.isBeakVisible, props.beakWidth, props.gapSpace); const positionProps: IPositionProps = props; positionProps.gapSpace = gap; const boundingRect: Rectangle = props.bounds ? _getRectangleFromIRect(props.bounds) - : new Rectangle(0, window.innerWidth - getScrollbarWidth(), 0, window.innerHeight); + : new Rectangle(0, theWin.innerWidth - getScrollbarWidth(), 0, theWin.innerHeight); const positionedElement: IElementPositionInfo = _positionElementRelative( positionProps, @@ -978,8 +982,10 @@ function _positionCard( hostElement: HTMLElement, callout: HTMLElement, previousPositions?: ICalloutPositionedInfo, + win?: Window, ): ICalloutPositionedInfo { - return _positionCallout(props, hostElement, callout, previousPositions, false, undefined, true); + const theWin = win ?? getWindow()!; + return _positionCallout(props, hostElement, callout, previousPositions, false, undefined, true, theWin); } function _getRectangleFromTarget(target: Element | MouseEvent | Point | Rectangle): Rectangle { @@ -1028,8 +1034,9 @@ export function positionElement( hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: IPositionedData, + win?: Window, ): IPositionedData { - return _positionElement(props, hostElement, elementToPosition, previousPositions); + return _positionElement(props, hostElement, elementToPosition, previousPositions, win); } export function positionCallout( @@ -1039,6 +1046,7 @@ export function positionCallout( previousPositions?: ICalloutPositionedInfo, shouldScroll?: boolean, minimumScrollResizeHeight?: number, + win?: Window, ): ICalloutPositionedInfo { return _positionCallout( props, @@ -1047,6 +1055,8 @@ export function positionCallout( previousPositions, shouldScroll, minimumScrollResizeHeight, + undefined, + win, ); } @@ -1055,8 +1065,9 @@ export function positionCard( hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo, + win?: Window, ): ICalloutPositionedInfo { - return _positionCard(props, hostElement, elementToPosition, previousPositions); + return _positionCard(props, hostElement, elementToPosition, previousPositions, win); } /** @@ -1071,11 +1082,13 @@ export function getMaxHeight( gapSpace: number = 0, bounds?: IRectangle, coverTarget?: boolean, + win?: Window, ): number { + const theWin = win ?? getWindow()!; const targetRect = _getRectangleFromTarget(target); const boundingRectangle = bounds ? _getRectangleFromIRect(bounds) - : new Rectangle(0, window.innerWidth - getScrollbarWidth(), 0, window.innerHeight); + : new Rectangle(0, theWin.innerWidth - getScrollbarWidth(), 0, theWin.innerHeight); return _getMaxHeightFromTargetRectangle(targetRect, targetEdge, gapSpace, boundingRectangle, coverTarget); } diff --git a/packages/react/src/utilities/selection/SelectionZone.tsx b/packages/react/src/utilities/selection/SelectionZone.tsx index 01fafadb6f6e70..82ed097d3b7271 100644 --- a/packages/react/src/utilities/selection/SelectionZone.tsx +++ b/packages/react/src/utilities/selection/SelectionZone.tsx @@ -200,12 +200,13 @@ export class SelectionZone extends React.Component): void => { let target = ev.target as HTMLElement; + const win = getWindow(this._root.current); + const doc = win?.document; - if (document.activeElement !== target && !elementContains(document.activeElement as HTMLElement, target)) { + if (doc?.activeElement !== target && !elementContains(doc?.activeElement as HTMLElement, target)) { this.ignoreNextFocus(); return; } @@ -737,9 +740,11 @@ export class SelectionZone extends React.Component; // @public export class AutoScroll { - constructor(element: HTMLElement); + constructor(element: HTMLElement, win?: Window); // (undocumented) dispose(): void; } @@ -241,7 +241,7 @@ export class EventGroup { onAll(target: any, events: { [key: string]: (args?: any) => void; }, useCapture?: boolean): void; - static raise(target: any, eventName: string, eventArgs?: any, bubbleEvent?: boolean): boolean | undefined; + static raise(target: any, eventName: string, eventArgs?: any, bubbleEvent?: boolean, doc?: Document): boolean | undefined; raise(eventName: string, eventArgs?: any, bubbleEvent?: boolean): boolean | undefined; // (undocumented) static stopPropagation(event: any): void; @@ -375,7 +375,7 @@ export function getPreviousElement(rootElement: HTMLElement, currentElement: HTM export function getPropsWithDefaults(defaultProps: Partial, propsWithoutDefaults: TProps): TProps; // @public -export function getRect(element: HTMLElement | Window | null): IRectangle | undefined; +export function getRect(element: HTMLElement | Window | null, win?: Window): IRectangle | undefined; // @public @deprecated (undocumented) export function getResourceUrl(url: string): string; @@ -391,7 +391,7 @@ export function getRTLSafeKeyCode(key: number, theme?: { }): number; // @public -export function getScrollbarWidth(): number; +export function getScrollbarWidth(doc?: Document): number; export { getVirtualParent } @@ -811,7 +811,7 @@ export function isElementTabbable(element: HTMLElement, checkTabIndex?: boolean) export function isElementVisible(element: HTMLElement | undefined | null): boolean; // @public -export function isElementVisibleAndNotHidden(element: HTMLElement | undefined | null): boolean; +export function isElementVisibleAndNotHidden(element: HTMLElement | undefined | null, win?: Window): boolean; // Warning: (ae-internal-missing-underscore) The name "ISerializableObject" should be prefixed with an underscore because the declaration is marked as @internal // @@ -1052,7 +1052,7 @@ export { portalContainsElement } export function precisionRound(value: number, precision: number, base?: number): number; // @public @deprecated -export function raiseClick(target: Element): void; +export function raiseClick(target: Element, doc?: Document): void; // @public export class Rectangle { @@ -1228,7 +1228,7 @@ export function setWarningCallback(warningCallback?: (message: string) => void): export function shallowCompare(a: TA, b: TB): boolean; // @public -export function shouldWrapFocus(element: HTMLElement, noWrapDataAttribute: 'data-no-vertical-wrap' | 'data-no-horizontal-wrap'): boolean; +export function shouldWrapFocus(element: HTMLElement, noWrapDataAttribute: 'data-no-vertical-wrap' | 'data-no-horizontal-wrap', doc?: Document): boolean; // @public export function styled, TStyleProps, TStyleSet extends IStyleSet>(Component: React_2.ComponentClass | React_2.FunctionComponent, baseStyles: IStyleFunctionOrObject, getProps?: (props: TComponentProps) => Partial, customizable?: ICustomizableProps, pure?: boolean): React_2.FunctionComponent; diff --git a/packages/utilities/src/AutoScroll.ts b/packages/utilities/src/AutoScroll.ts index 478a892efa7a15..86a23ea963d53c 100644 --- a/packages/utilities/src/AutoScroll.ts +++ b/packages/utilities/src/AutoScroll.ts @@ -2,6 +2,7 @@ import { EventGroup } from './EventGroup'; import { findScrollableParent } from './scroll'; import { getRect } from './dom/getRect'; import type { IRectangle } from './IRectangle'; +import { getWindow } from './dom'; declare function setTimeout(cb: Function, delay: number): number; @@ -26,21 +27,22 @@ export class AutoScroll { private _isVerticalScroll!: boolean; private _timeoutId?: number; - constructor(element: HTMLElement) { + constructor(element: HTMLElement, win?: Window) { + const theWin = win ?? getWindow(element)!; this._events = new EventGroup(this); this._scrollableParent = findScrollableParent(element) as HTMLElement; this._incrementScroll = this._incrementScroll.bind(this); - this._scrollRect = getRect(this._scrollableParent); + this._scrollRect = getRect(this._scrollableParent, theWin); // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (this._scrollableParent === (window as any)) { - this._scrollableParent = document.body; + if (this._scrollableParent === (theWin as any)) { + this._scrollableParent = theWin.document.body; } if (this._scrollableParent) { - this._events.on(window, 'mousemove', this._onMouseMove, true); - this._events.on(window, 'touchmove', this._onTouchMove, true); + this._events.on(theWin, 'mousemove', this._onMouseMove, true); + this._events.on(theWin, 'touchmove', this._onTouchMove, true); } } diff --git a/packages/utilities/src/DelayedRender.tsx b/packages/utilities/src/DelayedRender.tsx index 3787b6c8dbb590..29f1aa484f92e3 100644 --- a/packages/utilities/src/DelayedRender.tsx +++ b/packages/utilities/src/DelayedRender.tsx @@ -7,7 +7,6 @@ import { IReactProps } from './React.types'; * * @public */ -// eslint-disable-next-line deprecation/deprecation export interface IDelayedRenderProps extends IReactProps<{}> { /** * Number of milliseconds to delay rendering children. @@ -51,6 +50,7 @@ export class DelayedRender extends React.Component { this.setState({ isRendered: true, diff --git a/packages/utilities/src/EventGroup.ts b/packages/utilities/src/EventGroup.ts index 87927e95f6f11a..3ea6e16961a466 100644 --- a/packages/utilities/src/EventGroup.ts +++ b/packages/utilities/src/EventGroup.ts @@ -1,3 +1,4 @@ +import { getDocument } from './dom'; import { assign } from './object'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -61,12 +62,19 @@ export class EventGroup { * which may lead to unexpected behavior if it differs from the defaults. * */ - public static raise(target: any, eventName: string, eventArgs?: any, bubbleEvent?: boolean): boolean | undefined { + public static raise( + target: any, + eventName: string, + eventArgs?: any, + bubbleEvent?: boolean, + doc?: Document, + ): boolean | undefined { let retVal; + const theDoc = doc ?? getDocument()!; if (EventGroup._isElement(target)) { - if (typeof document !== 'undefined' && document.createEvent) { - let ev = document.createEvent('HTMLEvents'); + if (typeof theDoc !== 'undefined' && theDoc.createEvent) { + let ev = theDoc.createEvent('HTMLEvents'); // eslint-disable-next-line deprecation/deprecation ev.initEvent(eventName, bubbleEvent || false, true); @@ -74,9 +82,9 @@ export class EventGroup { assign(ev, eventArgs); retVal = target.dispatchEvent(ev); - } else if (typeof document !== 'undefined' && (document as any).createEventObject) { + } else if (typeof theDoc !== 'undefined' && (theDoc as any).createEventObject) { // IE8 - let evObj = (document as any).createEventObject(eventArgs); + let evObj = (theDoc as any).createEventObject(eventArgs); // cannot set cancelBubble on evObj, fireEvent will overwrite it target.fireEvent('on' + eventName, evObj); } diff --git a/packages/utilities/src/FabricPerformance.ts b/packages/utilities/src/FabricPerformance.ts index d1c8179007dcdc..e131bc6074b399 100644 --- a/packages/utilities/src/FabricPerformance.ts +++ b/packages/utilities/src/FabricPerformance.ts @@ -67,7 +67,7 @@ export class FabricPerformance { measurement.totalDuration += duration; measurement.count++; measurement.all.push({ - duration: duration, + duration, timeStamp: end, }); FabricPerformance.summary[name] = measurement; diff --git a/packages/utilities/src/dom/canUseDOM.ts b/packages/utilities/src/dom/canUseDOM.ts index 01520b98ccc7a8..e7289568b5a152 100644 --- a/packages/utilities/src/dom/canUseDOM.ts +++ b/packages/utilities/src/dom/canUseDOM.ts @@ -3,11 +3,11 @@ */ export function canUseDOM(): boolean { return ( + // eslint-disable-next-line no-restricted-globals typeof window !== 'undefined' && !!( - window.document && - // eslint-disable-next-line deprecation/deprecation - window.document.createElement + // eslint-disable-next-line no-restricted-globals, deprecation/deprecation + (window.document && window.document.createElement) ) ); } diff --git a/packages/utilities/src/dom/getDocument.ts b/packages/utilities/src/dom/getDocument.ts index ae975f065e76fd..221794c6e464f6 100644 --- a/packages/utilities/src/dom/getDocument.ts +++ b/packages/utilities/src/dom/getDocument.ts @@ -8,11 +8,13 @@ import { canUseDOM } from './canUseDOM'; * @public */ export function getDocument(rootElement?: HTMLElement | null): Document | undefined { + // eslint-disable-next-line no-restricted-globals if (!canUseDOM() || typeof document === 'undefined') { return undefined; } else { const el = rootElement as Element; + // eslint-disable-next-line no-restricted-globals return el && el.ownerDocument ? el.ownerDocument : document; } } diff --git a/packages/utilities/src/dom/getFirstVisibleElementFromSelector.ts b/packages/utilities/src/dom/getFirstVisibleElementFromSelector.ts index 253b0f16116db7..303f9a5abcf451 100644 --- a/packages/utilities/src/dom/getFirstVisibleElementFromSelector.ts +++ b/packages/utilities/src/dom/getFirstVisibleElementFromSelector.ts @@ -9,8 +9,11 @@ import { getDocument } from './getDocument'; * @public */ export function getFirstVisibleElementFromSelector(selector: string): Element | undefined { - const elements = getDocument()!.querySelectorAll(selector); + const doc = getDocument()!; + const elements = doc.querySelectorAll(selector); // Iterate across the elements that match the selector and return the first visible/non-hidden element - return Array.from(elements).find((element: HTMLElement) => isElementVisibleAndNotHidden(element)); + return Array.from(elements).find((element: HTMLElement) => + isElementVisibleAndNotHidden(element, doc.defaultView ?? undefined), + ); } diff --git a/packages/utilities/src/dom/getRect.ts b/packages/utilities/src/dom/getRect.ts index 63d24fd214859a..e4d00483766d34 100644 --- a/packages/utilities/src/dom/getRect.ts +++ b/packages/utilities/src/dom/getRect.ts @@ -1,21 +1,26 @@ import type { IRectangle } from '../IRectangle'; +import { getWindow } from './getWindow'; /** * Helper to get bounding client rect. Passing in window will get the window size. * * @public */ -export function getRect(element: HTMLElement | Window | null): IRectangle | undefined { +export function getRect(element: HTMLElement | Window | null, win?: Window): IRectangle | undefined { + const theWin = + win ?? (!element || (element && element.hasOwnProperty('devicePixelRatio'))) + ? getWindow() + : getWindow(element as HTMLElement)!; let rect: IRectangle | undefined; if (element) { - if (element === window) { + if (element === theWin) { rect = { left: 0, top: 0, - width: window.innerWidth, - height: window.innerHeight, - right: window.innerWidth, - bottom: window.innerHeight, + width: theWin.innerWidth, + height: theWin.innerHeight, + right: theWin.innerWidth, + bottom: theWin.innerHeight, }; } else if ((element as { getBoundingClientRect?: unknown }).getBoundingClientRect) { rect = (element as HTMLElement).getBoundingClientRect(); diff --git a/packages/utilities/src/dom/getWindow.ts b/packages/utilities/src/dom/getWindow.ts index 8d37fb26009846..c4e41a9c393c5a 100644 --- a/packages/utilities/src/dom/getWindow.ts +++ b/packages/utilities/src/dom/getWindow.ts @@ -6,6 +6,7 @@ let _window: Window | undefined = undefined; // hits a memory leak, whereas aliasing it and calling "typeof _window" does not. // Caching the window value at the file scope lets us minimize the impact. try { + // eslint-disable-next-line no-restricted-globals _window = window; } catch (e) { /* no-op */ diff --git a/packages/utilities/src/dom/raiseClick.ts b/packages/utilities/src/dom/raiseClick.ts index d66c78a03e5157..fecf98ccca3b71 100644 --- a/packages/utilities/src/dom/raiseClick.ts +++ b/packages/utilities/src/dom/raiseClick.ts @@ -1,21 +1,24 @@ +import { getDocument } from './getDocument'; + /** Raises a click event. * @deprecated Moved to `FocusZone` component since it was the only place internally using this function. */ -export function raiseClick(target: Element): void { - const event = createNewEvent('MouseEvents'); +export function raiseClick(target: Element, doc?: Document): void { + const theDoc = doc ?? getDocument()!; + const event = createNewEvent('MouseEvents', theDoc); // eslint-disable-next-line deprecation/deprecation event.initEvent('click', true, true); target.dispatchEvent(event); } -function createNewEvent(eventName: string): Event { +function createNewEvent(eventName: string, doc: Document): Event { let event; if (typeof Event === 'function') { // Chrome, Opera, Firefox event = new Event(eventName); } else { // IE - event = document.createEvent('Event'); + event = doc.createEvent('Event'); // eslint-disable-next-line deprecation/deprecation event.initEvent(eventName, true, true); } diff --git a/packages/utilities/src/focus.ts b/packages/utilities/src/focus.ts index e3ab748badb23b..03e371dbaea9f0 100644 --- a/packages/utilities/src/focus.ts +++ b/packages/utilities/src/focus.ts @@ -382,12 +382,13 @@ export function isElementVisible(element: HTMLElement | undefined | null): boole * * @public */ -export function isElementVisibleAndNotHidden(element: HTMLElement | undefined | null): boolean { +export function isElementVisibleAndNotHidden(element: HTMLElement | undefined | null, win?: Window): boolean { + const theWin = win ?? getWindow()!; return ( !!element && isElementVisible(element) && !element.hidden && - window.getComputedStyle(element).visibility !== 'hidden' + theWin.getComputedStyle(element).visibility !== 'hidden' ); } @@ -456,8 +457,8 @@ export function isElementFocusSubZone(element?: HTMLElement): boolean { * @public */ export function doesElementContainFocus(element: HTMLElement): boolean { - let document = getDocument(element); - let currentActiveElement: HTMLElement | undefined = document && (document.activeElement as HTMLElement); + let doc = getDocument(element); + let currentActiveElement: HTMLElement | undefined = doc && (doc.activeElement as HTMLElement); if (currentActiveElement && elementContains(element, currentActiveElement)) { return true; } @@ -473,8 +474,10 @@ export function doesElementContainFocus(element: HTMLElement): boolean { export function shouldWrapFocus( element: HTMLElement, noWrapDataAttribute: 'data-no-vertical-wrap' | 'data-no-horizontal-wrap', + doc?: Document, ): boolean { - return elementContainsAttribute(element, noWrapDataAttribute) === 'true' ? false : true; + const theDoc = doc ?? getDocument()!; + return elementContainsAttribute(element, noWrapDataAttribute, theDoc) === 'true' ? false : true; } let animationId: number | undefined = undefined; diff --git a/packages/utilities/src/mobileDetector.ts b/packages/utilities/src/mobileDetector.ts index 4dfd584475e5ef..97fcbc7a0ecb4d 100644 --- a/packages/utilities/src/mobileDetector.ts +++ b/packages/utilities/src/mobileDetector.ts @@ -3,8 +3,10 @@ * Used to determine whether iOS-specific behavior should be applied. */ export const isIOS = (): boolean => { + // eslint-disable-next-line no-restricted-globals if (!window || !window.navigator || !window.navigator.userAgent) { return false; } + // eslint-disable-next-line no-restricted-globals return /iPad|iPhone|iPod/i.test(window.navigator.userAgent); }; diff --git a/packages/utilities/src/scroll.ts b/packages/utilities/src/scroll.ts index 008b56bd76b2d9..3319ce06027975 100644 --- a/packages/utilities/src/scroll.ts +++ b/packages/utilities/src/scroll.ts @@ -136,20 +136,21 @@ export function enableBodyScroll(): void { * * @public */ -export function getScrollbarWidth(): number { +export function getScrollbarWidth(doc?: Document): number { if (_scrollbarWidth === undefined) { - let scrollDiv: HTMLElement = document.createElement('div'); + const theDoc = doc ?? getDocument()!; + let scrollDiv: HTMLElement = theDoc.createElement('div'); scrollDiv.style.setProperty('width', '100px'); scrollDiv.style.setProperty('height', '100px'); scrollDiv.style.setProperty('overflow', 'scroll'); scrollDiv.style.setProperty('position', 'absolute'); scrollDiv.style.setProperty('top', '-9999px'); - document.body.appendChild(scrollDiv); + theDoc.body.appendChild(scrollDiv); // Get the scrollbar width _scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; // Delete the DIV - document.body.removeChild(scrollDiv); + theDoc.body.removeChild(scrollDiv); } return _scrollbarWidth; diff --git a/packages/utilities/src/selection/Selection.test.ts b/packages/utilities/src/selection/Selection.test.ts index 3187fd7cc19f08..ac51e9796ea3ef 100644 --- a/packages/utilities/src/selection/Selection.test.ts +++ b/packages/utilities/src/selection/Selection.test.ts @@ -150,7 +150,7 @@ describe('Selection', () => { } const items: ICustomItem[] = [{ id: 'a' }, { id: 'b' }]; const selection = new Selection({ - onSelectionChanged: onSelectionChanged, + onSelectionChanged, getKey: (item: ICustomItem) => item.id, items, });