diff --git a/change/@fluentui-eslint-plugin-ea97d70f-085e-47bb-a6ac-6faba3f09cfc.json b/change/@fluentui-eslint-plugin-ea97d70f-085e-47bb-a6ac-6faba3f09cfc.json new file mode 100644 index 0000000000000..c7c95b7b663ff --- /dev/null +++ b/change/@fluentui-eslint-plugin-ea97d70f-085e-47bb-a6ac-6faba3f09cfc.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: disallow direct access to globals", + "packageName": "@fluentui/eslint-plugin", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-priority-overflow-3c4d0234-bd4e-473b-9d6f-b4a89e85eb7e.json b/change/@fluentui-priority-overflow-3c4d0234-bd4e-473b-9d6f-b4a89e85eb7e.json new file mode 100644 index 0000000000000..75b74abda6919 --- /dev/null +++ b/change/@fluentui-priority-overflow-3c4d0234-bd4e-473b-9d6f-b4a89e85eb7e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable eslint rule", + "packageName": "@fluentui/priority-overflow", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-aria-a93678b3-8ffb-47d5-9d3b-1fbf8b4ba011.json b/change/@fluentui-react-aria-a93678b3-8ffb-47d5-9d3b-1fbf8b4ba011.json new file mode 100644 index 0000000000000..97c92988994b2 --- /dev/null +++ b/change/@fluentui-react-aria-a93678b3-8ffb-47d5-9d3b-1fbf8b4ba011.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "add optional `window` argument to `scrollIntoView`.", + "packageName": "@fluentui/react-aria", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-menu-85b57efe-b104-402f-ac09-c78d05848110.json b/change/@fluentui-react-menu-85b57efe-b104-402f-ac09-c78d05848110.json new file mode 100644 index 0000000000000..de115b0b1edb9 --- /dev/null +++ b/change/@fluentui-react-menu-85b57efe-b104-402f-ac09-c78d05848110.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable eslint rule.", + "packageName": "@fluentui/react-menu", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-message-bar-e51754db-df15-41cc-9872-123edeb4990e.json b/change/@fluentui-react-message-bar-e51754db-df15-41cc-9872-123edeb4990e.json new file mode 100644 index 0000000000000..bd03f29aab3f5 --- /dev/null +++ b/change/@fluentui-react-message-bar-e51754db-df15-41cc-9872-123edeb4990e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable eslint rule", + "packageName": "@fluentui/react-message-bar", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-popover-83cdc4d1-3431-4143-8e7a-e9afae638791.json b/change/@fluentui-react-popover-83cdc4d1-3431-4143-8e7a-e9afae638791.json new file mode 100644 index 0000000000000..de27a9da023be --- /dev/null +++ b/change/@fluentui-react-popover-83cdc4d1-3431-4143-8e7a-e9afae638791.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable eslint rule.", + "packageName": "@fluentui/react-popover", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-table-ddd67de8-e1fd-4ab5-b0b5-40f161357694.json b/change/@fluentui-react-table-ddd67de8-e1fd-4ab5-b0b5-40f161357694.json new file mode 100644 index 0000000000000..a4adf62ebb1c7 --- /dev/null +++ b/change/@fluentui-react-table-ddd67de8-e1fd-4ab5-b0b5-40f161357694.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: use requestAnimationFrame from Fluent context.", + "packageName": "@fluentui/react-table", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-tabster-fcd4a805-ad5d-4f87-b659-ba102deedd56.json b/change/@fluentui-react-tabster-fcd4a805-ad5d-4f87-b659-ba102deedd56.json new file mode 100644 index 0000000000000..ee2df09007aa1 --- /dev/null +++ b/change/@fluentui-react-tabster-fcd4a805-ad5d-4f87-b659-ba102deedd56.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "update eslint comments", + "packageName": "@fluentui/react-tabster", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-teaching-popover-bcd345f2-959c-4653-b294-b4bfa456d469.json b/change/@fluentui-react-teaching-popover-bcd345f2-959c-4653-b294-b4bfa456d469.json new file mode 100644 index 0000000000000..9d2bbaedf4f00 --- /dev/null +++ b/change/@fluentui-react-teaching-popover-bcd345f2-959c-4653-b294-b4bfa456d469.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Use `MutationObserver` from global context.", + "packageName": "@fluentui/react-teaching-popover", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-toast-fbabe77b-dce1-41c3-9760-2e0786626a05.json b/change/@fluentui-react-toast-fbabe77b-dce1-41c3-9760-2e0786626a05.json new file mode 100644 index 0000000000000..29fc9752fcccb --- /dev/null +++ b/change/@fluentui-react-toast-fbabe77b-dce1-41c3-9760-2e0786626a05.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: disable eslint rule.", + "packageName": "@fluentui/react-toast", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-utilities-c872de02-afc7-4b9a-9150-71eec49dcd12.json b/change/@fluentui-react-utilities-c872de02-afc7-4b9a-9150-71eec49dcd12.json new file mode 100644 index 0000000000000..a73febd82e95e --- /dev/null +++ b/change/@fluentui-react-utilities-c872de02-afc7-4b9a-9150-71eec49dcd12.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: use globals from Fluent context.", + "packageName": "@fluentui/react-utilities", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-virtualizer-80a7f92f-f93e-41be-8cf1-c97f839de717.json b/change/@fluentui-react-virtualizer-80a7f92f-f93e-41be-8cf1-c97f839de717.json new file mode 100644 index 0000000000000..8d674474a3d57 --- /dev/null +++ b/change/@fluentui-react-virtualizer-80a7f92f-f93e-41be-8cf1-c97f839de717.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix: update internal function to optionally use global Fluent context.", + "packageName": "@fluentui/react-virtualizer", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/eslint-plugin/src/configs/restricted-globals.js b/packages/eslint-plugin/src/configs/restricted-globals.js index 5cc7632b4002c..0a931330df035 100644 --- a/packages/eslint-plugin/src/configs/restricted-globals.js +++ b/packages/eslint-plugin/src/configs/restricted-globals.js @@ -1,243 +1,29 @@ -// These will be enabled in a follow up PR. -// https://github.com/microsoft/fluentui/issues/29964 - -// 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', -// ]; +// A list of globals that are problematic when using multiple windows. +const restrictedGlobals = [ + 'customElements', + 'devicePixelRatio', + 'location', + 'navigator', + 'performance', + + 'cancelAnimationFrame', + 'cancelIdleCallback', + 'clearImmediate', + 'clearInterval', + 'clearTimeout', + 'fetch', + 'getComputedStyle', + 'matchMedia', + 'requestAnimationFrame', + 'requestIdleCallback', + 'setImmediate', + 'setInterval', + 'setTimeout', + + 'IntersectionObserver', + 'MutationObserver', + 'ResizeObserver', +]; const reactLegacy = [ 'error', @@ -261,12 +47,12 @@ const react = [ 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.`, - // }; - // }), + ...restrictedGlobals.map(key => { + return { + name: key, + message: `Get a reference to \`window\` from \`useFluent()\` and access \`${key}\` from there.`, + }; + }), ]; module.exports = { diff --git a/packages/react-components/priority-overflow/src/createResizeObserver.ts b/packages/react-components/priority-overflow/src/createResizeObserver.ts index 7e8a7597c946d..0035dd853a170 100644 --- a/packages/react-components/priority-overflow/src/createResizeObserver.ts +++ b/packages/react-components/priority-overflow/src/createResizeObserver.ts @@ -5,9 +5,9 @@ * @returns function to cleanup the resize observer */ export function observeResize(elementToObserve: HTMLElement, callback: ResizeObserverCallback) { - const GlobalResizeObsever = elementToObserve.ownerDocument.defaultView?.ResizeObserver; + const GlobalResizeObserver = elementToObserve.ownerDocument.defaultView?.ResizeObserver; - if (!GlobalResizeObsever) { + if (!GlobalResizeObserver) { if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line no-console console.error('@fluentui/priority-overflow', 'ResizeObserver does not exist on container window'); @@ -15,7 +15,7 @@ export function observeResize(elementToObserve: HTMLElement, callback: ResizeObs return () => null; } - let resizeObserver: ResizeObserver | undefined = new GlobalResizeObsever(callback); + let resizeObserver: InstanceType | undefined = new GlobalResizeObserver(callback); resizeObserver.observe(elementToObserve); return () => { diff --git a/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.test.tsx b/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.test.tsx index e9fb4577be23b..f042a714b8a5f 100644 --- a/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.test.tsx +++ b/packages/react-components/react-aria/src/AriaLiveAnnouncer/useAriaLiveAnnouncer.test.tsx @@ -17,6 +17,7 @@ describe('useAriaLiveAnnouncer', () => { body: { append: jest.fn(), }, + defaultView: global, } as unknown as Document; const ContextWrapper: React.FC = props => { return {props.children}; diff --git a/packages/react-components/react-aria/src/activedescendant/scrollIntoView.test.ts b/packages/react-components/react-aria/src/activedescendant/scrollIntoView.test.ts index 7d33854079d96..9188534869416 100644 --- a/packages/react-components/react-aria/src/activedescendant/scrollIntoView.test.ts +++ b/packages/react-components/react-aria/src/activedescendant/scrollIntoView.test.ts @@ -187,4 +187,36 @@ describe('scrollIntoView', () => { expect(mockAncestorScrollTo).toHaveBeenCalledWith(0, 122); }); + + it('should use the provided `window` argument', () => { + listboxGrandParent = { + ...listboxGrandParent, + scrollTop: 0, + }; + listboxParent = { + ...listboxParent, + offsetParent: listboxGrandParent as Element, + parentElement: listboxGrandParent as HTMLElement, + }; + listbox = { + ...listbox, + scrollHeight: 100, + offsetHeight: 100, + offsetParent: listboxGrandParent as Element, + parentElement: listboxParent as HTMLElement, + }; + const option: Partial = { + offsetHeight: 10, + offsetTop: 160, + offsetParent: listboxGrandParent as HTMLElement, + parentElement: listbox as HTMLElement, + contains: jest.fn().mockReturnValue(false), + }; + + const getComputedStyleSpy = jest.spyOn(window, 'getComputedStyle'); + + scrollIntoView(option as HTMLElement); + + expect(getComputedStyleSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/react-components/react-aria/src/activedescendant/scrollIntoView.ts b/packages/react-components/react-aria/src/activedescendant/scrollIntoView.ts index c415d4b2c3ba0..3ca94fe44b16e 100644 --- a/packages/react-components/react-aria/src/activedescendant/scrollIntoView.ts +++ b/packages/react-components/react-aria/src/activedescendant/scrollIntoView.ts @@ -1,8 +1,11 @@ -export const scrollIntoView = (target: HTMLElement | null | undefined) => { +export const scrollIntoView = (target: HTMLElement | null | undefined, winArg?: Window | null) => { if (!target) { return; } + // eslint-disable-next-line no-restricted-globals + const win = winArg ?? window; + const scrollParent = findScrollableParent(target.parentElement as HTMLElement); if (!scrollParent) { return; @@ -11,7 +14,7 @@ export const scrollIntoView = (target: HTMLElement | null | undefined) => { const { offsetHeight } = target; const offsetTop = getTotalOffsetTop(target, scrollParent); - const { scrollMarginTop, scrollMarginBottom } = getScrollMargins(target); + const { scrollMarginTop, scrollMarginBottom } = getScrollMargins(target, win); const { offsetHeight: parentOffsetHeight, scrollTop } = scrollParent; @@ -52,8 +55,8 @@ const getTotalOffsetTop = (element: HTMLElement, scrollParent: HTMLElement): num return element.offsetTop + getTotalOffsetTop(element.offsetParent as HTMLElement, scrollParent); }; -const getScrollMargins = (element: HTMLElement) => { - const computedStyles = getComputedStyle(element); +const getScrollMargins = (element: HTMLElement, win: Window) => { + const computedStyles = win.getComputedStyle(element); const scrollMarginTop = getIntValueOfComputedStyle(computedStyles.scrollMarginTop) ?? getIntValueOfComputedStyle(computedStyles.scrollMarginBlockStart); diff --git a/packages/react-components/react-aria/src/activedescendant/useActiveDescendant.ts b/packages/react-components/react-aria/src/activedescendant/useActiveDescendant.ts index 0512298e8e968..33413f848f42a 100644 --- a/packages/react-components/react-aria/src/activedescendant/useActiveDescendant.ts +++ b/packages/react-components/react-aria/src/activedescendant/useActiveDescendant.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { useEventCallback, useMergedRefs } from '@fluentui/react-utilities'; import { useOnKeyboardNavigationChange } from '@fluentui/react-tabster'; import { useOptionWalker } from './useOptionWalker'; @@ -32,6 +33,8 @@ export function useActiveDescendant(null); const activeParentRef = React.useRef(null); const attributeVisibilityRef = React.useRef(true); + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; const removeAttribute = React.useCallback(() => { activeParentRef.current?.removeAttribute('aria-activedescendant'); @@ -87,7 +90,7 @@ export function useActiveDescendant): CarouselState { const { onValueChange } = props; + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; const { ref: carouselRef, walker: carouselWalker } = useCarouselWalker_unstable(); const [store] = React.useState(() => createCarouselStore()); @@ -60,6 +64,10 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref { + if (!win) { + return; + } + const config: MutationObserverInit = { attributes: true, attributeFilter: [CAROUSEL_ITEM], @@ -94,7 +102,7 @@ export function useCarousel_unstable(props: CarouselProps, ref: React.Ref { observer.disconnect(); }; - }, [carouselWalker, store]); + }, [carouselWalker, store, win]); const selectPageByDirection: CarouselContextValue['selectPageByDirection'] = useEventCallback((event, direction) => { const active = carouselWalker.active(); diff --git a/packages/react-components/react-icons-compat/src/icon.ts b/packages/react-components/react-icons-compat/src/icon.ts index 998e4429787b3..52e5bbebf577f 100644 --- a/packages/react-components/react-icons-compat/src/icon.ts +++ b/packages/react-components/react-icons-compat/src/icon.ts @@ -197,6 +197,8 @@ export function setIconOptions(options: Partial): void { } let _missingIcons: string[] = []; +// TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 +// eslint-disable-next-line no-restricted-globals let _missingIconsTimer: ReturnType | undefined = undefined; function _warnDuplicateIcon(iconName: string): void { @@ -207,6 +209,7 @@ function _warnDuplicateIcon(iconName: string): void { if (!options.disableWarnings) { _missingIcons.push(iconName); if (_missingIconsTimer === undefined) { + // eslint-disable-next-line no-restricted-globals _missingIconsTimer = setTimeout(() => { // eslint-disable-next-line no-console console.warn( diff --git a/packages/react-components/react-menu/src/components/MenuPopover/useMenuPopover.ts b/packages/react-components/react-menu/src/components/MenuPopover/useMenuPopover.ts index c90b12dfcd1d7..aadc45946067e 100644 --- a/packages/react-components/react-menu/src/components/MenuPopover/useMenuPopover.ts +++ b/packages/react-components/react-menu/src/components/MenuPopover/useMenuPopover.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { ArrowLeft, Tab, ArrowRight, Escape } from '@fluentui/keyboard-keys'; -import { getIntrinsicElementProps, useEventCallback, useMergedRefs, slot } from '@fluentui/react-utilities'; +import { getIntrinsicElementProps, useEventCallback, useMergedRefs, slot, useTimeout } from '@fluentui/react-utilities'; import { MenuPopoverProps, MenuPopoverState } from './MenuPopover.types'; import { useMenuContext_unstable } from '../../contexts/menuContext'; import { dispatchMenuEnterEvent } from '../../utils/index'; @@ -25,8 +25,8 @@ export const useMenuPopover_unstable = (props: MenuPopoverProps, ref: React.Ref< const triggerRef = useMenuContext_unstable(context => context.triggerRef); const isSubmenu = useIsSubmenu(); const canDispatchCustomEventRef = React.useRef(true); - const throttleDispatchTimerRef = React.useRef(0); const restoreFocusSourceAttributes = useRestoreFocusSource(); + const [setThrottleTimeout, clearThrottleTimeout] = useTimeout(); const { dir } = useFluent(); const CloseArrowKey = dir === 'ltr' ? ArrowLeft : ArrowRight; @@ -43,19 +43,17 @@ export const useMenuPopover_unstable = (props: MenuPopoverProps, ref: React.Ref< if (canDispatchCustomEventRef.current) { canDispatchCustomEventRef.current = false; dispatchMenuEnterEvent(popoverRef.current as HTMLElement, e); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore #16889 Node setTimeout type leaking - throttleDispatchTimerRef.current = setTimeout(() => (canDispatchCustomEventRef.current = true), 250); + setThrottleTimeout(() => (canDispatchCustomEventRef.current = true), 250); } }); } }, - [popoverRef, throttleDispatchTimerRef], + [popoverRef, setThrottleTimeout], ); React.useEffect(() => { - () => clearTimeout(throttleDispatchTimerRef.current); - }, []); + () => clearThrottleTimeout(); + }, [clearThrottleTimeout]); const inline = useMenuContext_unstable(context => context.inline) ?? false; const mountNode = useMenuContext_unstable(context => context.mountNode); diff --git a/packages/react-components/react-message-bar/src/components/MessageBar/useMessageBarReflow.ts b/packages/react-components/react-message-bar/src/components/MessageBar/useMessageBarReflow.ts index b113ebfcbfeff..53034e7a4c48b 100644 --- a/packages/react-components/react-message-bar/src/components/MessageBar/useMessageBarReflow.ts +++ b/packages/react-components/react-message-bar/src/components/MessageBar/useMessageBarReflow.ts @@ -6,6 +6,8 @@ export function useMessageBarReflow(enabled: boolean = false) { const { targetDocument } = useFluent(); const forceUpdate = React.useReducer(() => ({}), {})[1]; const reflowingRef = React.useRef(false); + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals const resizeObserverRef = React.useRef(null); const prevInlineSizeRef = React.useRef(-1); diff --git a/packages/react-components/react-table/src/hooks/useMeasureElement.ts b/packages/react-components/react-table/src/hooks/useMeasureElement.ts index 2ab924c47ae7c..c86a93261db11 100644 --- a/packages/react-components/react-table/src/hooks/useMeasureElement.ts +++ b/packages/react-components/react-table/src/hooks/useMeasureElement.ts @@ -11,6 +11,8 @@ export function useMeasureElement() const [width, setWidth] = React.useState(0); const container = React.useRef(undefined); + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals const resizeObserverRef = React.useRef(null); const { targetDocument } = useFluent(); diff --git a/packages/react-components/react-tabster/src/tabster-types-6.0.1-do-not-use.ts b/packages/react-components/react-tabster/src/tabster-types-6.0.1-do-not-use.ts index 5c58d65d790cb..e8487dcc5b406 100644 --- a/packages/react-components/react-tabster/src/tabster-types-6.0.1-do-not-use.ts +++ b/packages/react-components/react-tabster/src/tabster-types-6.0.1-do-not-use.ts @@ -123,6 +123,8 @@ export interface TabsterCoreProps { } export interface DOMAPI { + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals createMutationObserver: (callback: MutationCallback) => MutationObserver; createTreeWalker(doc: Document, root: Node, whatToShow?: number, filter?: NodeFilter | null): TreeWalker; getParentNode(node: Node | null | undefined): ParentNode | null; @@ -744,7 +746,6 @@ export const MoverKeys: MoverKeys = { End: 8, }; -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MoverAPI extends MoverAPIInternal, Disposable { /** @internal (will likely be exposed once the API is fully stable) */ moveFocus(fromElement: HTMLElement, key: MoverKey): HTMLElement | null; @@ -794,7 +795,6 @@ export interface GroupperAPIInternal { handleKeyPress(element: HTMLElement, event: KeyboardEvent, fromModalizer?: boolean): void; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface GroupperAPI extends GroupperAPIInternal, Disposable { /** @internal (will likely be exposed once the API is fully stable) */ moveFocus(element: HTMLElement, action: GroupperMoveFocusAction): HTMLElement | null; diff --git a/packages/react-components/react-teaching-popover/src/components/TeachingPopoverCarousel/Carousel/Carousel.tsx b/packages/react-components/react-teaching-popover/src/components/TeachingPopoverCarousel/Carousel/Carousel.tsx index a2e6f321ca1d8..80f47575f915a 100644 --- a/packages/react-components/react-teaching-popover/src/components/TeachingPopoverCarousel/Carousel/Carousel.tsx +++ b/packages/react-components/react-teaching-popover/src/components/TeachingPopoverCarousel/Carousel/Carousel.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { isHTMLElement, useMergedRefs, @@ -5,7 +6,7 @@ import { type EventHandler, useEventCallback, } from '@fluentui/react-utilities'; -import * as React from 'react'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { CAROUSEL_ITEM } from './constants'; import { useCarouselWalker_unstable } from './useCarouselWalker'; @@ -26,6 +27,8 @@ export type UseCarouselOptions = { export function useCarousel_unstable(options: UseCarouselOptions) { const { onValueChange, onFinish } = options; + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; const { ref: carouselRef, walker: carouselWalker } = useCarouselWalker_unstable(); const [store] = React.useState(() => createCarouselStore()); @@ -61,6 +64,10 @@ export function useCarousel_unstable(options: UseCarouselOptions) { }, [store]); React.useEffect(() => { + if (!win) { + return; + } + const config: MutationObserverInit = { attributes: true, attributeFilter: [CAROUSEL_ITEM], @@ -95,7 +102,7 @@ export function useCarousel_unstable(options: UseCarouselOptions) { }; // Create an observer instance linked to the callback function - const observer = new MutationObserver(callback); + const observer = new win.MutationObserver(callback); // Start observing the target node for configured mutations observer.observe(rootRef.current!, config); @@ -104,7 +111,7 @@ export function useCarousel_unstable(options: UseCarouselOptions) { return () => { observer.disconnect(); }; - }, [carouselWalker, store]); + }, [carouselWalker, store, win]); const selectPageByDirection: CarouselContextValue['selectPageByDirection'] = useEventCallback((event, direction) => { const active = carouselWalker.active(); diff --git a/packages/react-components/react-tree/stories/Tree/utils/mockFetch.ts b/packages/react-components/react-tree/stories/Tree/utils/mockFetch.ts index 7c65b4794822a..32e7c59920027 100644 --- a/packages/react-components/react-tree/stories/Tree/utils/mockFetch.ts +++ b/packages/react-components/react-tree/stories/Tree/utils/mockFetch.ts @@ -4,6 +4,7 @@ export interface Response { export const mockFetch = (type: string) => { return new Promise(resolve => { + // eslint-disable-next-line no-restricted-globals setTimeout(() => { const mockData: Response = { results: Array.from({ length: 10 }, (_, index) => ({ diff --git a/packages/react-components/react-utilities/src/hooks/useAnimationFrame.ts b/packages/react-components/react-utilities/src/hooks/useAnimationFrame.ts index a95e862aad739..5c7c318374ad5 100644 --- a/packages/react-components/react-utilities/src/hooks/useAnimationFrame.ts +++ b/packages/react-components/react-utilities/src/hooks/useAnimationFrame.ts @@ -1,5 +1,5 @@ -import { canUseDOM } from '../ssr/canUseDOM'; import { useBrowserTimer } from './useBrowserTimer'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; const setAnimationFrameNoop = (callback: FrameRequestCallback) => { callback(0); @@ -16,11 +16,11 @@ const cancelAnimationFrameNoop = (handle: number) => handle; * @returns A pair of [requestAnimationFrame, cancelAnimationFrame] that are stable between renders. */ export function useAnimationFrame() { - const isDOM = canUseDOM(); + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; - // TODO: figure it out a way to not call global.requestAnimationFrame and instead infer window from some context - const setAnimationFrame = isDOM ? requestAnimationFrame : setAnimationFrameNoop; - const clearAnimationFrame = isDOM ? cancelAnimationFrame : cancelAnimationFrameNoop; + const setAnimationFrame = win ? win.requestAnimationFrame : setAnimationFrameNoop; + const clearAnimationFrame = win ? win.cancelAnimationFrame : cancelAnimationFrameNoop; return useBrowserTimer(setAnimationFrame, clearAnimationFrame); } diff --git a/packages/react-components/react-utilities/src/hooks/useTimeout.ts b/packages/react-components/react-utilities/src/hooks/useTimeout.ts index 36c0813ef2107..14ae3b8ef09dd 100644 --- a/packages/react-components/react-utilities/src/hooks/useTimeout.ts +++ b/packages/react-components/react-utilities/src/hooks/useTimeout.ts @@ -1,4 +1,8 @@ import { useBrowserTimer } from './useBrowserTimer'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; + +const setTimeoutNoop = (_callback: Function) => -1; +const clearTimeoutNoop = (_handle: number) => undefined; /** * @internal @@ -9,6 +13,11 @@ import { useBrowserTimer } from './useBrowserTimer'; * @returns A pair of [setTimeout, clearTimeout] that are stable between renders. */ export function useTimeout() { - // TODO: figure it out a way to not call global.setTimeout and instead infer window from some context - return useBrowserTimer(setTimeout, clearTimeout); + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; + + const setTimerFn = win ? win.setTimeout : setTimeoutNoop; + const clearTimerFn = win ? win.clearTimeout : clearTimeoutNoop; + + return useBrowserTimer(setTimerFn, clearTimerFn); } diff --git a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts index ed1ed221babb2..73fabf08ec18d 100644 --- a/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts +++ b/packages/react-components/react-virtualizer/src/components/Virtualizer/useVirtualizer.ts @@ -126,6 +126,8 @@ export function useVirtualizer_unstable(props: VirtualizerProps): VirtualizerSta // Observe intersections of virtualized components const { setObserverList } = useIntersectionObserver( + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { /* Sanity check - do we even need virtualization? */ if (virtualizerLength > numItems) { diff --git a/packages/react-components/react-virtualizer/src/hooks/hooks.types.ts b/packages/react-components/react-virtualizer/src/hooks/hooks.types.ts index 436c40bdcf5c7..7d8d27dcb730c 100644 --- a/packages/react-components/react-virtualizer/src/hooks/hooks.types.ts +++ b/packages/react-components/react-virtualizer/src/hooks/hooks.types.ts @@ -46,5 +46,11 @@ export type VirtualizerDynamicPaginationProps = { * Backwards compatible with ResizeObserverCallback if preferred */ export interface ResizeCallbackWithRef { - (entries: ResizeObserverEntry[], observer: ResizeObserver, scrollRef?: MutableRefObject): void; + ( + entries: ResizeObserverEntry[], + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals + observer: ResizeObserver, + scrollRef?: MutableRefObject, + ): void; } diff --git a/packages/react-components/react-virtualizer/src/hooks/useDynamicVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useDynamicVirtualizerMeasure.ts index 91302fe36d734..9eda6d577c275 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useDynamicVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useDynamicVirtualizerMeasure.ts @@ -78,6 +78,8 @@ export const useDynamicVirtualizerMeasure = ( const resizeCallback = React.useCallback( ( _entries: ResizeObserverEntry[], + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals _observer: ResizeObserver, scrollRef?: React.MutableRefObject, ) => { diff --git a/packages/react-components/react-virtualizer/src/hooks/useIntersectionObserver.ts b/packages/react-components/react-virtualizer/src/hooks/useIntersectionObserver.ts index b0450c9f69722..8067e83e6afba 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useIntersectionObserver.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useIntersectionObserver.ts @@ -12,10 +12,14 @@ import { useMutationObserver } from './useMutationObserver'; * @param target target element that will have its current reading direction determined * @returns the corrected rootMargin (if it was necessary to correct) */ -export const getRTLRootMargin = (ltrRootMargin: string, target?: Element | Document | null | undefined): string => { - if (target) { +export const getRTLRootMargin = ( + ltrRootMargin: string, + target?: Element | Document | null | undefined, + win?: Window | null, +): string => { + if (target && win) { // get the computed dir for the target element - const newDir = getComputedStyle(target as Element).direction; + const newDir = win.getComputedStyle(target as Element).direction; // If we're in rtl reading direction, we might need to flip the margins on the left/right sides if (newDir === 'rtl') { @@ -52,17 +56,22 @@ export const useIntersectionObserver = ( ): { setObserverList: Dispatch>; setObserverInit: (newInit: IntersectionObserverInit | undefined) => void; + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals observer: MutableRefObject; } => { + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals const observer = useRef(); const [observerList, setObserverList] = useState(); const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; // set the initial init with corrected margins based on the observed root's calculated reading direction. const [observerInit, setObserverInit] = useState( options && { ...options, - rootMargin: getRTLRootMargin(options.rootMargin ?? '0px', options.root as Element), + rootMargin: getRTLRootMargin(options.rootMargin ?? '0px', options.root as Element, win), }, ); @@ -82,12 +91,12 @@ export const useIntersectionObserver = ( ) { setObserverInit({ ...observerInit, - rootMargin: getRTLRootMargin(ltrRootMargin.current, observerInit?.root), + rootMargin: getRTLRootMargin(ltrRootMargin.current, observerInit?.root, win), }); } } }, - [ltrRootMargin, observerInit, options?.root], + [ltrRootMargin, observerInit, options?.root, win], ); // Mutation observer for dir attribute changes in the document @@ -100,14 +109,13 @@ export const useIntersectionObserver = ( // Observer elements in passed in list and clean up previous list // This effect is only triggered when observerList is updated useIsomorphicLayoutEffect(() => { - const win = targetDocument?.defaultView; if (!win) { return; } observer.current = new win.IntersectionObserver(callback, { ...observerInit, - rootMargin: getRTLRootMargin(ltrRootMargin.current, observerInit?.root), + rootMargin: getRTLRootMargin(ltrRootMargin.current, observerInit?.root, win), }); // If we have an instance of IO and a list with elements, observer the elements @@ -123,7 +131,7 @@ export const useIntersectionObserver = ( observer.current.disconnect(); } }; - }, [observerList, observerInit, callback, targetDocument?.defaultView]); + }, [observerList, observerInit, callback, win]); // Do not use internally, we need to track external settings only here const setObserverInitExternal = useCallback( @@ -134,10 +142,10 @@ export const useIntersectionObserver = ( // Call the internal setter to update the value and ensure if our calculated direction is rtl, we flip the margin setObserverInit({ ...newInit, - rootMargin: getRTLRootMargin(ltrRootMargin.current, newInit?.root as Element), + rootMargin: getRTLRootMargin(ltrRootMargin.current, newInit?.root as Element, win), }); }, - [ltrRootMargin, setObserverInit], + [ltrRootMargin, setObserverInit, win], ); return { setObserverList, setObserverInit: setObserverInitExternal, observer }; diff --git a/packages/react-components/react-virtualizer/src/hooks/useMutationObserver.ts b/packages/react-components/react-virtualizer/src/hooks/useMutationObserver.ts index c46b43e82dd97..f7f4133adea68 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useMutationObserver.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useMutationObserver.ts @@ -1,5 +1,6 @@ import type { MutableRefObject } from 'react'; import * as React from 'react'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; const { useRef, useEffect } = React; @@ -8,14 +9,21 @@ export const useMutationObserver = ( callback: MutationCallback, options?: MutationObserverInit, ): { - observer: MutableRefObject; + observer: MutableRefObject; // eslint-disable-line no-restricted-globals } => { + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals const observer = useRef(); + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; useEffect(() => { + if (!win) { + return; + } // Create an observer instance linked to the callback function - observer.current = new MutationObserver(callback); - }, [callback]); + observer.current = new win.MutationObserver(callback); + }, [callback, win]); useEffect(() => { if (target) { diff --git a/packages/react-components/react-virtualizer/src/hooks/useResizeObserverRef.ts b/packages/react-components/react-virtualizer/src/hooks/useResizeObserverRef.ts index 3de686b456aed..75d4d4be7c767 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useResizeObserverRef.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useResizeObserverRef.ts @@ -11,6 +11,8 @@ export const useResizeObserverRef_unstable = (resizeCallback: ResizeCallbackWith const { targetDocument } = useFluent(); const container = React.useRef(null); // the handler for resize observer + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals const handleResize = debounce((entries: ResizeObserverEntry[], observer: ResizeObserver) => { resizeCallback(entries, observer, container); }); diff --git a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts index 091a1d074a555..aeeec5bb5a6db 100644 --- a/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts +++ b/packages/react-components/react-virtualizer/src/hooks/useVirtualizerMeasure.ts @@ -26,6 +26,8 @@ export const useStaticVirtualizerMeasure = ( const resizeCallback = React.useCallback( ( _entries: ResizeObserverEntry[], + // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 + // eslint-disable-next-line no-restricted-globals _observer: ResizeObserver, scrollRef?: React.MutableRefObject, ) => {