diff --git a/apps/perf-test-react-components/.eslintrc.json b/apps/perf-test-react-components/.eslintrc.json index 2948bbf09dea5..2b95bc03dc3f5 100644 --- a/apps/perf-test-react-components/.eslintrc.json +++ b/apps/perf-test-react-components/.eslintrc.json @@ -3,6 +3,7 @@ "root": true, "rules": { "@griffel/styles-file": "off", - "no-console": "off" + "no-console": "off", + "no-restricted-globals": "off" } } diff --git a/apps/perf-test-react-components/src/scenarios/FluentProviderWithTheme.tsx b/apps/perf-test-react-components/src/scenarios/FluentProviderWithTheme.tsx index 6c9a02c6a7112..d371eff22d9a6 100644 --- a/apps/perf-test-react-components/src/scenarios/FluentProviderWithTheme.tsx +++ b/apps/perf-test-react-components/src/scenarios/FluentProviderWithTheme.tsx @@ -5,9 +5,11 @@ import { teamsLightTheme } from '@fluentui/react-theme'; const LayoutShift: React.FunctionComponent = ({ children }) => { // eslint-disable-next-line no-restricted-properties React.useLayoutEffect(() => { + // eslint-disable-next-line no-restricted-globals if (document.body) { // Accessing the offsetWidth forces reflow (browser synchronously calculates style and layout. // This allows us to measure theme impact on the rendering performance. + // eslint-disable-next-line no-restricted-globals document.body.offsetWidth; } }); diff --git a/apps/perf-test-react-components/src/scenarios/MakeStyles.tsx b/apps/perf-test-react-components/src/scenarios/MakeStyles.tsx index 5375ef92490bf..539dbafda703b 100644 --- a/apps/perf-test-react-components/src/scenarios/MakeStyles.tsx +++ b/apps/perf-test-react-components/src/scenarios/MakeStyles.tsx @@ -1,7 +1,9 @@ import { mergeClasses, makeStyles, createDOMRenderer } from '@griffel/core'; import * as React from 'react'; -const renderer = createDOMRenderer(document); +const renderer = + // eslint-disable-next-line no-restricted-globals + createDOMRenderer(document); const useStyles = makeStyles({ view: { diff --git a/apps/perf-test/.eslintrc.json b/apps/perf-test/.eslintrc.json index 6c6d649f51758..b77a03e1dea94 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 ac81a128fc768..aa07c25a34204 100644 --- a/apps/pr-deploy-site/.eslintrc.json +++ b/apps/pr-deploy-site/.eslintrc.json @@ -12,6 +12,7 @@ // turn off conflicting or unwanted rules from normal set "curly": "off", "no-var": "off", + "no-restricted-globals": "off", "vars-on-top": "off", "prefer-arrow-callback": "off" } diff --git a/apps/public-docsite-resources/.eslintrc.json b/apps/public-docsite-resources/.eslintrc.json index 8e090ba63188f..b9cc959897e9b 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-v9/.eslintrc.json b/apps/public-docsite-v9/.eslintrc.json index b3f394f7cbaca..54ac641d7b3bf 100644 --- a/apps/public-docsite-v9/.eslintrc.json +++ b/apps/public-docsite-v9/.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/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/useDebounce.ts b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/useDebounce.ts index c00486012917c..21854c7aade79 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/useDebounce.ts +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/useDebounce.ts @@ -6,6 +6,7 @@ export const useDebounce = (fn: (...args: unknown[]) => void, duration: number) return React.useCallback( (...args: unknown[]) => { clearTimeout(timeoutRef.current); + // eslint-disable-next-line no-restricted-globals timeoutRef.current = window.setTimeout(() => { fn(...args); }, duration); diff --git a/apps/public-docsite-v9/src/DocsComponents/Toc.stories.tsx b/apps/public-docsite-v9/src/DocsComponents/Toc.stories.tsx index 91c12bd7cd2cb..5356d38fbf1f1 100644 --- a/apps/public-docsite-v9/src/DocsComponents/Toc.stories.tsx +++ b/apps/public-docsite-v9/src/DocsComponents/Toc.stories.tsx @@ -99,6 +99,7 @@ export const Toc = ({ stories }: { stories: TocItem[] }) => { ); stories.forEach(link => { + // eslint-disable-next-line no-restricted-globals const element = document.getElementById(nameToHash(link.name)); if (element) { observer.observe(element); diff --git a/apps/public-docsite/.eslintrc.json b/apps/public-docsite/.eslintrc.json index 80f89945c1a8d..d617947c59dff 100644 --- a/apps/public-docsite/.eslintrc.json +++ b/apps/public-docsite/.eslintrc.json @@ -5,6 +5,7 @@ "@typescript-eslint/no-explicit-any": "off", "deprecation/deprecation": "off", "import/no-webpack-loader-syntax": "off", // ok in this project + "no-restricted-globals": "off", "prefer-const": "off", "react/jsx-no-bind": "off" } diff --git a/apps/react-18-tests-v8/.eslintrc.json b/apps/react-18-tests-v8/.eslintrc.json index 92f2e74b606c9..b2f318c6b6a1e 100644 --- a/apps/react-18-tests-v8/.eslintrc.json +++ b/apps/react-18-tests-v8/.eslintrc.json @@ -1,5 +1,7 @@ { "extends": ["plugin:@fluentui/eslint-plugin/react"], "root": true, - "rules": {} + "rules": { + "no-restricted-globals": "off" + } } diff --git a/apps/react-18-tests-v8/src/index.tsx b/apps/react-18-tests-v8/src/index.tsx index 87a4f9be66eab..1035695c1018b 100644 --- a/apps/react-18-tests-v8/src/index.tsx +++ b/apps/react-18-tests-v8/src/index.tsx @@ -2,7 +2,10 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; -const root = createRoot(document.getElementById('root') as HTMLElement); +const root = createRoot( + // eslint-disable-next-line no-restricted-globals + document.getElementById('root') as HTMLElement, +); root.render( diff --git a/apps/react-18-tests-v9/.eslintrc.json b/apps/react-18-tests-v9/.eslintrc.json index 92f2e74b606c9..b2f318c6b6a1e 100644 --- a/apps/react-18-tests-v9/.eslintrc.json +++ b/apps/react-18-tests-v9/.eslintrc.json @@ -1,5 +1,7 @@ { "extends": ["plugin:@fluentui/eslint-plugin/react"], "root": true, - "rules": {} + "rules": { + "no-restricted-globals": "off" + } } diff --git a/apps/react-18-tests-v9/src/index.tsx b/apps/react-18-tests-v9/src/index.tsx index 87a4f9be66eab..1035695c1018b 100644 --- a/apps/react-18-tests-v9/src/index.tsx +++ b/apps/react-18-tests-v9/src/index.tsx @@ -2,7 +2,10 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; -const root = createRoot(document.getElementById('root') as HTMLElement); +const root = createRoot( + // eslint-disable-next-line no-restricted-globals + document.getElementById('root') as HTMLElement, +); root.render( diff --git a/apps/recipes-react-components/.eslintrc.json b/apps/recipes-react-components/.eslintrc.json index 453f767c8db07..b9d702c5e755d 100644 --- a/apps/recipes-react-components/.eslintrc.json +++ b/apps/recipes-react-components/.eslintrc.json @@ -7,6 +7,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/apps/ssr-tests-v9/.eslintrc.json b/apps/ssr-tests-v9/.eslintrc.json index f502b07b22439..b6719ff5c00cf 100644 --- a/apps/ssr-tests-v9/.eslintrc.json +++ b/apps/ssr-tests-v9/.eslintrc.json @@ -2,6 +2,7 @@ "extends": ["plugin:@fluentui/eslint-plugin/node"], "root": true, "rules": { - "import/no-extraneous-dependencies": ["error", { "packageDir": ["../../", "./"] }] + "import/no-extraneous-dependencies": ["error", { "packageDir": ["../../", "./"] }], + "no-restricted-globals": "off" } } diff --git a/apps/stress-test/.eslintrc.json b/apps/stress-test/.eslintrc.json index f502b07b22439..b6719ff5c00cf 100644 --- a/apps/stress-test/.eslintrc.json +++ b/apps/stress-test/.eslintrc.json @@ -2,6 +2,7 @@ "extends": ["plugin:@fluentui/eslint-plugin/node"], "root": true, "rules": { - "import/no-extraneous-dependencies": ["error", { "packageDir": ["../../", "./"] }] + "import/no-extraneous-dependencies": ["error", { "packageDir": ["../../", "./"] }], + "no-restricted-globals": "off" } } diff --git a/apps/theming-designer/.eslintrc.json b/apps/theming-designer/.eslintrc.json index f7aed51167d6c..0f42e8d9a881b 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", + "no-restricted-globals": "off", "prefer-const": "off" } } diff --git a/apps/vr-tests-react-components/.eslintrc.json b/apps/vr-tests-react-components/.eslintrc.json index 76ca2160a370b..ffeab27cf4c88 100644 --- a/apps/vr-tests-react-components/.eslintrc.json +++ b/apps/vr-tests-react-components/.eslintrc.json @@ -8,6 +8,7 @@ "@typescript-eslint/naming-convention": "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/apps/vr-tests/.eslintrc.json b/apps/vr-tests/.eslintrc.json index b3f394f7cbaca..54ac641d7b3bf 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-cra-template-e9bc2562-933b-49d3-b708-7115c08a4810.json b/change/@fluentui-cra-template-e9bc2562-933b-49d3-b708-7115c08a4810.json new file mode 100644 index 0000000000000..a0cb086c51e6a --- /dev/null +++ b/change/@fluentui-cra-template-e9bc2562-933b-49d3-b708-7115c08a4810.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable lint for no-restricted-globals", + "packageName": "@fluentui/cra-template", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-dom-utilities-18f5f20c-92d5-4e9f-bd2d-494657065c63.json b/change/@fluentui-dom-utilities-18f5f20c-92d5-4e9f-bd2d-494657065c63.json new file mode 100644 index 0000000000000..e992714bdc096 --- /dev/null +++ b/change/@fluentui-dom-utilities-18f5f20c-92d5-4e9f-bd2d-494657065c63.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: allow `document` and `window` globals to be passed in where needed.", + "packageName": "@fluentui/dom-utilities", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-eslint-plugin-12ef753c-74ec-4c3c-8e65-e5121c05bde3.json b/change/@fluentui-eslint-plugin-12ef753c-74ec-4c3c-8e65-e5121c05bde3.json new file mode 100644 index 0000000000000..f842f73133064 --- /dev/null +++ b/change/@fluentui-eslint-plugin-12ef753c-74ec-4c3c-8e65-e5121c05bde3.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable global `document` and `window` access for React.", + "packageName": "@fluentui/eslint-plugin", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-global-context-6f67cc92-8e93-4c13-9e33-cc0c6d64fb49.json b/change/@fluentui-global-context-6f67cc92-8e93-4c13-9e33-cc0c6d64fb49.json new file mode 100644 index 0000000000000..5ddef01cf7e61 --- /dev/null +++ b/change/@fluentui-global-context-6f67cc92-8e93-4c13-9e33-cc0c6d64fb49.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "chore: disable lint for no-restricted-globals", + "packageName": "@fluentui/global-context", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-merge-styles-41e63bcf-870e-4e2b-bef8-645de9934120.json b/change/@fluentui-merge-styles-41e63bcf-870e-4e2b-bef8-645de9934120.json new file mode 100644 index 0000000000000..099de7d83f5ba --- /dev/null +++ b/change/@fluentui-merge-styles-41e63bcf-870e-4e2b-bef8-645de9934120.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: address lint for restricted globals", + "packageName": "@fluentui/merge-styles", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-public-docsite-setup-99131765-5261-4a19-9b79-7a22a24cdf75.json b/change/@fluentui-public-docsite-setup-99131765-5261-4a19-9b79-7a22a24cdf75.json new file mode 100644 index 0000000000000..4e969bd98ac0f --- /dev/null +++ b/change/@fluentui-public-docsite-setup-99131765-5261-4a19-9b79-7a22a24cdf75.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: lint errors", + "packageName": "@fluentui/public-docsite-setup", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-118c9f1a-8f95-44bc-9e6c-f1e1258a8aaf.json b/change/@fluentui-react-118c9f1a-8f95-44bc-9e6c-f1e1258a8aaf.json new file mode 100644 index 0000000000000..d02e573529a19 --- /dev/null +++ b/change/@fluentui-react-118c9f1a-8f95-44bc-9e6c-f1e1258a8aaf.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: use `document` and `window` globals from context", + "packageName": "@fluentui/react", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-calendar-compat-c72f4681-3f40-49bc-9184-195307bcda06.json b/change/@fluentui-react-calendar-compat-c72f4681-3f40-49bc-9184-195307bcda06.json new file mode 100644 index 0000000000000..b7ce9c8ebf884 --- /dev/null +++ b/change/@fluentui-react-calendar-compat-c72f4681-3f40-49bc-9184-195307bcda06.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: use globals from context", + "packageName": "@fluentui/react-calendar-compat", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-datepicker-compat-0fe329a8-786d-4a84-af00-4bb8afba2a3d.json b/change/@fluentui-react-datepicker-compat-0fe329a8-786d-4a84-af00-4bb8afba2a3d.json new file mode 100644 index 0000000000000..99877cd61e766 --- /dev/null +++ b/change/@fluentui-react-datepicker-compat-0fe329a8-786d-4a84-af00-4bb8afba2a3d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable lint for no-restricted-globals", + "packageName": "@fluentui/react-datepicker-compat", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-file-type-icons-05eaab0e-2b0a-4cf7-8b58-5da26471a17a.json b/change/@fluentui-react-file-type-icons-05eaab0e-2b0a-4cf7-8b58-5da26471a17a.json new file mode 100644 index 0000000000000..464da2e0e0cb1 --- /dev/null +++ b/change/@fluentui-react-file-type-icons-05eaab0e-2b0a-4cf7-8b58-5da26471a17a.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: update `getFileTypeIconsSuffix()` to take optional parameter for `window` reference.", + "packageName": "@fluentui/react-file-type-icons", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-focus-de2be8a6-f279-4e12-9677-371c811e5831.json b/change/@fluentui-react-focus-de2be8a6-f279-4e12-9677-371c811e5831.json new file mode 100644 index 0000000000000..6e2a5dc114b25 --- /dev/null +++ b/change/@fluentui-react-focus-de2be8a6-f279-4e12-9677-371c811e5831.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: allow `document` and `window` globals to be passed in where needed.", + "packageName": "@fluentui/react-focus", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-motion-preview-b9bda7bc-5381-412e-9808-2c0a86796435.json b/change/@fluentui-react-motion-preview-b9bda7bc-5381-412e-9808-2c0a86796435.json new file mode 100644 index 0000000000000..64b355b62a3c4 --- /dev/null +++ b/change/@fluentui-react-motion-preview-b9bda7bc-5381-412e-9808-2c0a86796435.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable lint for no-retricted-globals", + "packageName": "@fluentui/react-motion-preview", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-shared-contexts-44f55996-a722-481d-bec7-a74dcaf5eeaa.json b/change/@fluentui-react-shared-contexts-44f55996-a722-481d-bec7-a74dcaf5eeaa.json new file mode 100644 index 0000000000000..c01002bdd714c --- /dev/null +++ b/change/@fluentui-react-shared-contexts-44f55996-a722-481d-bec7-a74dcaf5eeaa.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable lint for no-restricted-globals", + "packageName": "@fluentui/react-shared-contexts", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-utilities-0dc8f037-947b-456a-bd8e-f93489587224.json b/change/@fluentui-react-utilities-0dc8f037-947b-456a-bd8e-f93489587224.json new file mode 100644 index 0000000000000..d43d0ca718193 --- /dev/null +++ b/change/@fluentui-react-utilities-0dc8f037-947b-456a-bd8e-f93489587224.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update to get `window` reference via `getWindow()`.", + "packageName": "@fluentui/react-utilities", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-window-provider-6899238f-933b-4bb8-9c5e-27c84a62b74c.json b/change/@fluentui-react-window-provider-6899238f-933b-4bb8-9c5e-27c84a62b74c.json new file mode 100644 index 0000000000000..81b046294617d --- /dev/null +++ b/change/@fluentui-react-window-provider-6899238f-933b-4bb8-9c5e-27c84a62b74c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: lint errors and explicit exports", + "packageName": "@fluentui/react-window-provider", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-set-version-2eb367d7-e877-4209-9ae4-72a3c1096a55.json b/change/@fluentui-set-version-2eb367d7-e877-4209-9ae4-72a3c1096a55.json new file mode 100644 index 0000000000000..a26ee2a09da74 --- /dev/null +++ b/change/@fluentui-set-version-2eb367d7-e877-4209-9ae4-72a3c1096a55.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: lint exception comment", + "packageName": "@fluentui/set-version", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-utilities-530b2e82-88d8-48c2-aba1-1b791c2bed2c.json b/change/@fluentui-utilities-530b2e82-88d8-48c2-aba1-1b791c2bed2c.json new file mode 100644 index 0000000000000..2b78746fb4f44 --- /dev/null +++ b/change/@fluentui-utilities-530b2e82-88d8-48c2-aba1-1b791c2bed2c.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: allow `document` and `window` globals to be passed in where needed.", + "packageName": "@fluentui/utilities", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/cra-template/template/src/index.tsx b/packages/cra-template/template/src/index.tsx index 90113a51c9827..6d72dddd55759 100644 --- a/packages/cra-template/template/src/index.tsx +++ b/packages/cra-template/template/src/index.tsx @@ -13,7 +13,11 @@ mergeStyles({ }, }); -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + , + // eslint-disable-next-line no-restricted-globals + document.getElementById('root'), +); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) diff --git a/packages/dom-utilities/etc/dom-utilities.api.md b/packages/dom-utilities/etc/dom-utilities.api.md index 24373f59c4fcc..2e27fa6ecb197 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 04a53bc19884c..8a88beeb4a90d 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 9dc8118575595..afac46bc46f25 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 a5b9391aa576a..979b364d3e1a7 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-config.js b/packages/eslint-plugin/src/configs/react-config.js index 2663520b69bd5..fed0b8662460f 100644 --- a/packages/eslint-plugin/src/configs/react-config.js +++ b/packages/eslint-plugin/src/configs/react-config.js @@ -1,4 +1,5 @@ const configHelpers = require('../utils/configHelpers'); +const { react: restrictedGlobals } = require('./restricted-globals'); /** @type {import("eslint").Linter.Config} */ module.exports = { @@ -28,6 +29,7 @@ module.exports = { '@griffel/hook-naming': 'error', '@griffel/no-shorthands': 'error', '@griffel/styles-file': 'error', + 'no-restricted-globals': restrictedGlobals, /** * react eslint rules * @see https://github.com/yannickcr/eslint-plugin-react @@ -121,6 +123,7 @@ module.exports = { // Test overrides files: [...configHelpers.testFiles], rules: { + 'no-restricted-globals': 'off', 'react/jsx-no-bind': 'off', }, }, @@ -129,6 +132,7 @@ module.exports = { rules: { // allow makeStyles calls in stories as examples should be defined in a single file '@griffel/styles-file': 'off', + 'no-restricted-globals': 'off', // allow arrow functions in stories for now (may want to change this later since using // constantly-mutating functions can be an anti-pattern which we may not want to demonstrate // in our converged components docs; it happened to be allowed starting out because .stories diff --git a/packages/eslint-plugin/src/configs/react-legacy.js b/packages/eslint-plugin/src/configs/react-legacy.js index 8d696ec41bfa1..b267a6610b408 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,6 +10,16 @@ module.exports = { rules: { 'jsdoc/check-tag-names': 'off', '@griffel/no-shorthands': '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 new file mode 100644 index 0000000000000..f645af6956606 --- /dev/null +++ b/packages/eslint-plugin/src/configs/restricted-globals.js @@ -0,0 +1,272 @@ +// Via: https://developer.mozilla.org/en-US/docs/Web/API/Window +// const windowKeys = [ +// // Instance properties +// 'caches', +// 'clientInformation', +// 'closed', +// 'console', +// 'credentialless', +// 'crypto', +// 'customElements', +// 'devicePixelRatio', +// 'frameElement', +// 'frames', +// 'fullScreen', +// 'history', +// 'indexedDB', +// 'innerHeight', +// 'innerWidth', +// 'isSecureContext', +// 'launchQueue', +// 'length', +// 'localStorage', +// 'location', +// 'locationbar', +// 'menubar', +// 'name', +// 'navigation', +// 'navigator', +// 'opener', +// 'origin', +// 'outerHeight', +// 'outerWidth', +// 'pageXOffset', +// 'pageYOffset', +// 'parent', +// 'performance', +// 'personalbar', +// 'scheduler', +// 'screen', +// 'screenX', +// 'screenY', +// 'scrollbars', +// 'scrollX', +// 'scrollY', +// 'self', +// 'sessionStorage', +// 'speechSynthesis', +// 'statusbar', +// 'toolbar', +// 'top', +// 'visualViewport', +// 'window', + +// // Deprecated properties +// 'defaultStatus', +// 'event', +// 'external', +// 'orientation', +// 'returnValue', +// 'sidebar', +// 'status', + +// // Instance methods +// 'addEventListener', +// 'alert', +// 'atob', +// 'blur', +// 'btoa', +// 'cancelAnimationFrame', +// 'cancelIdleCallback', +// 'clearImmediate', +// 'clearInterval', +// 'clearTimeout', +// 'close', +// 'confirm', +// 'createImageBitmap', +// 'dispatchEvent', +// 'dump', +// 'fetch', +// 'find', +// 'focus', +// 'getComputedStyle', +// 'getSelection', +// 'matchMedia', +// 'moveBy', +// 'moveTo', +// 'open', +// 'postMessage', +// 'print', +// 'prompt', +// 'queryLocalFonts', +// 'removeEventListener', +// 'reportError', +// 'requestAnimationFrame', +// 'requestIdleCallback', +// 'resizeBy', +// 'resizeTo', +// 'scroll', +// 'scrollBy', +// 'scrollTo', +// 'setImmediate', +// 'setInterval', +// 'setTimeout', +// 'showDirectoryPicker', +// 'showOpenFilePicker', +// 'showSaveFilePicker', +// 'stop', +// 'updateCommands', + +// // Deprecated methods +// 'back', +// 'captureEvents', +// 'forward', +// 'releaseEvents', +// 'showModalDialog', + +// // Events +// 'onabort', +// 'onafterprint', +// 'onanimationend', +// 'onanimationiteration', +// 'onanimationstart', +// 'onappinstalled', +// 'onauxclick', +// 'onbeforeinput', +// 'onbeforeinstallprompt', +// 'onbeforematch', +// 'onbeforeprint', +// 'onbeforetoggle', +// 'onbeforeunload', +// 'onbeforexrselect', +// 'onblur', +// 'oncancel', +// 'oncanplay', +// 'oncanplaythrough', +// 'onchange', +// 'onclick', +// 'onclose', +// 'oncontentvisibilityautostatechange', +// 'oncontextlost', +// 'oncontextmenu', +// 'oncontextrestored', +// 'oncuechange', +// 'ondblclick', +// 'ondevicemotion', +// 'ondeviceorientation', +// 'ondeviceorientationabsolute', +// 'ondrag', +// 'ondragend', +// 'ondragenter', +// 'ondragleave', +// 'ondragover', +// 'ondragstart', +// 'ondrop', +// 'ondurationchange', +// 'onemptied', +// 'onended', +// 'onerror', +// 'onfocus', +// 'onformdata', +// 'ongotpointercapture', +// 'onhashchange', +// 'oninput', +// 'oninvalid', +// 'onkeydown', +// 'onkeypress', +// 'onkeyup', +// 'onlanguagechange', +// 'onload', +// 'onloadeddata', +// 'onloadedmetadata', +// 'onloadstart', +// 'onlostpointercapture', +// 'onmessage', +// 'onmessageerror', +// 'onmousedown', +// 'onmouseenter', +// 'onmouseleave', +// 'onmousemove', +// 'onmouseout', +// 'onmouseover', +// 'onmouseup', +// 'onmousewheel', +// 'onoffline', +// 'ononline', +// 'onoverscroll', +// 'onpagehide', +// 'onpageshow', +// 'onpause', +// 'onplay', +// 'onplaying', +// 'onpointercancel', +// 'onpointerdown', +// 'onpointerenter', +// 'onpointerleave', +// 'onpointermove', +// 'onpointerout', +// 'onpointerover', +// 'onpointerrawupdate', +// 'onpointerup', +// 'onpopstate', +// 'onprogress', +// 'onratechange', +// 'onrejectionhandled', +// 'onreset', +// 'onresize', +// 'onscroll', +// 'onscrollend', +// 'onsearch', +// 'onsecuritypolicyviolation', +// 'onseeked', +// 'onseeking', +// 'onselect', +// 'onselectionchange', +// 'onselectstart', +// 'onslotchange', +// 'onstalled', +// 'onstorage', +// 'onsubmit', +// 'onsuspend', +// 'ontimeupdate', +// 'ontimezonechange', +// 'ontoggle', +// 'ontransitioncancel', +// 'ontransitionend', +// 'ontransitionrun', +// 'ontransitionstart', +// 'onunhandledrejection', +// 'onunload', +// 'onvolumechange', +// 'onwaiting', +// 'onwebkitanimationend', +// 'onwebkitanimationiteration', +// 'onwebkitanimationstart', +// 'onwebkittransitionend', +// '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', + { + name: 'window', + message: 'Get a reference to `window` from `useFluent()`.', + }, + { + name: 'document', + message: 'Get a reference to `document` from `useFluent()`.', + }, + // ...windowKeys.map(key => { + // return { + // name: key, + // message: `Get a reference to \`window\` from \`useFluent()\` and access \`${key}\` from there.`, + // }; + // }), +]; + +module.exports = { + reactLegacy, + react, +}; diff --git a/packages/eslint-plugin/src/utils/configHelpers.js b/packages/eslint-plugin/src/utils/configHelpers.js index 496ef038a2dc5..37af2f6c4f7c0 100644 --- a/packages/eslint-plugin/src/utils/configHelpers.js +++ b/packages/eslint-plugin/src/utils/configHelpers.js @@ -31,7 +31,7 @@ function getProjectMetadata(options) { } const testFiles = [ - '**/*{.,-}{test,spec,e2e}.{ts,tsx}', + '**/*{.,-}{test,spec,e2e,cy}.{ts,tsx}', '**/{test,tests}/**', '**/testUtilities.{ts,tsx}', '**/common/{isConformant,snapshotSerializers}.{ts,tsx}', diff --git a/packages/merge-styles/src/StyleOptionsState.ts b/packages/merge-styles/src/StyleOptionsState.ts index c59ad4bba7ca9..d1eda55135b62 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 3be6223b3d311..e291a23cac9fd 100644 --- a/packages/merge-styles/src/Stylesheet.ts +++ b/packages/merge-styles/src/Stylesheet.ts @@ -1,3 +1,4 @@ +/* eslint no-restricted-globals: 0 */ // globals in stylesheets will be addressed in a separate PR import { IStyle } from './IStyle'; export const InjectionMode = { diff --git a/packages/merge-styles/src/getVendorSettings.ts b/packages/merge-styles/src/getVendorSettings.ts index dfdcbdba55797..41ba34307a398 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/public-docsite-setup/src/index.ts b/packages/public-docsite-setup/src/index.ts index a898ed053c65f..58f28f8b85016 100644 --- a/packages/public-docsite-setup/src/index.ts +++ b/packages/public-docsite-setup/src/index.ts @@ -1,3 +1,3 @@ -export * from './constants'; -export * from './types'; +export { BUNDLE_NAME, MANIFEST_NAME_FORMAT, MANIFEST_VARIANTS } from './constants'; +export type { VersionMenuItem, VersionSwitcherDefinition, SiteConfig, SiteGlobals, ManifestVariant } from './types'; // loadSite is NOT exported (see comment in file) diff --git a/packages/public-docsite-setup/src/loadSite.ts b/packages/public-docsite-setup/src/loadSite.ts index 5e87676f24748..b97e1d15a9981 100644 --- a/packages/public-docsite-setup/src/loadSite.ts +++ b/packages/public-docsite-setup/src/loadSite.ts @@ -186,7 +186,7 @@ function loadSiteInternal(options: { // This is used by the example editor in 7+ (but having it defined in 5-6 is harmless) window.MonacoConfig = { - baseUrl: baseUrl, + baseUrl, useMinified, crossDomain: true, }; @@ -214,10 +214,12 @@ function getParameterByName(name: string) { } function loadScript(src: string, onLoad?: () => void, onError?: () => void) { + // eslint-disable-next-line no-restricted-globals const script = document.createElement('script'); script.src = src; script.onload = onLoad || null; script.onerror = onError || null; + // eslint-disable-next-line no-restricted-globals document.head.appendChild(script); } diff --git a/packages/react-charting/.eslintrc.json b/packages/react-charting/.eslintrc.json index 20576f82da422..478004905dea8 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-components/global-context/src/global-context-selector.ts b/packages/react-components/global-context/src/global-context-selector.ts index ec2d843afd7e6..1ad1982ebc2dc 100644 --- a/packages/react-components/global-context/src/global-context-selector.ts +++ b/packages/react-components/global-context/src/global-context-selector.ts @@ -5,7 +5,10 @@ import { getMajorVersion } from './utils'; import { GlobalObject } from './types'; const isBrowser = canUseDOM(); -const globalObject: GlobalObject = isBrowser ? window : global; +const globalObject: GlobalObject = isBrowser + ? // eslint-disable-next-line no-restricted-globals + window + : global; // Identifier for the symbol, for easy idenfitifaction of symbols created by this util // Useful for clearning global object during SSR reloads diff --git a/packages/react-components/global-context/src/global-context.ts b/packages/react-components/global-context/src/global-context.ts index a994ddea8f0e4..ad117ce1b8f3e 100644 --- a/packages/react-components/global-context/src/global-context.ts +++ b/packages/react-components/global-context/src/global-context.ts @@ -4,7 +4,10 @@ import { GlobalObject } from './types'; import { getMajorVersion } from './utils'; const isBrowser = canUseDOM(); -const globalObject: GlobalObject = isBrowser ? window : global; +const globalObject: GlobalObject = isBrowser + ? // eslint-disable-next-line no-restricted-globals + window + : global; // Identifier for the symbol, for easy idenfitifaction of symbols created by this util // Useful for clearning global object during SSR reloads diff --git a/packages/react-components/react-calendar-compat/src/components/Calendar/Calendar.tsx b/packages/react-components/react-calendar-compat/src/components/Calendar/Calendar.tsx index 0d3cc43404a39..106d7ba119ec9 100644 --- a/packages/react-components/react-calendar-compat/src/components/Calendar/Calendar.tsx +++ b/packages/react-components/react-calendar-compat/src/components/Calendar/Calendar.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Backspace, Enter, Escape, PageDown, PageUp, Space } from '@fluentui/keyboard-keys'; import { useControllableState } from '@fluentui/react-utilities'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { addMonths, addYears, @@ -10,7 +11,6 @@ import { DEFAULT_DATE_FORMATTING, FirstWeekOfYear, focusAsync, - getWindow, } from '../../utils'; import { CalendarDay } from '../CalendarDay/CalendarDay'; import { CalendarMonth } from '../CalendarMonth/CalendarMonth'; @@ -76,20 +76,20 @@ function useVisibilityState({ showMonthPickerAsOverlay, }: CalendarProps) { /** State used to show/hide month picker */ + const showMonthPickerAsOverlayState = useShowMonthPickerAsOverloay({ + isDayPickerVisible: isDayPickerVisibleProp, + showMonthPickerAsOverlay, + }); const [isMonthPickerVisible, setIsMonthPickerVisible] = useControllableState({ defaultState: false, initialState: true, - state: getShowMonthPickerAsOverlay({ isDayPickerVisible: isDayPickerVisibleProp, showMonthPickerAsOverlay }) - ? undefined - : isMonthPickerVisibleProp, + state: showMonthPickerAsOverlayState ? undefined : isMonthPickerVisibleProp, }); /** State used to show/hide day picker */ const [isDayPickerVisible, setIsDayPickerVisible] = useControllableState({ defaultState: true, initialState: true, - state: getShowMonthPickerAsOverlay({ isDayPickerVisible: isDayPickerVisibleProp, showMonthPickerAsOverlay }) - ? undefined - : isDayPickerVisibleProp, + state: showMonthPickerAsOverlayState ? undefined : isDayPickerVisibleProp, }); const toggleDayMonthPickerVisibility = () => { @@ -104,14 +104,16 @@ function useFocusLogic({ componentRef }: CalendarProps, isDayPickerVisible: bool const dayPicker = React.useRef(null); const monthPicker = React.useRef(null); const focusOnUpdate = React.useRef(false); + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; const focus = React.useCallback(() => { if (isDayPickerVisible && dayPicker.current) { - focusAsync(dayPicker.current); + focusAsync(dayPicker.current, win); } else if (isMonthPickerVisible && monthPicker.current) { - focusAsync(monthPicker.current); + focusAsync(monthPicker.current, win); } - }, [isDayPickerVisible, isMonthPickerVisible]); + }, [isDayPickerVisible, isMonthPickerVisible, win]); React.useImperativeHandle(componentRef, () => ({ focus }), [focus]); @@ -231,10 +233,12 @@ export const Calendar: React.FunctionComponent = React.forwardRef navigateDay(date); }; - const onHeaderSelect = getShowMonthPickerAsOverlay({ + const showMonthPickerAsOverlay = useShowMonthPickerAsOverloay({ isDayPickerVisible: isDayPickerVisibleProp, showMonthPickerAsOverlay: showMonthPickerAsOverlayProp, - }) + }); + + const onHeaderSelect = showMonthPickerAsOverlay ? (): void => { toggleDayMonthPickerVisibility(); @@ -297,10 +301,6 @@ export const Calendar: React.FunctionComponent = React.forwardRef break; } }; - const showMonthPickerAsOverlay = getShowMonthPickerAsOverlay({ - isDayPickerVisible: isDayPickerVisibleProp, - showMonthPickerAsOverlay: showMonthPickerAsOverlayProp, - }); const monthPickerOnly = !showMonthPickerAsOverlay && !isDayPickerVisible; @@ -401,7 +401,8 @@ export const Calendar: React.FunctionComponent = React.forwardRef ); Calendar.displayName = 'Calendar'; -function getShowMonthPickerAsOverlay({ isDayPickerVisible, showMonthPickerAsOverlay }: CalendarProps) { - const win = getWindow(); +const useShowMonthPickerAsOverloay = ({ isDayPickerVisible, showMonthPickerAsOverlay }: CalendarProps) => { + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; return showMonthPickerAsOverlay || (isDayPickerVisible && win && win.innerWidth <= MIN_SIZE_FORCE_OVERLAY); -} +}; diff --git a/packages/react-components/react-calendar-compat/src/utils/dom.ts b/packages/react-components/react-calendar-compat/src/utils/dom.ts deleted file mode 100644 index 4ce45e2fa22b1..0000000000000 --- a/packages/react-components/react-calendar-compat/src/utils/dom.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { canUseDOM } from '@fluentui/react-utilities'; - -export function getWindow(targetElement?: Element | null): Window | undefined { - if (!canUseDOM() || typeof window === 'undefined') { - return undefined; - } - - const el = targetElement as Element; - - return el && el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : window; -} diff --git a/packages/react-components/react-calendar-compat/src/utils/focus.ts b/packages/react-components/react-calendar-compat/src/utils/focus.ts index 97bc5ddb7bfa8..ce48f80e48029 100644 --- a/packages/react-components/react-calendar-compat/src/utils/focus.ts +++ b/packages/react-components/react-calendar-compat/src/utils/focus.ts @@ -1,5 +1,3 @@ -import { getWindow } from './dom'; - let targetToFocusOnNextRepaint: HTMLElement | { focus: () => void } | null | undefined = undefined; /** @@ -8,7 +6,10 @@ let targetToFocusOnNextRepaint: HTMLElement | { focus: () => void } | null | und * only the latest called focusAsync element will actually be focused * @param element - The element to focus */ -export function focusAsync(element: HTMLElement | { focus: () => void } | undefined | null): void { +export function focusAsync( + element: HTMLElement | { focus: () => void } | undefined | null, + win: Window | undefined | null, +): void { if (element) { // An element was already queued to be focused, so replace that one with the new element if (targetToFocusOnNextRepaint) { @@ -18,8 +19,6 @@ export function focusAsync(element: HTMLElement | { focus: () => void } | undefi targetToFocusOnNextRepaint = element; - const win = getWindow(element as Element); - if (win) { // element.focus() is a no-op if the element is no longer in the DOM, meaning this is always safe win.requestAnimationFrame(() => { diff --git a/packages/react-components/react-calendar-compat/src/utils/index.ts b/packages/react-components/react-calendar-compat/src/utils/index.ts index 488020805bb61..cb8c73d5cadcb 100644 --- a/packages/react-components/react-calendar-compat/src/utils/index.ts +++ b/packages/react-components/react-calendar-compat/src/utils/index.ts @@ -3,5 +3,4 @@ export * from './constants'; export * from './dateFormatting'; export * from './dateGrid'; export * from './dateMath'; -export * from './dom'; export * from './focus'; diff --git a/packages/react-components/react-components/stories/AccessibilityScenarios/utils.tsx b/packages/react-components/react-components/stories/AccessibilityScenarios/utils.tsx index 06438c6e95fc4..540d8e94f0f62 100644 --- a/packages/react-components/react-components/stories/AccessibilityScenarios/utils.tsx +++ b/packages/react-components/react-components/stories/AccessibilityScenarios/utils.tsx @@ -30,6 +30,7 @@ export const BackLink = () => Go back to main menu = ({ pageTitle, children }) => { React.useEffect(() => { + // eslint-disable-next-line no-restricted-globals document.title = pageTitle + APP_TITLE_SEPARATOR + APP_TITLE; }, [pageTitle]); diff --git a/packages/react-components/react-datepicker-compat/src/components/Calendar/Calendar.tsx b/packages/react-components/react-datepicker-compat/src/components/Calendar/Calendar.tsx new file mode 100644 index 0000000000000..106d7ba119ec9 --- /dev/null +++ b/packages/react-components/react-datepicker-compat/src/components/Calendar/Calendar.tsx @@ -0,0 +1,408 @@ +import * as React from 'react'; +import { Backspace, Enter, Escape, PageDown, PageUp, Space } from '@fluentui/keyboard-keys'; +import { useControllableState } from '@fluentui/react-utilities'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +import { + addMonths, + addYears, + DateRangeType, + DayOfWeek, + DEFAULT_CALENDAR_STRINGS, + DEFAULT_DATE_FORMATTING, + FirstWeekOfYear, + focusAsync, +} from '../../utils'; +import { CalendarDay } from '../CalendarDay/CalendarDay'; +import { CalendarMonth } from '../CalendarMonth/CalendarMonth'; +import { useCalendarStyles_unstable } from './useCalendarStyles.styles'; +import type { ICalendarDay } from '../CalendarDay/CalendarDay.types'; +import type { ICalendarMonth } from '../CalendarMonth/CalendarMonth.types'; +import type { CalendarProps } from './Calendar.types'; + +const MIN_SIZE_FORCE_OVERLAY = 440; + +const defaultWorkWeekDays: DayOfWeek[] = [ + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, +]; + +function useDateState({ value, today = new Date(), onSelectDate }: CalendarProps) { + /** The currently selected date in the calendar */ + const [selectedDate, setSelectedDate] = useControllableState({ + defaultState: today, + initialState: today, + state: value, + }); + + /** The currently focused date in the day picker, but not necessarily selected */ + const [navigatedDay = today, setNavigatedDay] = React.useState(value); + + /** The currently focused date in the month picker, but not necessarily selected */ + const [navigatedMonth = today, setNavigatedMonth] = React.useState(value); + + /** If using a controlled value, when that value changes, navigate to that date */ + const [lastSelectedDate = today, setLastSelectedDate] = React.useState(value); + if (value && lastSelectedDate.valueOf() !== value.valueOf()) { + setNavigatedDay(value); + setNavigatedMonth(value); + setLastSelectedDate(value); + } + + const navigateMonth = (date: Date) => { + setNavigatedMonth(date); + }; + + const navigateDay = (date: Date) => { + setNavigatedMonth(date); + setNavigatedDay(date); + }; + + const onDateSelected = (date: Date, selectedDateRangeArray?: Date[]) => { + setNavigatedMonth(date); + setNavigatedDay(date); + setSelectedDate(date); + onSelectDate?.(date, selectedDateRangeArray); + }; + + return [selectedDate, navigatedDay, navigatedMonth, onDateSelected, navigateDay, navigateMonth] as const; +} + +function useVisibilityState({ + isDayPickerVisible: isDayPickerVisibleProp, + isMonthPickerVisible: isMonthPickerVisibleProp, + showMonthPickerAsOverlay, +}: CalendarProps) { + /** State used to show/hide month picker */ + const showMonthPickerAsOverlayState = useShowMonthPickerAsOverloay({ + isDayPickerVisible: isDayPickerVisibleProp, + showMonthPickerAsOverlay, + }); + const [isMonthPickerVisible, setIsMonthPickerVisible] = useControllableState({ + defaultState: false, + initialState: true, + state: showMonthPickerAsOverlayState ? undefined : isMonthPickerVisibleProp, + }); + /** State used to show/hide day picker */ + const [isDayPickerVisible, setIsDayPickerVisible] = useControllableState({ + defaultState: true, + initialState: true, + state: showMonthPickerAsOverlayState ? undefined : isDayPickerVisibleProp, + }); + + const toggleDayMonthPickerVisibility = () => { + setIsMonthPickerVisible(!isMonthPickerVisible); + setIsDayPickerVisible(!isDayPickerVisible); + }; + + return [isMonthPickerVisible, isDayPickerVisible, toggleDayMonthPickerVisibility] as const; +} + +function useFocusLogic({ componentRef }: CalendarProps, isDayPickerVisible: boolean, isMonthPickerVisible: boolean) { + const dayPicker = React.useRef(null); + const monthPicker = React.useRef(null); + const focusOnUpdate = React.useRef(false); + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; + + const focus = React.useCallback(() => { + if (isDayPickerVisible && dayPicker.current) { + focusAsync(dayPicker.current, win); + } else if (isMonthPickerVisible && monthPicker.current) { + focusAsync(monthPicker.current, win); + } + }, [isDayPickerVisible, isMonthPickerVisible, win]); + + React.useImperativeHandle(componentRef, () => ({ focus }), [focus]); + + React.useEffect(() => { + if (focusOnUpdate.current) { + focus(); + focusOnUpdate.current = false; + } + }); + + const focusOnNextUpdate = () => { + focusOnUpdate.current = true; + }; + + return [dayPicker, monthPicker, focusOnNextUpdate] as const; +} + +/** + * @internal + */ +export const Calendar: React.FunctionComponent = React.forwardRef( + (props, forwardedRef) => { + const { + allFocusable = false, + calendarDayProps, + calendarMonthProps, + className, + componentRef, + dateRangeType = DateRangeType.Day, + dateTimeFormatter = DEFAULT_DATE_FORMATTING, + firstDayOfWeek = DayOfWeek.Sunday, + firstWeekOfYear = FirstWeekOfYear.FirstDay, + highlightCurrentMonth = false, + highlightSelectedMonth = false, + id, + isDayPickerVisible: isDayPickerVisibleProp = true, + isMonthPickerVisible: isMonthPickerVisibleProp = true, + maxDate, + minDate, + onDismiss, + onSelectDate, + restrictedDates, + showCloseButton = false, + showGoToToday = true, + showMonthPickerAsOverlay: showMonthPickerAsOverlayProp = false, + showSixWeeksByDefault = false, + showWeekNumbers = false, + strings = DEFAULT_CALENDAR_STRINGS, + today = new Date(), + value, + workWeekDays = defaultWorkWeekDays, + } = props; + + const [selectedDate, navigatedDay, navigatedMonth, onDateSelected, navigateDay, navigateMonth] = useDateState({ + onSelectDate, + value, + today, + }); + const [isMonthPickerVisible, isDayPickerVisible, toggleDayMonthPickerVisibility] = useVisibilityState({ + isDayPickerVisible: isDayPickerVisibleProp, + isMonthPickerVisible: isMonthPickerVisibleProp, + showMonthPickerAsOverlay: showMonthPickerAsOverlayProp, + }); + const [dayPicker, monthPicker, focusOnNextUpdate] = useFocusLogic( + { componentRef }, + isDayPickerVisible, + isMonthPickerVisible, + ); + + const renderGoToTodayButton = () => { + let goTodayEnabled = showGoToToday; + + if (goTodayEnabled && today) { + goTodayEnabled = + navigatedDay.getFullYear() !== today.getFullYear() || + navigatedDay.getMonth() !== today.getMonth() || + navigatedMonth.getFullYear() !== today.getFullYear() || + navigatedMonth.getMonth() !== today.getMonth(); + } + + return ( + showGoToToday && ( + + ) + ); + }; + + const onNavigateDayDate = (date: Date, focusOnNavigatedDay: boolean): void => { + navigateDay(date); + if (focusOnNavigatedDay) { + focusOnNextUpdate(); + } + }; + + const onNavigateMonthDate = (date: Date, focusOnNavigatedDay: boolean): void => { + if (focusOnNavigatedDay) { + focusOnNextUpdate(); + } + + if (!focusOnNavigatedDay) { + navigateMonth(date); + return; + } + + if (monthPickerOnly) { + onDateSelected(date); + } + + navigateDay(date); + }; + + const showMonthPickerAsOverlay = useShowMonthPickerAsOverloay({ + isDayPickerVisible: isDayPickerVisibleProp, + showMonthPickerAsOverlay: showMonthPickerAsOverlayProp, + }); + + const onHeaderSelect = showMonthPickerAsOverlay + ? (): void => { + toggleDayMonthPickerVisibility(); + + focusOnNextUpdate(); + } + : undefined; + + const onGotoToday = (): void => { + navigateDay(today!); + focusOnNextUpdate(); + }; + + const onButtonKeyDown = (callback: () => void): ((ev: React.KeyboardEvent) => void) => { + return (ev: React.KeyboardEvent) => { + switch (ev.key) { + case Enter: + case Space: + callback(); + break; + } + }; + }; + + const onDatePickerPopupKeyDown = (ev: React.KeyboardEvent): void => { + switch (ev.key) { + case Enter: + ev.preventDefault(); + break; + + case Backspace: + ev.preventDefault(); + break; + + case Escape: + ev.stopPropagation(); + onDismiss?.(); + break; + + case PageUp: + if (ev.ctrlKey) { + // go to next year + navigateDay(addYears(navigatedDay, 1)); + } else { + // go to next month + navigateDay(addMonths(navigatedDay, 1)); + } + ev.preventDefault(); + break; + case PageDown: + if (ev.ctrlKey) { + // go to previous year + navigateDay(addYears(navigatedDay, -1)); + } else { + // go to previous month + navigateDay(addMonths(navigatedDay, -1)); + } + ev.preventDefault(); + break; + default: + break; + } + }; + + const monthPickerOnly = !showMonthPickerAsOverlay && !isDayPickerVisible; + + const classes = useCalendarStyles_unstable({ + className, + isDayPickerVisible, + isMonthPickerVisible, + showWeekNumbers, + }); + + let todayDateString: string = ''; + let selectedDateString: string = ''; + if (dateTimeFormatter && strings!.todayDateFormatString) { + todayDateString = strings!.todayDateFormatString.replace( + '{0}', + dateTimeFormatter.formatMonthDayYear(today, strings!), + ); + } + if (dateTimeFormatter && strings!.selectedDateFormatString) { + const dateStringFormatter = monthPickerOnly + ? dateTimeFormatter.formatMonthYear + : dateTimeFormatter.formatMonthDayYear; + selectedDateString = strings!.selectedDateFormatString.replace( + '{0}', + dateStringFormatter(selectedDate, strings!), + ); + } + const selectionAndTodayString = selectedDateString + ', ' + todayDateString; + + return ( +
+
+ {selectedDateString} +
+ {isDayPickerVisible && ( + + )} + {isDayPickerVisible && isMonthPickerVisible &&
} + {isMonthPickerVisible ? ( +
+ + {renderGoToTodayButton()} +
+ ) : ( + renderGoToTodayButton() + )} +
+ ); + }, +); +Calendar.displayName = 'Calendar'; + +const useShowMonthPickerAsOverloay = ({ isDayPickerVisible, showMonthPickerAsOverlay }: CalendarProps) => { + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; + return showMonthPickerAsOverlay || (isDayPickerVisible && win && win.innerWidth <= MIN_SIZE_FORCE_OVERLAY); +}; diff --git a/packages/react-components/react-datepicker-compat/src/utils/focus.ts b/packages/react-components/react-datepicker-compat/src/utils/focus.ts new file mode 100644 index 0000000000000..ce48f80e48029 --- /dev/null +++ b/packages/react-components/react-datepicker-compat/src/utils/focus.ts @@ -0,0 +1,32 @@ +let targetToFocusOnNextRepaint: HTMLElement | { focus: () => void } | null | undefined = undefined; + +/** + * Sets focus to an element asynchronously. The focus will be set at the next browser repaint, + * meaning it won't cause any extra recalculations. If more than one focusAsync is called during one frame, + * only the latest called focusAsync element will actually be focused + * @param element - The element to focus + */ +export function focusAsync( + element: HTMLElement | { focus: () => void } | undefined | null, + win: Window | undefined | null, +): void { + if (element) { + // An element was already queued to be focused, so replace that one with the new element + if (targetToFocusOnNextRepaint) { + targetToFocusOnNextRepaint = element; + return; + } + + targetToFocusOnNextRepaint = element; + + if (win) { + // element.focus() is a no-op if the element is no longer in the DOM, meaning this is always safe + win.requestAnimationFrame(() => { + targetToFocusOnNextRepaint && targetToFocusOnNextRepaint.focus(); + + // We are done focusing for this frame, so reset the queued focus element + targetToFocusOnNextRepaint = undefined; + }); + } + } +} diff --git a/packages/react-components/react-datepicker-compat/src/utils/index.ts b/packages/react-components/react-datepicker-compat/src/utils/index.ts new file mode 100644 index 0000000000000..cb8c73d5cadcb --- /dev/null +++ b/packages/react-components/react-datepicker-compat/src/utils/index.ts @@ -0,0 +1,6 @@ +export * from './animations'; +export * from './constants'; +export * from './dateFormatting'; +export * from './dateGrid'; +export * from './dateMath'; +export * from './focus'; diff --git a/packages/react-components/react-motion-preview/src/utils/dom-style.ts b/packages/react-components/react-motion-preview/src/utils/dom-style.ts index faa0aef71fa58..bbde104c95ffd 100644 --- a/packages/react-components/react-motion-preview/src/utils/dom-style.ts +++ b/packages/react-components/react-motion-preview/src/utils/dom-style.ts @@ -81,7 +81,11 @@ export const hasCSSOMSupport = (node: HTMLElementWithStyledMap) => { * @returns - CSS styles. */ export const getElementComputedStyle = (node: HTMLElement): CSSStyleDeclaration => { - const win = canUseDOM() && (node.ownerDocument?.defaultView ?? window); + const win = + canUseDOM() && + (node.ownerDocument?.defaultView ?? + // eslint-disable-next-line no-restricted-globals + window); if (!win) { return { diff --git a/packages/react-components/react-shared-contexts/src/ProviderContext/ProviderContext.ts b/packages/react-components/react-shared-contexts/src/ProviderContext/ProviderContext.ts index 5651db5554568..4b965b8e85a8f 100644 --- a/packages/react-components/react-shared-contexts/src/ProviderContext/ProviderContext.ts +++ b/packages/react-components/react-shared-contexts/src/ProviderContext/ProviderContext.ts @@ -16,6 +16,7 @@ const ProviderContext = React.createContext( ) as React.Context; const providerContextDefaultValue: ProviderContextValue = { + // eslint-disable-next-line no-restricted-globals targetDocument: typeof document === 'object' ? document : undefined, dir: 'ltr' as const, }; diff --git a/packages/react-components/react-utilities/package.json b/packages/react-components/react-utilities/package.json index 013aa0efb5c04..73d6cbf8169bc 100644 --- a/packages/react-components/react-utilities/package.json +++ b/packages/react-components/react-utilities/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-shared-contexts": "^9.10.0", "@swc/helpers": "^0.5.1" }, "peerDependencies": { diff --git a/packages/react-components/react-utilities/src/hooks/useOnClickOutside.ts b/packages/react-components/react-utilities/src/hooks/useOnClickOutside.ts index 74b414a8b8ed5..1ae1f9b3e5f1b 100644 --- a/packages/react-components/react-utilities/src/hooks/useOnClickOutside.ts +++ b/packages/react-components/react-utilities/src/hooks/useOnClickOutside.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import { useEventCallback } from './useEventCallback'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; /** * @internal @@ -45,6 +46,8 @@ const DEFAULT_CONTAINS: UseOnClickOrScrollOutsideOptions['contains'] = (parent, * Utility to perform checks where a click/touch event was made outside a component */ export const useOnClickOutside = (options: UseOnClickOrScrollOutsideOptions) => { + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; const { refs, callback, element, disabled, disabledFocusOnIframe, contains = DEFAULT_CONTAINS } = options; const timeoutId = React.useRef(undefined); @@ -80,7 +83,7 @@ export const useOnClickOutside = (options: UseOnClickOrScrollOutsideOptions) => // Store the current event to avoid triggering handlers immediately // Note this depends on a deprecated but extremely well supported quirk of the web platform // https://github.com/facebook/react/issues/20074 - let currentEvent = getWindowEvent(window); + let currentEvent = getWindowEvent(win); const conditionalHandler = (event: MouseEvent | TouchEvent) => { // Skip if this event is the same as the one running when we added the handlers @@ -99,7 +102,7 @@ export const useOnClickOutside = (options: UseOnClickOrScrollOutsideOptions) => element?.addEventListener('mousedown', handleMouseDown, true); // Garbage collect this event after it's no longer useful to avoid memory leaks - timeoutId.current = window.setTimeout(() => { + timeoutId.current = win?.setTimeout(() => { currentEvent = undefined; }, 1); @@ -109,13 +112,13 @@ export const useOnClickOutside = (options: UseOnClickOrScrollOutsideOptions) => element?.removeEventListener('contextmenu', conditionalHandler, true); element?.removeEventListener('mousedown', handleMouseDown, true); - clearTimeout(timeoutId.current); + win?.clearTimeout(timeoutId.current); currentEvent = undefined; }; - }, [listener, element, disabled, handleMouseDown]); + }, [listener, element, disabled, handleMouseDown, win]); }; -const getWindowEvent = (target: Node | Window): Event | undefined => { +const getWindowEvent = (target: Node | Window | null | undefined): Event | undefined => { if (target) { if (typeof (target as Window).window === 'object' && (target as Window).window === target) { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/react-components/react-utilities/src/ssr/canUseDOM.ts b/packages/react-components/react-utilities/src/ssr/canUseDOM.ts index 01520b98ccc7a..ee7d26eab76e3 100644 --- a/packages/react-components/react-utilities/src/ssr/canUseDOM.ts +++ b/packages/react-components/react-utilities/src/ssr/canUseDOM.ts @@ -3,10 +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 && // eslint-disable-line no-restricted-globals + // eslint-disable-next-line deprecation/deprecation, no-restricted-globals window.document.createElement ) ); diff --git a/packages/react-components/theme-designer/.eslintrc.json b/packages/react-components/theme-designer/.eslintrc.json index a86d9b8501ad5..eab62c7c347c8 100644 --- a/packages/react-components/theme-designer/.eslintrc.json +++ b/packages/react-components/theme-designer/.eslintrc.json @@ -2,6 +2,7 @@ "extends": ["plugin:@fluentui/eslint-plugin/react"], "root": true, "rules": { - "@griffel/styles-file": "off" + "@griffel/styles-file": "off", + "no-restricted-globals": "off" } } diff --git a/packages/react-docsite-components/.eslintrc.json b/packages/react-docsite-components/.eslintrc.json index a6ecfa7038e6b..ec2a316fd41b8 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 d4c9b90997c5e..4a59b1205ddc6 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 10a55ade40937..94cce37333d4c 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 48170857a98a3..6b67935fce28c 100644 --- a/packages/react-file-type-icons/src/getFileTypeIconProps.ts +++ b/packages/react-file-type-icons/src/getFileTypeIconProps.ts @@ -152,8 +152,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 a8ae33009abcc..f04745ebdc183 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 implements IFocu this._activeElement && elementContains(this._root.current, this._activeElement) && isElementTabbable(this._activeElement) && - (!bypassHiddenElements || isElementVisibleAndNotHidden(this._activeElement)) + (!bypassHiddenElements || + isElementVisibleAndNotHidden(this._activeElement, this._getDocument().defaultView ?? undefined)) ) { this._activeElement.focus(); return true; @@ -1442,7 +1445,7 @@ export class FocusZone extends React.Component implements IFocu * Returns true if the element is a descendant of the FocusZone through a React portal. */ private _portalContainsElement(element: HTMLElement): boolean { - return element && !!this._root.current && portalContainsElement(element, this._root.current); + return element && !!this._root.current && portalContainsElement(element, this._root.current, this._getDocument()); } private _getDocument(): Document { diff --git a/packages/react-monaco-editor/.eslintrc.json b/packages/react-monaco-editor/.eslintrc.json index 58655c94a33bc..2743cb1a1673d 100644 --- a/packages/react-monaco-editor/.eslintrc.json +++ b/packages/react-monaco-editor/.eslintrc.json @@ -2,6 +2,7 @@ "extends": ["plugin:@fluentui/eslint-plugin/react--legacy"], "root": true, "rules": { - "import/no-webpack-loader-syntax": "off" // ok in this project + "import/no-webpack-loader-syntax": "off", // ok in this project + "no-restricted-globals": "off" } } diff --git a/packages/react-window-provider/src/WindowProvider.tsx b/packages/react-window-provider/src/WindowProvider.tsx index 61758d0c12ddf..4b03b2d7ceb21 100644 --- a/packages/react-window-provider/src/WindowProvider.tsx +++ b/packages/react-window-provider/src/WindowProvider.tsx @@ -16,6 +16,7 @@ export type WindowProviderProps = { */ // eslint-disable-next-line @fluentui/no-context-default-value export const WindowContext = React.createContext({ + // eslint-disable-next-line no-restricted-globals window: typeof window === 'object' ? window : undefined, }); diff --git a/packages/react-window-provider/src/index.ts b/packages/react-window-provider/src/index.ts index 0283d9270b318..e132b1ab8d252 100644 --- a/packages/react-window-provider/src/index.ts +++ b/packages/react-window-provider/src/index.ts @@ -1,3 +1,10 @@ -export * from './WindowProvider'; +export { + // eslint-disable-next-line @fluentui/ban-context-export + WindowContext, + useWindow, + useDocument, + WindowProvider, +} from './WindowProvider'; +export type { WindowProviderProps } from './WindowProvider'; import './version'; diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index 46d51c6deb703..8175036057ddc 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -691,6 +691,8 @@ export class BasePicker> 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: () => { @@ -8124,6 +8130,8 @@ export interface IScrollablePaneContext { notifySubscribers: (sort?: boolean) => void; syncScrollSticky: (sticky: Sticky) => void; }; + // (undocumented) + window: Window | undefined; } // @public (undocumented) @@ -9769,6 +9777,8 @@ export class KeytipLayerBase extends React_2.Component; + // (undocumented) static defaultProps: IKeytipLayerProps; // (undocumented) getCurrentSequence(): string; @@ -9877,6 +9887,8 @@ export class List extends React_2.Component, IListState; + // (undocumented) static defaultProps: { startIndex: number; onRenderCell: (item: any, index: number, containsFocus: boolean) => JSX.Element; @@ -10040,6 +10052,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) @@ -10132,6 +10146,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; @@ -10395,13 +10411,13 @@ export enum Position { } // @public (undocumented) -export function positionCallout(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo): ICalloutPositionedInfo; +export function positionCallout(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo, 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; @@ -10582,6 +10598,8 @@ export class ScrollablePaneBase extends React_2.Component; + // (undocumented) forceLayoutUpdate(): void; // (undocumented) getScrollPosition: () => number; @@ -11348,6 +11366,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 5f666dd3be6c3..e14239192e386 100644 --- a/packages/react/src/components/Callout/CalloutContent.base.tsx +++ b/packages/react/src/components/Callout/CalloutContent.base.tsx @@ -20,6 +20,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 { useWindowEx } from '../../utilities/dom'; const COMPONENT_NAME = 'CalloutContentBase'; @@ -161,6 +162,7 @@ function usePositions( const positionAttempts = React.useRef(0); const previousTarget = React.useRef(); const async = useAsync(); + const win = useWindowEx(); const { hidden, target, finalHeight, calloutMaxHeight, onPositioned, directionalHint } = props; React.useEffect(() => { @@ -184,8 +186,8 @@ 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); + ? positionCard(currentProps, hostElement.current, dupeCalloutElement, previousPositions, win) + : positionCallout(currentProps, hostElement.current, dupeCalloutElement, previousPositions, win); // clean up duplicate calloutElement calloutElement.parentElement?.removeChild(dupeCalloutElement); @@ -233,6 +235,7 @@ function usePositions( positions, props, target, + win, ]); return positions; diff --git a/packages/react/src/components/ChoiceGroup/ChoiceGroup.base.tsx b/packages/react/src/components/ChoiceGroup/ChoiceGroup.base.tsx index 32d982585eee8..f081267d1effc 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,8 @@ const useComponentRef = ( componentRef?: IRefObject, focusProviders?: React.RefObject[], ) => { + const doc = useDocumentEx(); + React.useImperativeHandle( componentRef, () => ({ @@ -64,10 +68,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 ce307961c3c05..bd685fb6ebba3 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 0bce232488344..759f6990affde 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 1666f4076356e..d8ef8bbed0d9c 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 630e7ba599f96..8d2e4e15d6312 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 @@ -1225,7 +1231,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 5bab87d397cdd..0a4aeb836f4dc 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,7 @@ 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 cac8816f5241e..78bbf79439ba1 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 0dbaa54132297..3abe764a832b7 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 b4b6c6e9a75ef..668fa0caaae79 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 6f539afcfd1d7..aa3ee9e9d7d75 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 42d8f922df505..e1268041db446 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 9ffc4d26f85b6..003bc18fd71a8 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 = {}; @@ -342,7 +346,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._onAsyncResize); + const win = getWindowEx(this.context); + + this._events.on(win, 'resize', this._onAsyncResize); 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 79de834f3dedd..64f7e832cec9e 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,10 @@ 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 32d0ac872e081..fb94c8fafac49 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 8438e2ad06a36..329e90467153e 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 ce2d43c27d719..e110b0ec16450 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(); @@ -79,8 +83,9 @@ export class ScrollablePaneBase public componentDidMount() { const { initialScrollPosition } = this.props; + const win = getWindowEx(this.context); 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 fe6a24d16d3c6..78ab4edfc1a4e 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 5254f6ab1f19d..73d8e44fb32c4 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,24 @@ 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 ddfa101521852..bdb3071508720 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 fa89e9c5c7b98..6e35a44910e1b 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 f8b4365120e2a..d2aaaa6ba4553 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 39175c58acb40..6265255f4c76e 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,8 @@ export class TooltipHostBase extends React.Component { this._hideTooltip(); @@ -202,6 +206,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 +220,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 +508,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 +603,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 +1036,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 c9df1913d9478..7aefa561449cb 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 0000000000000..623a08b1c7143 --- /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 cd88583b5a3e1..32e8020b1beb3 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'; @@ -868,10 +868,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, @@ -887,14 +889,16 @@ function _positionCallout( callout: HTMLElement, previousPositions?: ICalloutPositionedInfo, doNotFinalizeReturnEdge?: boolean, + win?: Window, ): ICalloutPositionedInfo { + const theWin = win ?? getWindow()!; const beakWidth: number = props.isBeakVisible ? props.beakWidth || 0 : 0; const gap: number = _calculateActualBeakWidthInPixels(beakWidth) / 2 + (props.gapSpace ? props.gapSpace : 0); 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, callout, @@ -920,8 +924,10 @@ function _positionCard( hostElement: HTMLElement, callout: HTMLElement, previousPositions?: ICalloutPositionedInfo, + win?: Window, ): ICalloutPositionedInfo { - return _positionCallout(props, hostElement, callout, previousPositions, true); + const theWin = win ?? getWindow()!; + return _positionCallout(props, hostElement, callout, previousPositions, true, theWin); } // END PRIVATE FUNCTIONS @@ -945,8 +951,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( @@ -954,8 +961,9 @@ export function positionCallout( hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo, + win?: Window, ): ICalloutPositionedInfo { - return _positionCallout(props, hostElement, elementToPosition, previousPositions); + return _positionCallout(props, hostElement, elementToPosition, previousPositions, undefined, win); } export function positionCard( @@ -963,8 +971,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); } /** @@ -979,14 +988,16 @@ export function getMaxHeight( gapSpace: number = 0, bounds?: IRectangle, coverTarget?: boolean, + win?: Window, ): number { + const theWin = win ?? getWindow()!; const mouseTarget: MouseEvent = target as MouseEvent; const elementTarget: Element = target as Element; const rectOrPointTarget: Point & Rectangle = target as Point & Rectangle; let targetRect: Rectangle; const boundingRectangle = bounds ? _getRectangleFromIRect(bounds) - : new Rectangle(0, window.innerWidth - getScrollbarWidth(), 0, window.innerHeight); + : new Rectangle(0, theWin.innerWidth - getScrollbarWidth(), 0, theWin.innerHeight); // eslint-disable-next-line deprecation/deprecation const left = rectOrPointTarget.left || rectOrPointTarget.x; diff --git a/packages/react/src/utilities/selection/SelectionZone.tsx b/packages/react/src/utilities/selection/SelectionZone.tsx index 01fafadb6f6e7..82ed097d3b727 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 478a892efa7a1..523ac8ed6dd4f 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, win); // 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 3787b6c8dbb59..29f1aa484f92e 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 87927e95f6f11..3ea6e16961a46 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 d1c8179007dcd..e131bc6074b39 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 01520b98ccc7a..ee7d26eab76e3 100644 --- a/packages/utilities/src/dom/canUseDOM.ts +++ b/packages/utilities/src/dom/canUseDOM.ts @@ -3,10 +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 && // eslint-disable-line no-restricted-globals + // eslint-disable-next-line deprecation/deprecation, no-restricted-globals window.document.createElement ) ); diff --git a/packages/utilities/src/dom/getDocument.ts b/packages/utilities/src/dom/getDocument.ts index ae975f065e76f..15300f89b052c 100644 --- a/packages/utilities/src/dom/getDocument.ts +++ b/packages/utilities/src/dom/getDocument.ts @@ -8,11 +8,18 @@ import { canUseDOM } from './canUseDOM'; * @public */ export function getDocument(rootElement?: HTMLElement | null): Document | undefined { - if (!canUseDOM() || typeof document === 'undefined') { + if ( + !canUseDOM() || + // eslint-disable-next-line no-restricted-globals + typeof document === 'undefined' + ) { return undefined; } else { const el = rootElement as Element; - return el && el.ownerDocument ? el.ownerDocument : document; + return el && el.ownerDocument + ? el.ownerDocument + : // eslint-disable-next-line no-restricted-globals + document; } } diff --git a/packages/utilities/src/dom/getFirstVisibleElementFromSelector.ts b/packages/utilities/src/dom/getFirstVisibleElementFromSelector.ts index 253b0f16116db..303f9a5abcf45 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 63d24fd214859..e4d00483766d3 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 8d37fb2600984..c4e41a9c393c5 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 d66c78a03e515..fecf98ccca3b7 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 e3ab748badb23..1275b00377f40 100644 --- a/packages/utilities/src/focus.ts +++ b/packages/utilities/src/focus.ts @@ -382,12 +382,14 @@ 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 { + // eslint-disable-next-line no-restricted-globals + const theWin = win ?? getWindow()!; return ( !!element && isElementVisible(element) && !element.hidden && - window.getComputedStyle(element).visibility !== 'hidden' + theWin.getComputedStyle(element).visibility !== 'hidden' ); } @@ -456,8 +458,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 +475,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 4dfd584475e5e..09efd1718d5be 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 => { - if (!window || !window.navigator || !window.navigator.userAgent) { + // eslint-disable-next-line no-restricted-globals + const win = window; + if (!win || !win.navigator || !win.navigator.userAgent) { return false; } - return /iPad|iPhone|iPod/i.test(window.navigator.userAgent); + return /iPad|iPhone|iPod/i.test(win.navigator.userAgent); }; diff --git a/packages/utilities/src/scroll.ts b/packages/utilities/src/scroll.ts index 008b56bd76b2d..3319ce0602797 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 3187fd7cc19f0..ac51e9796ea3e 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, });