Disallow `window` and `document` access for `@fluentui/react` and related packages. (#30063)

This commit is contained in:
Sean Monahan 2024-01-09 16:51:42 -08:00 коммит произвёл GitHub
Родитель c43a582b1e
Коммит 5858fe0525
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
81 изменённых файлов: 590 добавлений и 189 удалений

Просмотреть файл

@ -2,6 +2,7 @@
"extends": ["plugin:@fluentui/eslint-plugin/react"],
"root": true,
"rules": {
"no-console": "off"
"no-console": "off",
"no-restricted-globals": "off"
}
}

Просмотреть файл

@ -13,7 +13,8 @@
"curly": "off",
"no-var": "off",
"vars-on-top": "off",
"prefer-arrow-callback": "off"
"prefer-arrow-callback": "off",
"no-restricted-globals": "off"
}
}
]

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -6,6 +6,7 @@
"deprecation/deprecation": "off",
"import/no-webpack-loader-syntax": "off", // ok in this project
"prefer-const": "off",
"react/jsx-no-bind": "off"
"react/jsx-no-bind": "off",
"no-restricted-globals": "off"
}
}

Просмотреть файл

@ -4,6 +4,7 @@
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"deprecation/deprecation": "off",
"prefer-const": "off"
"prefer-const": "off",
"no-restricted-globals": "off"
}
}

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: disallow document and window access",
"packageName": "@fluentui/dom-utilities",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: add restricted globals for @fluentui/react and related packages",
"packageName": "@fluentui/eslint-plugin",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: disallow document and window access",
"packageName": "@fluentui/merge-styles",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: disallow document and window access",
"packageName": "@fluentui/react",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: disallow document and window access",
"packageName": "@fluentui/react-file-type-icons",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: disallow document and window access",
"packageName": "@fluentui/react-focus",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: clean up exports",
"packageName": "@fluentui/react-window-provider",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: disallow document and window access",
"packageName": "@fluentui/utilities",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -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)
```

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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);

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -1,6 +1,7 @@
// @ts-check
const configHelpers = require('../utils/configHelpers');
const path = require('path');
const { reactLegacy: restrictedGlobals } = require('./restricted-globals');
/** @type {import("eslint").Linter.Config} */
module.exports = {
@ -9,7 +10,16 @@ module.exports = {
rules: {
'jsdoc/check-tag-names': 'off',
'@griffel/no-shorthands': 'off',
'no-restricted-globals': 'off',
'no-restricted-globals': restrictedGlobals,
},
overrides: [],
overrides: [
{
// Test overrides
files: [...configHelpers.testFiles, '**/*.stories.tsx'],
rules: {
'no-restricted-globals': 'off',
'react/jsx-no-bind': 'off',
},
},
],
};

Просмотреть файл

@ -239,6 +239,18 @@
// 'onwheel',
// ];
const reactLegacy = [
'error',
{
name: 'window',
message: 'Get a reference to `window` via context.',
},
{
name: 'document',
message: 'Get a reference to `document` via context.',
},
];
const react = [
'error',
{
@ -258,5 +270,6 @@ const react = [
];
module.exports = {
reactLegacy,
react,
};

Просмотреть файл

@ -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;

Просмотреть файл

@ -1,3 +1,6 @@
/* eslint no-restricted-globals: 0 */
// globals in stylesheets will be addressed as part of shadow DOM work.
// See: https://github.com/microsoft/fluentui/issues/28058
import { IStyle } from './IStyle';
export const InjectionMode = {

Просмотреть файл

@ -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();

Просмотреть файл

@ -1,4 +1,7 @@
{
"extends": ["plugin:@fluentui/eslint-plugin/react--legacy"],
"root": true
"root": true,
"rules": {
"no-restricted-globals": "off"
}
}

Просмотреть файл

@ -2,6 +2,7 @@
"extends": ["plugin:@fluentui/eslint-plugin/react--legacy"],
"root": true,
"rules": {
"deprecation/deprecation": "off"
"deprecation/deprecation": "off",
"no-restricted-globals": "off"
}
}

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -160,8 +160,14 @@ export function getFileTypeIconNameFromExtensionOrType(
return iconBaseName || GENERIC_FILE;
}
export function getFileTypeIconSuffix(size: FileTypeIconSize, imageFileType: ImageFileType = 'svg'): string {
let devicePixelRatio: number = window.devicePixelRatio;
export function getFileTypeIconSuffix(
size: FileTypeIconSize,
imageFileType: ImageFileType = 'svg',
win?: Window,
): string {
// eslint-disable-next-line no-restricted-globals
win ??= window;
let devicePixelRatio: number = win.devicePixelRatio;
let devicePixelRatioSuffix = ''; // Default is 1x
// SVGs scale well, so you can generally use the default image.

Просмотреть файл

@ -59,12 +59,14 @@ function raiseClickFromKeyboardEvent(target: Element, ev?: React.KeyboardEvent<H
cancelable: ev?.cancelable,
});
} else {
// eslint-disable-next-line no-restricted-globals
event = document.createEvent('MouseEvents');
// eslint-disable-next-line deprecation/deprecation
event.initMouseEvent(
'click',
ev ? ev.bubbles : false,
ev ? ev.cancelable : false,
// eslint-disable-next-line no-restricted-globals
window, // not using getWindow() since this can only be run client side
0, // detail
0, // screen x

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -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';

Просмотреть файл

@ -691,6 +691,8 @@ export class BasePicker<T, P extends IBasePickerProps<T>> extends React_2.Compon
// (undocumented)
componentWillUnmount(): void;
// (undocumented)
static contextType: React_2.Context<WindowProviderProps>;
// (undocumented)
protected currentPromise: PromiseLike<any> | undefined;
// (undocumented)
dismissSuggestions: (ev?: any) => void;
@ -797,6 +799,8 @@ export class BaseSelectedItemsList<T, P extends IBaseSelectedItemsListProps<T>>
// (undocumented)
componentDidUpdate(oldProps: P, oldState: IBaseSelectedItemsListState<IObjectWithKey>): void;
// (undocumented)
static contextType: React_2.Context<WindowProviderProps>;
// (undocumented)
protected copyItems(items: T[]): void;
// (undocumented)
static getDerivedStateFromProps(newProps: IBaseSelectedItemsListProps<any>): {
@ -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<IDetailsListProps, IDetai
// (undocumented)
componentWillUnmount(): void;
// (undocumented)
static contextType: React_2.Context<WindowProviderProps>;
// (undocumented)
static defaultProps: {
layoutMode: DetailsListLayoutMode;
selectionMode: SelectionMode_2;
@ -1746,7 +1752,7 @@ export function getColorFromHSV(hsv: IHSV, a?: number): IColor;
export function getColorFromRGBA(rgba: IRGB): IColor;
// @public
export function getColorFromString(inputColor: string): IColor | undefined;
export function getColorFromString(inputColor: string, doc?: Document): IColor | undefined;
// @public (undocumented)
export const getCommandBarStyles: (props: ICommandBarStyleProps) => ICommandBarStyles;
@ -1856,7 +1862,7 @@ export function getLayerHostSelector(): string | undefined;
export const getLayerStyles: (props: ILayerStyleProps) => ILayerStyles;
// @public
export function getMaxHeight(target: Element | MouseEvent | Point | Rectangle, targetEdge: DirectionalHint, gapSpace?: number, bounds?: IRectangle, coverTarget?: boolean): number;
export function getMaxHeight(target: Element | MouseEvent | Point | Rectangle, targetEdge: DirectionalHint, gapSpace?: number, bounds?: IRectangle, coverTarget?: boolean, win?: Window): number;
// @public
export const getMeasurementCache: () => {
@ -8126,6 +8132,8 @@ export interface IScrollablePaneContext {
notifySubscribers: (sort?: boolean) => void;
syncScrollSticky: (sticky: Sticky) => void;
};
// (undocumented)
window: Window | undefined;
}
// @public (undocumented)
@ -9772,6 +9780,8 @@ export class KeytipLayerBase extends React_2.Component<IKeytipLayerProps, IKeyti
// (undocumented)
componentWillUnmount(): void;
// (undocumented)
static contextType: React_2.Context<WindowProviderProps>;
// (undocumented)
static defaultProps: IKeytipLayerProps;
// (undocumented)
getCurrentSequence(): string;
@ -9880,6 +9890,8 @@ export class List<T = any> extends React_2.Component<IListProps<T>, IListState<T
// (undocumented)
componentWillUnmount(): void;
// (undocumented)
static contextType: React_2.Context<WindowProviderProps>;
// (undocumented)
static defaultProps: {
startIndex: number;
onRenderCell: (item: any, index: number, containsFocus: boolean) => JSX.Element;
@ -10043,6 +10055,8 @@ export const Nav: React_2.FunctionComponent<INavProps>;
export class NavBase extends React_2.Component<INavProps, INavState> implements INav {
constructor(props: INavProps);
// (undocumented)
static contextType: React_2.Context<WindowProviderProps>;
// (undocumented)
static defaultProps: INavProps;
focus(forceIntoFirstElement?: boolean): boolean;
// (undocumented)
@ -10135,6 +10149,8 @@ export class PanelBase extends React_2.Component<IPanelProps, IPanelState> imple
// (undocumented)
componentWillUnmount(): void;
// (undocumented)
static contextType: React_2.Context<WindowProviderProps>;
// (undocumented)
static defaultProps: IPanelProps;
// (undocumented)
dismiss: (ev?: React_2.SyntheticEvent<HTMLElement> | KeyboardEvent) => void;
@ -10398,13 +10414,13 @@ export enum Position {
}
// @public (undocumented)
export function positionCallout(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo, shouldScroll?: boolean, minimumScrollResizeHeight?: number): ICalloutPositionedInfo;
export function positionCallout(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo, shouldScroll?: boolean, minimumScrollResizeHeight?: number, win?: Window): ICalloutPositionedInfo;
// @public (undocumented)
export function positionCard(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo): ICalloutPositionedInfo;
export function positionCard(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: ICalloutPositionedInfo, win?: Window): ICalloutPositionedInfo;
// @public
export function positionElement(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: IPositionedData): IPositionedData;
export function positionElement(props: IPositionProps, hostElement: HTMLElement, elementToPosition: HTMLElement, previousPositions?: IPositionedData, win?: Window): IPositionedData;
// @public (undocumented)
export const PositioningContainer: React_2.FunctionComponent<IPositioningContainerProps>;
@ -10585,6 +10601,8 @@ export class ScrollablePaneBase extends React_2.Component<IScrollablePaneProps,
// (undocumented)
get contentContainer(): HTMLDivElement | null;
// (undocumented)
static contextType: React_2.Context<WindowProviderProps>;
// (undocumented)
forceLayoutUpdate(): void;
// (undocumented)
getScrollPosition: () => number;
@ -11351,6 +11369,8 @@ export class TooltipHostBase extends React_2.Component<ITooltipHostProps, IToolt
// (undocumented)
componentWillUnmount(): void;
// (undocumented)
static contextType: React_2.Context<WindowProviderProps>;
// (undocumented)
static defaultProps: {
delay: TooltipDelay;
};

Просмотреть файл

@ -21,7 +21,7 @@ import type { ICalloutProps, ICalloutContentStyleProps, ICalloutContentStyles }
import type { Point, IRectangle } from '../../Utilities';
import type { ICalloutPositionedInfo, IPositionProps, IPosition } from '../../Positioning';
import type { Target } from '@fluentui/react-hooks';
import { useWindow } from '@fluentui/react-window-provider';
import { useWindowEx } from '../../utilities/dom';
const COMPONENT_NAME = 'CalloutContentBase';
@ -208,7 +208,7 @@ function usePositions(
preferScrollResizePositioning,
} = props;
const win = useWindow();
const win = useWindowEx();
const localRef = React.useRef<HTMLDivElement | null>();
let popupStyles: CSSStyleDeclaration | undefined;
if (localRef.current !== popupRef.current) {
@ -243,8 +243,16 @@ function usePositions(
// If there is a finalHeight given then we assume that the user knows and will handle
// additional positioning adjustments so we should call positionCard
const newPositions: ICalloutPositionedInfo = finalHeight
? positionCard(currentProps, hostElement.current, dupeCalloutElement, previousPositions)
: positionCallout(currentProps, hostElement.current, dupeCalloutElement, previousPositions, shouldScroll);
? positionCard(currentProps, hostElement.current, dupeCalloutElement, previousPositions, win)
: positionCallout(
currentProps,
hostElement.current,
dupeCalloutElement,
previousPositions,
shouldScroll,
undefined,
win,
);
// clean up duplicate calloutElement
calloutElement.parentElement?.removeChild(dupeCalloutElement);
@ -295,6 +303,7 @@ function usePositions(
hideOverflow,
preferScrollResizePositioning,
popupOverflowY,
win,
]);
return positions;

Просмотреть файл

@ -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<IChoiceGroupStyleProps, IChoiceGroupStyles>();
@ -36,9 +37,10 @@ const focusSelectedOption = (
keyChecked: IChoiceGroupProps['selectedKey'],
id: string,
focusProviders?: React.RefObject<HTMLElement>[],
doc?: Document,
) => {
const optionToFocus = findOption(options, keyChecked) || options.filter(option => !option.disabled)[0];
const elementToFocus = optionToFocus && document.getElementById(getOptionId(optionToFocus, id));
const elementToFocus = optionToFocus && doc?.getElementById(getOptionId(optionToFocus, id));
if (elementToFocus) {
elementToFocus.focus();
@ -57,6 +59,7 @@ const useComponentRef = (
componentRef?: IRefObject<IChoiceGroup>,
focusProviders?: React.RefObject<HTMLElement>[],
) => {
const doc = useDocumentEx();
React.useImperativeHandle(
componentRef,
() => ({
@ -64,10 +67,10 @@ const useComponentRef = (
return findOption(options, keyChecked);
},
focus() {
focusSelectedOption(options, keyChecked, id, focusProviders);
focusSelectedOption(options, keyChecked, id, focusProviders, doc);
},
}),
[options, keyChecked, id, focusProviders],
[options, keyChecked, id, focusProviders, doc],
);
};

Просмотреть файл

@ -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<ICoachmarkStyleProps, ICoachmarkStyles>();
@ -253,6 +254,8 @@ function useProximityHandlers(
/** The target element the mouse would be in proximity to */
const targetElementRect = React.useRef<DOMRect>();
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<ICoachmarkProps> = React.for
>((propsWithoutDefaults, forwardedRef) => {
const props = getPropsWithDefaults(DEFAULT_PROPS, propsWithoutDefaults);
const win = useWindowEx();
const entityInnerHostElementRef = React.useRef<HTMLDivElement | null>(null);
const translateAnimationContainer = React.useRef<HTMLDivElement | null>(null);
@ -420,7 +424,7 @@ export const CoachmarkBase: React.FunctionComponent<ICoachmarkProps> = React.for
const [beakPositioningProps, transformOrigin] = useBeakPosition(props, targetAlignment, targetPosition);
const [isMeasuring, entityInnerHostRect] = useEntityHostMeasurements(props, entityInnerHostElementRef);
const [bounds, setBounds] = React.useState<IRectangle | undefined>(
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<ICoachmarkProps> = 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 {

Просмотреть файл

@ -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<number | undefined>();
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;
}

Просмотреть файл

@ -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<IColorRectangleProps, IColorRectangleState>
implements IColorRectangle
{
public static contextType = WindowContext;
public static defaultProps: Partial<IColorRectangleProps> = {
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);

Просмотреть файл

@ -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<IComboBoxInternalProps, IComboBoxState> implements IComboBox {
public static contextType = WindowContext;
/** The input aspect of the combo box */
private _autofill = React.createRef<IAutofill>();
@ -368,6 +372,7 @@ class ComboBoxInternal extends React.Component<IComboBoxInternalProps, IComboBox
this._async.setTimeout(() => 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<IComboBoxInternalProps, IComboBox
!isOpen &&
this._focusInputAfterClose &&
this._autofill.current &&
document.activeElement !== this._autofill.current.inputElement))
doc?.activeElement !== this._autofill.current.inputElement))
) {
this.focus(undefined /*shouldOpenOnFocus*/, true /*useFocusAsync*/);
}
@ -1221,6 +1226,7 @@ class ComboBoxInternal extends React.Component<IComboBoxInternalProps, IComboBox
*/
// eslint-disable-next-line deprecation/deprecation
private _onBlur = (event: React.FocusEvent<HTMLElement | Autofill | BaseButton | Button>): void => {
const doc = getDocumentEx(this.context);
// Do nothing if the blur is coming from something
// inside the comboBox root or the comboBox menu since
// it we are not really blurring from the whole comboBox
@ -1231,7 +1237,7 @@ class ComboBoxInternal extends React.Component<IComboBoxInternalProps, IComboBox
// 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 as Element;
relatedTarget = doc?.activeElement as Element;
}
if (relatedTarget) {
@ -1239,7 +1245,7 @@ class ComboBoxInternal extends React.Component<IComboBoxInternalProps, IComboBox
const isBlurFromComboBoxMenu = this._comboBoxMenu.current?.contains(relatedTarget as HTMLElement);
const isBlurFromComboBoxMenuAncestor =
this._comboBoxMenu.current &&
findElementRecursive(this._comboBoxMenu.current, (element: HTMLElement) => element === relatedTarget);
findElementRecursive(this._comboBoxMenu.current, (element: HTMLElement) => element === relatedTarget, doc);
if (isBlurFromComboBoxTitle || isBlurFromComboBoxMenu || isBlurFromComboBoxMenuAncestor) {
if (

Просмотреть файл

@ -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<IDetailsListStyleProps, IDetailsListStyles>();
const COMPONENT_NAME = 'DetailsList';
@ -779,6 +781,8 @@ export class DetailsListBase extends React.Component<IDetailsListProps, IDetails
useFastIcons: true,
};
public static contextType = WindowContext;
// References
private _async: Async;
private _root = React.createRef<HTMLDivElement>();
@ -934,6 +938,8 @@ export class DetailsListBase extends React.Component<IDetailsListProps, IDetails
public componentDidUpdate(prevProps: IDetailsListProps, prevState: IDetailsListState) {
this._notifyColumnsResized();
const doc = getDocumentEx(this.context);
if (this._initialFocusedIndex !== undefined) {
const item = this.props.items[this._initialFocusedIndex];
if (item) {
@ -949,7 +955,7 @@ export class DetailsListBase extends React.Component<IDetailsListProps, IDetails
this.props.items !== prevProps.items &&
this.props.items.length > 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,

Просмотреть файл

@ -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<IDocumentCardStyleProps, IDocumentCardStyles>();
@ -31,6 +33,8 @@ export class DocumentCardBase extends React.Component<IDocumentCardProps, any> i
type: DocumentCardType.normal,
};
public static contextType = WindowContext;
private _rootElement = React.createRef<HTMLDivElement>();
private _classNames: IProcessedStyleSet<IDocumentCardStyles>;
@ -109,14 +113,16 @@ export class DocumentCardBase extends React.Component<IDocumentCardProps, any> i
private _onAction = (ev: React.SyntheticEvent<HTMLElement>): 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();

Просмотреть файл

@ -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<IDocumentCardTitleStyleProps, IDocumentCardTitleStyles>();
@ -23,6 +25,8 @@ const TRUNCATION_VERTICAL_OVERFLOW_THRESHOLD = 5;
* {@docCategory DocumentCard}
*/
export class DocumentCardTitleBase extends React.Component<IDocumentCardTitleProps, IDocumentCardTitleState> {
public static contextType = WindowContext;
private _titleElement = React.createRef<HTMLDivElement>();
private _classNames: IProcessedStyleSet<IDocumentCardTitleStyles>;
private _async: Async;
@ -53,12 +57,13 @@ export class DocumentCardTitleBase extends React.Component<IDocumentCardTitlePro
}
if (prevProps.shouldTruncate !== this.props.shouldTruncate) {
const win = getWindowEx(this.context);
if (this.props.shouldTruncate) {
this._truncateTitle();
this._async.requestAnimationFrame(this._shrinkTitle);
this._events.on(window, 'resize', this._updateTruncation);
this._events.on(win, 'resize', this._updateTruncation);
} else {
this._events.off(window, 'resize', this._updateTruncation);
this._events.off(win, 'resize', this._updateTruncation);
}
} else if (this._needMeasurement) {
this._async.requestAnimationFrame(() => {
@ -71,7 +76,8 @@ export class DocumentCardTitleBase extends React.Component<IDocumentCardTitlePro
public componentDidMount(): void {
if (this.props.shouldTruncate) {
this._truncateTitle();
this._events.on(window, 'resize', this._updateTruncation);
const win = getWindowEx(this.context);
this._events.on(win, 'resize', this._updateTruncation);
}
}

Просмотреть файл

@ -50,6 +50,8 @@ import type { IWithResponsiveModeState } from '../../ResponsiveMode';
import type { ISelectableDroppableTextProps } from '../../SelectableOption';
import type { ICheckboxStyleProps, ICheckboxStyles } from '../../Checkbox';
import { IFocusTrapZoneProps } from '../FocusTrapZone/FocusTrapZone.types';
import { WindowContext } from '@fluentui/react-window-provider';
import { getDocumentEx, getWindowEx } from '../../utilities/dom';
const COMPONENT_NAME = 'Dropdown';
const getClassNames = classNamesFunction<IDropdownStyleProps, IDropdownStyles>();
@ -187,6 +189,8 @@ class DropdownInternal extends React.Component<IDropdownInternalProps, IDropdown
options: [] as IDropdownOption[],
};
public static contextType = WindowContext;
private _host = React.createRef<HTMLDivElement>();
private _focusZone = React.createRef<FocusZone>();
private _dropDown = React.createRef<HTMLDivElement>();
@ -937,14 +941,15 @@ class DropdownInternal extends React.Component<IDropdownInternalProps, IDropdown
* for updating focus are not interacting during scroll
*/
private _onScroll = (): void => {
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<IDropdownInternalProps, IDropdown
}
private _onItemMouseMove(item: any, ev: React.MouseEvent<HTMLElement>): 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;
}

Просмотреть файл

@ -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<IFocusTrapZoneProps> & {
const lastBumper = React.useRef<HTMLDivElement>(null);
const mergedRootRef = useMergedRefs(root, ref) as React.Ref<HTMLDivElement>;
const doc = useDocument();
const win = useWindowEx()!;
const isFirstRender = usePrevious(false) ?? true;
@ -263,17 +265,17 @@ export const FocusTrapZone: React.FunctionComponent<IFocusTrapZoneProps> & {
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(() => {

Просмотреть файл

@ -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<IKeytipLayerProps, IKeytipL
content: '',
};
public static contextType = WindowContext;
private _events: EventGroup;
private _async: Async;
@ -133,13 +137,14 @@ export class KeytipLayerBase extends React.Component<IKeytipLayerProps, IKeytipL
}
public componentDidMount(): void {
const win = getWindowEx(this.context);
// Add window listeners
this._events.on(window, 'mouseup', this._onDismiss, true /* useCapture */);
this._events.on(window, 'pointerup', this._onDismiss, true /* useCapture */);
this._events.on(window, 'resize', this._onDismiss);
this._events.on(window, 'keydown', this._onKeyDown, true /* useCapture */);
this._events.on(window, 'keypress', this._onKeyPress, true /* useCapture */);
this._events.on(window, 'scroll', this._onDismiss, true /* useCapture */);
this._events.on(win, 'mouseup', this._onDismiss, true /* useCapture */);
this._events.on(win, 'pointerup', this._onDismiss, true /* useCapture */);
this._events.on(win, 'resize', this._onDismiss);
this._events.on(win, 'keydown', this._onKeyDown, true /* useCapture */);
this._events.on(win, 'keypress', this._onKeyPress, true /* useCapture */);
this._events.on(win, 'scroll', this._onDismiss, true /* useCapture */);
// Add keytip listeners
this._events.on(this._keytipManager, KeytipEvents.ENTER_KEYTIP_MODE, this._enterKeytipMode);
@ -384,13 +389,15 @@ export class KeytipLayerBase extends React.Component<IKeytipLayerProps, IKeytipL
}
private _isKeytipInstanceTargetVisible = (keySequences: string[], instanceCount: number): boolean => {
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;
};

Просмотреть файл

@ -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];

Просмотреть файл

@ -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<T = any> extends React.Component<IListProps<T>, IListState<T>>
renderedWindowsBehind: DEFAULT_RENDERED_WINDOWS_BEHIND,
};
public static contextType = WindowContext;
private _root = React.createRef<HTMLDivElement>();
private _surface = React.createRef<HTMLDivElement>();
private _pageRefs: Record<string, unknown> = {};
@ -347,7 +351,9 @@ export class List<T = any> extends React.Component<IListProps<T>, IListState<T>>
this.setState({ ...this._updatePages(this.props, this.state), hasMounted: true });
this._measureVersion++;
this._events.on(window, 'resize', this._onAsyncResizeDebounced);
const win = getWindowEx(this.context);
this._events.on(win, 'resize', this._onAsyncResizeDebounced);
if (this._root.current) {
this._events.on(this._root.current, 'focus', this._onFocus, true);
}

Просмотреть файл

@ -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<IMarqueeSelectionStyleProps, IMarqueeSelectionStyles>();
@ -41,6 +43,8 @@ export class MarqueeSelectionBase extends React.Component<IMarqueeSelectionProps
isEnabled: true,
};
public static contextType = WindowContext;
private _async: Async;
private _events: EventGroup;
private _root = React.createRef<HTMLDivElement>();
@ -71,8 +75,11 @@ export class MarqueeSelectionBase extends React.Component<IMarqueeSelectionProps
}
public componentDidMount(): void {
const win = getWindowEx(this.context);
const doc = getDocumentEx(this.context);
this._scrollableParent = findScrollableParent(this._root.current) as HTMLElement;
this._scrollableSurface = this._scrollableParent === (window as any) ? document.body : this._scrollableParent;
this._scrollableSurface = this._scrollableParent === (win as any) ? doc?.body : this._scrollableParent;
// When scroll events come from window, we need to read scrollTop values from the body.
const hitTarget = this.props.isDraggingConstrainedToRoot ? this._root.current : this._scrollableSurface;
@ -163,13 +170,14 @@ export class MarqueeSelectionBase extends React.Component<IMarqueeSelectionProps
(!onShouldStartSelection || onShouldStartSelection(ev))
) {
if (this._scrollableSurface && ev.button === 0 && this._root.current) {
const win = getWindowEx(this.context);
this._selectedIndicies = {};
this._preservedIndicies = undefined;
this._events.on(window, 'mousemove', this._onAsyncMouseMove, true);
this._events.on(win, 'mousemove', this._onAsyncMouseMove, true);
this._events.on(this._scrollableParent, 'scroll', this._onAsyncMouseMove);
this._events.on(window, 'click', this._onMouseUp, true);
this._events.on(win, 'click', this._onMouseUp, true);
this._autoScroll = new AutoScroll(this._root.current);
this._autoScroll = new AutoScroll(this._root.current, win);
this._scrollTop = this._scrollableSurface.scrollTop;
this._scrollLeft = this._scrollableSurface.scrollLeft;
this._rootRect = this._root.current.getBoundingClientRect();
@ -277,7 +285,8 @@ export class MarqueeSelectionBase extends React.Component<IMarqueeSelectionProps
}
private _onMouseUp(ev: MouseEvent): void {
this._events.off(window);
const win = getWindowEx(this.context);
this._events.off(win);
this._events.off(this._scrollableParent, 'scroll');
if (this._autoScroll) {

Просмотреть файл

@ -15,6 +15,8 @@ import type {
INavStyles,
IRenderGroupHeaderProps,
} from './Nav.types';
import { WindowContext } from '@fluentui/react-window-provider';
import { getDocumentEx } from '../../utilities/dom';
// The number pixels per indentation level for Nav links.
const _indentationSize = 14;
@ -43,6 +45,8 @@ export class NavBase extends React.Component<INavProps, INavState> implements IN
groups: null,
};
public static contextType = WindowContext;
private _focusZone = React.createRef<IFocusZone>();
constructor(props: INavProps) {
super(props);
@ -360,8 +364,9 @@ export class NavBase extends React.Component<INavProps, INavState> 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;

Просмотреть файл

@ -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<IOverflowSetStyleProps, IOverflowSetStyles>();
const COMPONENT_NAME = 'OverflowSet';
const useComponentRef = (props: IOverflowSetProps, divContainer: React.RefObject<HTMLDivElement>) => {
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],
);
};

Просмотреть файл

@ -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<IPanelStyleProps, IPanelStyles>();
const COMPONENT_NAME = 'Panel';
@ -48,6 +50,8 @@ export class PanelBase extends React.Component<IPanelProps, IPanelState> impleme
type: PanelType.smallFixedFar,
};
public static contextType = WindowContext;
private _async: Async;
private _events: EventGroup;
private _panel = React.createRef<HTMLDivElement>();
@ -107,11 +111,13 @@ export class PanelBase extends React.Component<IPanelProps, IPanelState> 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<IPanelProps, IPanelState> 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);
}
}

Просмотреть файл

@ -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<IScrollablePaneProps, IScrollablePaneState>
implements IScrollablePane
{
public static contextType = WindowContext;
private _root = React.createRef<HTMLDivElement>();
private _stickyAboveRef = React.createRef<HTMLDivElement>();
private _stickyBelowRef = React.createRef<HTMLDivElement>();
@ -78,9 +82,10 @@ export class ScrollablePaneBase
}
public componentDidMount() {
const win = getWindowEx(this.context);
const { initialScrollPosition } = this.props;
this._events.on(this.contentContainer, 'scroll', this._onScroll);
this._events.on(window, 'resize', this._onWindowResize);
this._events.on(win, 'resize', this._onWindowResize);
if (this.contentContainer && initialScrollPosition) {
this.contentContainer.scrollTop = initialScrollPosition;
}
@ -92,7 +97,7 @@ export class ScrollablePaneBase
});
this.notifySubscribers();
if ('MutationObserver' in window) {
if (win && 'MutationObserver' in win) {
this._mutationObserver = new MutationObserver(mutation => {
// Function to check if mutation is occuring in stickyAbove or stickyBelow
function checkIfMutationIsSticky(mutationRecord: MutationRecord): boolean {
@ -354,6 +359,7 @@ export class ScrollablePaneBase
notifySubscribers: this.notifySubscribers,
syncScrollSticky: this.syncScrollSticky,
},
window: getWindowEx(this.context),
};
};

Просмотреть файл

@ -130,6 +130,10 @@ export interface IScrollablePaneContext {
notifySubscribers: (sort?: boolean) => void;
syncScrollSticky: (sticky: Sticky) => void;
};
window: Window | undefined;
}
export const ScrollablePaneContext = React.createContext<IScrollablePaneContext>({ scrollablePane: undefined });
export const ScrollablePaneContext = React.createContext<IScrollablePaneContext>({
scrollablePane: undefined,
window: undefined,
});

Просмотреть файл

@ -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<T> {
items: T[];
@ -16,6 +18,8 @@ export class BaseSelectedItemsList<T, P extends IBaseSelectedItemsListProps<T>>
extends React.Component<P, IBaseSelectedItemsListState<T>>
implements IBaseSelectedItemsList<T>
{
public static contextType = WindowContext;
protected root: HTMLElement;
private _defaultSelection: Selection;
@ -213,22 +217,23 @@ export class BaseSelectedItemsList<T, P extends IBaseSelectedItemsListProps<T>>
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);
}
}
}

Просмотреть файл

@ -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<HTMLDivEl
const disposables = React.useRef<(() => void)[]>([]);
const { setTimeout, clearTimeout } = useSetTimeout();
const sliderLine = React.useRef<HTMLDivElement>(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<HTMLDivEl
newValue - internalState.latestLowerValue <= internalState.latestValue - newValue;
}
// safe to use `win!` since it can only be called on the client
if (event.type === 'mousedown') {
disposables.current.push(
on(window, 'mousemove', onMouseMoveOrTouchMove as (ev: Event) => 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);

Просмотреть файл

@ -263,6 +263,8 @@ export class Sticky extends React.Component<IStickyProps, IStickyState> {
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<IStickyProps, IStickyState> {
}
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<IStickyProps, IStickyState> {
}
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<IStickyProps, IStickyState> {
curr = curr.parentElement;
}
}
return window.getComputedStyle(curr).getPropertyValue('background-color');
return win.getComputedStyle(curr).getPropertyValue('background-color');
}
}

Просмотреть файл

@ -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<ISwatchColorPickerPr
>((props, ref) => {
const defaultId = useId('swatchColorPicker');
const id = props.id || defaultId;
const doc = useDocumentEx();
const internalState = useConst<ISwatchColorPickerInternalState>({
isNavigationIdle: true,
@ -161,13 +163,13 @@ export const SwatchColorPickerBase: React.FunctionComponent<ISwatchColorPickerPr
const targetElement = ev.currentTarget as HTMLElement;
// If navigation is idle and the targetElement is the focused element bail out
if (internalState.isNavigationIdle && !(document && targetElement === (document.activeElement as HTMLElement))) {
if (internalState.isNavigationIdle && !(doc && targetElement === (doc.activeElement as HTMLElement))) {
targetElement.focus();
}
return true;
},
[focusOnHover, internalState, disabled],
[focusOnHover, internalState, disabled, doc],
);
/**
@ -182,7 +184,7 @@ export const SwatchColorPickerBase: React.FunctionComponent<ISwatchColorPickerPr
}
// Get the elements that math the given selector
const elements = document.querySelectorAll(parentSelector);
const elements = doc?.querySelectorAll(parentSelector) ?? [];
// iterate over the elements return to make sure it is a parent of the target and focus it
for (let index = 0; index < elements.length; index += 1) {
@ -206,7 +208,7 @@ export const SwatchColorPickerBase: React.FunctionComponent<ISwatchColorPickerPr
}
}
},
[disabled, focusOnHover, internalState, mouseLeaveParentSelector],
[disabled, focusOnHover, internalState, mouseLeaveParentSelector, doc],
);
/**

Просмотреть файл

@ -8,7 +8,12 @@ export const getStyles = (props: ITooltipStyleProps): ITooltipStyles => {
// 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: [

Просмотреть файл

@ -16,6 +16,8 @@ import { TooltipOverflowMode } from './TooltipHost.types';
import { Tooltip } from './Tooltip';
import { TooltipDelay } from './Tooltip.types';
import type { ITooltipHostProps, ITooltipHostStyles, ITooltipHostStyleProps, ITooltipHost } from './TooltipHost.types';
import { WindowContext } from '@fluentui/react-window-provider';
import { getDocumentEx } from '../../utilities/dom';
export interface ITooltipHostState {
/** @deprecated No longer used internally */
@ -30,6 +32,7 @@ export class TooltipHostBase extends React.Component<ITooltipHostProps, ITooltip
delay: TooltipDelay.medium,
};
public static contextType = WindowContext;
private static _currentVisibleTooltip: ITooltipHost | undefined;
// The wrapping div that gets the hover events
@ -192,7 +195,7 @@ export class TooltipHostBase extends React.Component<ITooltipHostProps, ITooltip
// checking if the blurred element is still the document's activeElement,
// and ignoring when it next gets focus back.
// See https://github.com/microsoft/fluentui/issues/13541
this._ignoreNextFocusEvent = document?.activeElement === ev.target;
this._ignoreNextFocusEvent = getDocumentEx(this.context)?.activeElement === ev.target;
this._dismissTimerId = this._async.setTimeout(() => {
this._hideTooltip();
@ -202,6 +205,7 @@ export class TooltipHostBase extends React.Component<ITooltipHostProps, ITooltip
// Show Tooltip
private _onTooltipMouseEnter = (ev: any): void => {
const { overflowMode, delay } = this.props;
const doc = getDocumentEx(this.context);
if (TooltipHostBase._currentVisibleTooltip && TooltipHostBase._currentVisibleTooltip !== this) {
TooltipHostBase._currentVisibleTooltip.dismiss();
@ -215,7 +219,7 @@ export class TooltipHostBase extends React.Component<ITooltipHostProps, ITooltip
}
}
if (ev.target && portalContainsElement(ev.target as HTMLElement, this._getTargetElement())) {
if (ev.target && portalContainsElement(ev.target as HTMLElement, this._getTargetElement(), doc)) {
// Do not show tooltip when target is inside a portal relative to TooltipHost.
return;
}

Просмотреть файл

@ -29,6 +29,8 @@ import type {
import type { IBasePicker, IBasePickerProps, IBasePickerStyleProps, IBasePickerStyles } from './BasePicker.types';
import type { IAutofill } from '../Autofill/index';
import type { IPickerItemProps } from './PickerItem.types';
import { WindowContext } from '@fluentui/react-window-provider';
import { getDocumentEx } from '../../utilities/dom';
const legacyStyles: any = stylesImport;
@ -95,6 +97,8 @@ export class BasePicker<T, P extends IBasePickerProps<T>>
extends React.Component<P, IBasePickerState<T>>
implements IBasePicker<T>
{
public static contextType = WindowContext;
// Refs
protected root = React.createRef<HTMLDivElement>();
protected input = React.createRef<IAutofill>();
@ -505,7 +509,8 @@ export class BasePicker<T, P extends IBasePickerProps<T>>
});
} else {
this.setState({
suggestionsVisible: this.input.current! && this.input.current!.inputElement === document.activeElement,
suggestionsVisible:
this.input.current! && this.input.current!.inputElement === getDocumentEx(this.context)?.activeElement,
});
}
@ -599,7 +604,7 @@ export class BasePicker<T, P extends IBasePickerProps<T>>
// even when it's not. Using document.activeElement is another way
// for us to be able to get what the relatedTarget without relying
// on the event
relatedTarget = document.activeElement;
relatedTarget = getDocumentEx(this.context)!.activeElement;
}
if (relatedTarget && !elementContains(this.root.current!, relatedTarget as HTMLElement)) {
this.setState({ isFocused: false });
@ -1032,7 +1037,7 @@ export class BasePicker<T, P extends IBasePickerProps<T>>
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;

Просмотреть файл

@ -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<T> = React.MouseEvent<T> & React.TouchEvent<T> & Event;
export class DraggableZone extends React.Component<IDraggableZoneProps, IDraggableZoneState> {
public static contextType = WindowContext;
private _touchId?: number;
private _currentEventType = eventMapping.mouse;
private _events: (() => void)[] = [];
@ -153,9 +157,10 @@ export class DraggableZone extends React.Component<IDraggableZoneProps, IDraggab
// hook up the appropriate mouse/touch events to the body to ensure
// smooth dragging
const doc = getDocumentEx(this.context)!;
this._events = [
on(document.body, this._currentEventType.move, this._onDrag, true /* use capture phase */),
on(document.body, this._currentEventType.stop, this._onDragStop, true /* use capture phase */),
on(doc.body, this._currentEventType.move, this._onDrag, true /* use capture phase */),
on(doc.body, this._currentEventType.stop, this._onDragStop, true /* use capture phase */),
];
};
@ -259,7 +264,7 @@ export class DraggableZone extends React.Component<IDraggableZoneProps, IDraggab
* Returns if an element (or any of the element's parents) match the given selector
*/
private _matchesSelector(element: HTMLElement | null, selector: string): boolean {
if (!element || element === document.body) {
if (!element || element === getDocumentEx(this.context)?.body) {
return false;
}

Просмотреть файл

@ -1,3 +1,4 @@
import { getDocument } from '@fluentui/utilities';
import { MAX_COLOR_ALPHA } from './consts';
import { hsl2rgb } from './hsl2rgb';
import type { IRGB } from './interfaces';
@ -8,11 +9,13 @@ import type { IRGB } from './interfaces';
* Alpha in returned color defaults to 100.
* Four and eight digit hex values (with alpha) are supported if the current browser supports them.
*/
export function cssColor(color?: string): IRGB | undefined {
export function cssColor(color?: string, doc?: Document): IRGB | undefined {
if (!color) {
return undefined;
}
const theDoc = doc ?? getDocument()!;
// Need to check the following valid color formats: RGB(A), HSL(A), hex, named color
// First check for well formatted RGB(A), HSL(A), and hex formats at the start.
@ -24,7 +27,7 @@ export function cssColor(color?: string): IRGB | undefined {
}
// if the above fails, do the more expensive catch-all
return _browserCompute(color);
return _browserCompute(color, theDoc);
}
/**
@ -33,12 +36,12 @@ export function cssColor(color?: string): IRGB | undefined {
* This works by attaching an element to the DOM, which may fail in server-side rendering
* or with headless browsers.
*/
function _browserCompute(str: string): IRGB | undefined {
if (typeof document === 'undefined') {
function _browserCompute(str: string, doc: Document): IRGB | undefined {
if (typeof doc === 'undefined') {
// don't throw an error when used server-side
return undefined;
}
const elem = document.createElement('div');
const elem = doc.createElement('div');
elem.style.backgroundColor = str;
// This element must be attached to the DOM for getComputedStyle() to have a value
elem.style.position = 'absolute';
@ -46,10 +49,10 @@ function _browserCompute(str: string): IRGB | undefined {
elem.style.left = '-9999px';
elem.style.height = '1px';
elem.style.width = '1px';
document.body.appendChild(elem);
const eComputedStyle = getComputedStyle(elem);
doc.body.appendChild(elem);
const eComputedStyle = doc.defaultView?.getComputedStyle(elem);
const computedColor = eComputedStyle && eComputedStyle.backgroundColor;
document.body.removeChild(elem);
doc.body.removeChild(elem);
// computedColor is always an RGB(A) string, except for invalid colors in IE/Edge which return 'transparent'
// browsers return one of these if the color string is invalid,

Просмотреть файл

@ -1,3 +1,4 @@
import { getDocument } from '@fluentui/utilities';
import { cssColor } from './cssColor';
import { getColorFromRGBA } from './getColorFromRGBA';
import type { IColor } from './interfaces';
@ -10,8 +11,9 @@ import type { IColor } from './interfaces';
* Alpha defaults to 100 if not specified in `inputColor`.
* Returns undefined if the color string is invalid/not recognized.
*/
export function getColorFromString(inputColor: string): IColor | undefined {
const color = cssColor(inputColor);
export function getColorFromString(inputColor: string, doc?: Document): IColor | undefined {
const theDoc = doc ?? getDocument()!;
const color = cssColor(inputColor, theDoc);
if (!color) {
return;

Просмотреть файл

@ -195,9 +195,10 @@ export function withViewport<TProps extends { viewport?: IViewport }, TState>(
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();

Просмотреть файл

@ -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<WindowProviderProps, 'window'> | 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<WindowProviderProps, 'window'> | undefined) => {
// eslint-disable-next-line no-restricted-globals
return ctx?.window ?? typeof window !== 'undefined' ? window : undefined;
};

Просмотреть файл

@ -1,5 +1,5 @@
import { DirectionalHint } from '../../common/DirectionalHint';
import { getScrollbarWidth, getRTL } from '../../Utilities';
import { getScrollbarWidth, getRTL, getWindow } from '../../Utilities';
import { RectangleEdge } from './positioning.types';
import { Rectangle } from '../../Utilities';
import type { IRectangle, Point } from '../../Utilities';
@ -917,10 +917,12 @@ function _positionElement(
hostElement: HTMLElement,
elementToPosition: HTMLElement,
previousPositions?: IPositionedData,
win?: Window,
): IPositionedData {
const theWin = win ?? getWindow()!;
const boundingRect: Rectangle = props.bounds
? _getRectangleFromIRect(props.bounds)
: new Rectangle(0, window.innerWidth - getScrollbarWidth(), 0, window.innerHeight);
: new Rectangle(0, theWin.innerWidth - getScrollbarWidth(), 0, theWin.innerHeight);
const positionedElement: IElementPosition = _positionElementRelative(
props,
elementToPosition,
@ -942,14 +944,16 @@ function _positionCallout(
shouldScroll = false,
minimumScrollResizeHeight?: number,
doNotFinalizeReturnEdge?: boolean,
win?: Window,
): ICalloutPositionedInfo {
const theWin = win ?? getWindow()!;
const beakWidth: number = props.isBeakVisible ? props.beakWidth || 0 : 0;
const gap = _calculateGapSpace(props.isBeakVisible, props.beakWidth, props.gapSpace);
const positionProps: IPositionProps = props;
positionProps.gapSpace = gap;
const boundingRect: Rectangle = props.bounds
? _getRectangleFromIRect(props.bounds)
: new Rectangle(0, window.innerWidth - getScrollbarWidth(), 0, window.innerHeight);
: new Rectangle(0, theWin.innerWidth - getScrollbarWidth(), 0, theWin.innerHeight);
const positionedElement: IElementPositionInfo = _positionElementRelative(
positionProps,
@ -978,8 +982,10 @@ function _positionCard(
hostElement: HTMLElement,
callout: HTMLElement,
previousPositions?: ICalloutPositionedInfo,
win?: Window,
): ICalloutPositionedInfo {
return _positionCallout(props, hostElement, callout, previousPositions, false, undefined, true);
const theWin = win ?? getWindow()!;
return _positionCallout(props, hostElement, callout, previousPositions, false, undefined, true, theWin);
}
function _getRectangleFromTarget(target: Element | MouseEvent | Point | Rectangle): Rectangle {
@ -1028,8 +1034,9 @@ export function positionElement(
hostElement: HTMLElement,
elementToPosition: HTMLElement,
previousPositions?: IPositionedData,
win?: Window,
): IPositionedData {
return _positionElement(props, hostElement, elementToPosition, previousPositions);
return _positionElement(props, hostElement, elementToPosition, previousPositions, win);
}
export function positionCallout(
@ -1039,6 +1046,7 @@ export function positionCallout(
previousPositions?: ICalloutPositionedInfo,
shouldScroll?: boolean,
minimumScrollResizeHeight?: number,
win?: Window,
): ICalloutPositionedInfo {
return _positionCallout(
props,
@ -1047,6 +1055,8 @@ export function positionCallout(
previousPositions,
shouldScroll,
minimumScrollResizeHeight,
undefined,
win,
);
}
@ -1055,8 +1065,9 @@ export function positionCard(
hostElement: HTMLElement,
elementToPosition: HTMLElement,
previousPositions?: ICalloutPositionedInfo,
win?: Window,
): ICalloutPositionedInfo {
return _positionCard(props, hostElement, elementToPosition, previousPositions);
return _positionCard(props, hostElement, elementToPosition, previousPositions, win);
}
/**
@ -1071,11 +1082,13 @@ export function getMaxHeight(
gapSpace: number = 0,
bounds?: IRectangle,
coverTarget?: boolean,
win?: Window,
): number {
const theWin = win ?? getWindow()!;
const targetRect = _getRectangleFromTarget(target);
const boundingRectangle = bounds
? _getRectangleFromIRect(bounds)
: new Rectangle(0, window.innerWidth - getScrollbarWidth(), 0, window.innerHeight);
: new Rectangle(0, theWin.innerWidth - getScrollbarWidth(), 0, theWin.innerHeight);
return _getMaxHeightFromTargetRectangle(targetRect, targetEdge, gapSpace, boundingRectangle, coverTarget);
}

Просмотреть файл

@ -200,12 +200,13 @@ export class SelectionZone extends React.Component<ISelectionZoneProps, ISelecti
public componentDidMount(): void {
const win = getWindow(this._root.current);
const doc = win?.document;
// Track the latest modifier keys globally.
this._events.on(win, 'keydown, keyup', this._updateModifiers, true);
this._events.on(document, 'click', this._findScrollParentAndTryClearOnEmptyClick);
this._events.on(document.body, 'touchstart', this._onTouchStartCapture, true);
this._events.on(document.body, 'touchend', this._onTouchStartCapture, true);
this._events.on(doc, 'click', this._findScrollParentAndTryClearOnEmptyClick);
this._events.on(doc?.body, 'touchstart', this._onTouchStartCapture, true);
this._events.on(doc?.body, 'touchend', this._onTouchStartCapture, true);
// Subscribe to the selection to keep modal state updated.
this._events.on(this.props.selection, 'change', this._onSelectionChange);
@ -274,8 +275,10 @@ export class SelectionZone extends React.Component<ISelectionZoneProps, ISelecti
private _onMouseDownCapture = (ev: React.MouseEvent<HTMLElement>): 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<ISelectionZoneProps, ISelecti
* so this is less likely to cause layout thrashing then doing it in mount.
*/
private _findScrollParentAndTryClearOnEmptyClick(ev: MouseEvent) {
const win = getWindow(this._root.current);
const doc = win?.document;
const scrollParent = findScrollableParent(this._root.current) as HTMLElement;
// unbind this handler and replace binding with a binding on the actual scrollable parent
this._events.off(document, 'click', this._findScrollParentAndTryClearOnEmptyClick);
this._events.off(doc, 'click', this._findScrollParentAndTryClearOnEmptyClick);
this._events.on(scrollParent, 'click', this._tryClearOnEmptyClick);
// If we clicked inside the scrollable parent, call through to the handler on this click.

Просмотреть файл

@ -87,7 +87,7 @@ export const audioProperties: Record<string, number>;
// @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<TProps extends {}>(defaultProps: Partial<TProps>, 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<TA extends any, TB extends any>(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<TComponentProps extends IPropsWithStyles<TStyleProps, TStyleSet>, TStyleProps, TStyleSet extends IStyleSet<TStyleSet>>(Component: React_2.ComponentClass<TComponentProps> | React_2.FunctionComponent<TComponentProps>, baseStyles: IStyleFunctionOrObject<TStyleProps, TStyleSet>, getProps?: (props: TComponentProps) => Partial<TComponentProps>, customizable?: ICustomizableProps, pure?: boolean): React_2.FunctionComponent<TComponentProps>;

Просмотреть файл

@ -2,6 +2,7 @@ import { EventGroup } from './EventGroup';
import { findScrollableParent } from './scroll';
import { getRect } from './dom/getRect';
import type { IRectangle } from './IRectangle';
import { getWindow } from './dom';
declare function setTimeout(cb: Function, delay: number): number;
@ -26,21 +27,22 @@ export class AutoScroll {
private _isVerticalScroll!: boolean;
private _timeoutId?: number;
constructor(element: HTMLElement) {
constructor(element: HTMLElement, win?: Window) {
const theWin = win ?? getWindow(element)!;
this._events = new EventGroup(this);
this._scrollableParent = findScrollableParent(element) as HTMLElement;
this._incrementScroll = this._incrementScroll.bind(this);
this._scrollRect = getRect(this._scrollableParent);
this._scrollRect = getRect(this._scrollableParent, theWin);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this._scrollableParent === (window as any)) {
this._scrollableParent = document.body;
if (this._scrollableParent === (theWin as any)) {
this._scrollableParent = theWin.document.body;
}
if (this._scrollableParent) {
this._events.on(window, 'mousemove', this._onMouseMove, true);
this._events.on(window, 'touchmove', this._onTouchMove, true);
this._events.on(theWin, 'mousemove', this._onMouseMove, true);
this._events.on(theWin, 'touchmove', this._onTouchMove, true);
}
}

Просмотреть файл

@ -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<IDelayedRenderProps, IDelayed
public componentDidMount(): void {
let { delay } = this.props;
// eslint-disable-next-line no-restricted-globals
this._timeoutId = window.setTimeout(() => {
this.setState({
isRendered: true,

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -67,7 +67,7 @@ export class FabricPerformance {
measurement.totalDuration += duration;
measurement.count++;
measurement.all.push({
duration: duration,
duration,
timeStamp: end,
});
FabricPerformance.summary[name] = measurement;

Просмотреть файл

@ -3,11 +3,11 @@
*/
export function canUseDOM(): boolean {
return (
// eslint-disable-next-line no-restricted-globals
typeof window !== 'undefined' &&
!!(
window.document &&
// eslint-disable-next-line deprecation/deprecation
window.document.createElement
// eslint-disable-next-line no-restricted-globals, deprecation/deprecation
(window.document && window.document.createElement)
)
);
}

Просмотреть файл

@ -8,11 +8,13 @@ import { canUseDOM } from './canUseDOM';
* @public
*/
export function getDocument(rootElement?: HTMLElement | null): Document | undefined {
// eslint-disable-next-line no-restricted-globals
if (!canUseDOM() || typeof document === 'undefined') {
return undefined;
} else {
const el = rootElement as Element;
// eslint-disable-next-line no-restricted-globals
return el && el.ownerDocument ? el.ownerDocument : document;
}
}

Просмотреть файл

@ -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),
);
}

Просмотреть файл

@ -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();

Просмотреть файл

@ -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 */

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -382,12 +382,13 @@ export function isElementVisible(element: HTMLElement | undefined | null): boole
*
* @public
*/
export function isElementVisibleAndNotHidden(element: HTMLElement | undefined | null): boolean {
export function isElementVisibleAndNotHidden(element: HTMLElement | undefined | null, win?: Window): boolean {
const theWin = win ?? getWindow()!;
return (
!!element &&
isElementVisible(element) &&
!element.hidden &&
window.getComputedStyle(element).visibility !== 'hidden'
theWin.getComputedStyle(element).visibility !== 'hidden'
);
}
@ -456,8 +457,8 @@ export function isElementFocusSubZone(element?: HTMLElement): boolean {
* @public
*/
export function doesElementContainFocus(element: HTMLElement): boolean {
let document = getDocument(element);
let currentActiveElement: HTMLElement | undefined = document && (document.activeElement as HTMLElement);
let doc = getDocument(element);
let currentActiveElement: HTMLElement | undefined = doc && (doc.activeElement as HTMLElement);
if (currentActiveElement && elementContains(element, currentActiveElement)) {
return true;
}
@ -473,8 +474,10 @@ export function doesElementContainFocus(element: HTMLElement): boolean {
export function shouldWrapFocus(
element: HTMLElement,
noWrapDataAttribute: 'data-no-vertical-wrap' | 'data-no-horizontal-wrap',
doc?: Document,
): boolean {
return elementContainsAttribute(element, noWrapDataAttribute) === 'true' ? false : true;
const theDoc = doc ?? getDocument()!;
return elementContainsAttribute(element, noWrapDataAttribute, theDoc) === 'true' ? false : true;
}
let animationId: number | undefined = undefined;

Просмотреть файл

@ -3,8 +3,10 @@
* Used to determine whether iOS-specific behavior should be applied.
*/
export const isIOS = (): boolean => {
// eslint-disable-next-line no-restricted-globals
if (!window || !window.navigator || !window.navigator.userAgent) {
return false;
}
// eslint-disable-next-line no-restricted-globals
return /iPad|iPhone|iPod/i.test(window.navigator.userAgent);
};

Просмотреть файл

@ -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;

Просмотреть файл

@ -150,7 +150,7 @@ describe('Selection', () => {
}
const items: ICustomItem[] = [{ id: 'a' }, { id: 'b' }];
const selection = new Selection<ICustomItem>({
onSelectionChanged: onSelectionChanged,
onSelectionChanged,
getKey: (item: ICustomItem) => item.id,
items,
});