Skip to content

Commit

Permalink
Update helpers to support multi-window use cases (#509)
Browse files Browse the repository at this point in the history
  • Loading branch information
clauderic committed Nov 18, 2021
1 parent f19b8f4 commit 1c6369e
Show file tree
Hide file tree
Showing 32 changed files with 210 additions and 46 deletions.
28 changes: 28 additions & 0 deletions .changeset/multi-window-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@dnd-kit/core": patch
"@dnd-kit/utilities": patch
---

Helpers have been updated to support rendering in foreign `window` contexts (via `ReactDOM.render` or `ReactDOM.createPortal`).

For example, checking if an element is an instance of an `HTMLElement` is normally done like so:

```ts
if (element instanceof HTMLElement)
```

However, when rendering in a different window, this can return false even if the element is indeed an HTMLElement, because this code is equivalent to:

```ts
if (element instanceof window.HTMLElement)
```

And in this case, the `window` of the `element` is different from the main execution context `window`, because we are rendering via a portal into another window.

This can be solved by finding the local window of the element:

```ts
const elementWindow = element.ownerDocument.defaultView;

if (element instanceof elementWindow.HTMLElement)
```
4 changes: 2 additions & 2 deletions packages/core/src/components/DragOverlay/DragOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useContext, useEffect, useRef} from 'react';
import {CSS, useLazyMemo} from '@dnd-kit/utilities';
import {CSS, isKeyboardEvent, useLazyMemo} from '@dnd-kit/utilities';

import {getRelativeTransformOrigin} from '../../utilities';
import {applyModifiers, Modifiers} from '../../modifiers';
Expand All @@ -25,7 +25,7 @@ export interface Props {
}

const defaultTransition: TransitionGetter = (activatorEvent) => {
const isKeyboardActivator = activatorEvent instanceof KeyboardEvent;
const isKeyboardActivator = isKeyboardEvent(activatorEvent);

return isKeyboardActivator ? 'transform 250ms ease' : undefined;
};
Expand Down
16 changes: 10 additions & 6 deletions packages/core/src/hooks/utilities/useRect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useRef} from 'react';
import {useLazyMemo} from '@dnd-kit/utilities';
import {isHTMLElement, useLazyMemo} from '@dnd-kit/utilities';

import {getBoundingClientRect, getViewRect} from '../../utilities';
import type {LayoutRect} from '../../types';
Expand All @@ -10,7 +10,10 @@ export const useViewRect = createUseRectFn(getViewRect);
export const useClientRect = createUseRectFn(getBoundingClientRect);
export const useClientRects = createUseRectsFn(getBoundingClientRect);

export function useRect<T = LayoutRect, U = HTMLElement>(
export function useRect<
T = LayoutRect,
U extends Element | Window = HTMLElement
>(
element: U | null,
getRect: (element: U) => T,
forceRecompute?: boolean
Expand All @@ -28,7 +31,7 @@ export function useRect<T = LayoutRect, U = HTMLElement>(
(!previousValue && element) ||
element !== previousElement.current
) {
if (element instanceof HTMLElement && element.parentNode == null) {
if (isHTMLElement(element) && element.parentNode == null) {
return null;
}

Expand All @@ -41,9 +44,10 @@ export function useRect<T = LayoutRect, U = HTMLElement>(
);
}

export function createUseRectFn<T = LayoutRect, U = HTMLElement>(
getRect: RectFn<T, U>
) {
export function createUseRectFn<
T = LayoutRect,
U extends Element | Window = HTMLElement
>(getRect: RectFn<T, U>) {
return (element: U | null, forceRecompute?: boolean) =>
useRect(element, getRect, forceRecompute);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/sensors/keyboard/KeyboardSensor.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {
add as getAdjustedCoordinates,
subtract as getCoordinatesDelta,
getOwnerDocument,
getWindow,
} from '@dnd-kit/utilities';

import type {Coordinates} from '../../types';
import {
defaultCoordinates,
getBoundingClientRect,
getOwnerDocument,
getWindow,
getScrollPosition,
getScrollElementRect,
} from '../../utilities';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/sensors/mouse/MouseSensor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {MouseEvent} from 'react';
import {getOwnerDocument} from '@dnd-kit/utilities';

import {getOwnerDocument} from '../../utilities';
import type {SensorProps} from '../types';
import {
AbstractPointerSensor,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/sensors/pointer/AbstractPointerSensor.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {
subtract as getCoordinatesDelta,
getEventCoordinates,
getOwnerDocument,
getWindow,
} from '@dnd-kit/utilities';

import {
getEventListenerTarget,
hasExceededDistance,
Listeners,
} from '../utilities';

import {getOwnerDocument, getWindow} from '../../utilities';
import {EventName, preventDefault, stopPropagation} from '../events';
import {KeyboardCode} from '../keyboard';
import type {SensorInstance, SensorProps, SensorOptions} from '../types';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/sensors/pointer/PointerSensor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {PointerEvent} from 'react';
import {getOwnerDocument} from '@dnd-kit/utilities';

import {getOwnerDocument} from '../../utilities';
import type {SensorProps} from '../types';
import {
AbstractPointerSensor,
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/sensors/utilities/getEventListenerTarget.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import {getOwnerDocument} from '../../utilities';
import {getOwnerDocument, getWindow} from '@dnd-kit/utilities';

export function getEventListenerTarget(
element: EventTarget | null
target: EventTarget | null
): EventTarget | Document {
// If the `event.target` element is removed from the document events will still be targeted
// at it, and hence won't always bubble up to the window or document anymore.
// If there is any risk of an element being removed while it is being dragged,
// the best practice is to attach the event listeners directly to the target.
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
return element instanceof EventTarget ? element : getOwnerDocument(element);

const {EventTarget} = getWindow(target);

return target instanceof EventTarget ? target : getOwnerDocument(target);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {getEventCoordinates} from '@dnd-kit/utilities';
import {getEventCoordinates, isKeyboardEvent} from '@dnd-kit/utilities';

export function getRelativeTransformOrigin(
event: MouseEvent | TouchEvent | KeyboardEvent,
rect: ClientRect
) {
if (event instanceof KeyboardEvent) {
if (isKeyboardEvent(event)) {
return '0 0';
}

Expand Down
3 changes: 0 additions & 3 deletions packages/core/src/utilities/document/getOwnerDocument.ts

This file was deleted.

5 changes: 0 additions & 5 deletions packages/core/src/utilities/document/getWindow.ts

This file was deleted.

2 changes: 0 additions & 2 deletions packages/core/src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ export {
isViewRect,
} from './rect';

export {getOwnerDocument, getWindow} from './document';

export {noop} from './other';

export {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/utilities/nodes/getMeasurableNode.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {isHTMLElement} from '@dnd-kit/utilities';

export function getMeasurableNode(
node: HTMLElement | undefined | null
): HTMLElement | null {
Expand All @@ -10,5 +12,5 @@ export function getMeasurableNode(
}
const firstChild = node.children[0];

return firstChild instanceof HTMLElement ? firstChild : node;
return isHTMLElement(firstChild) ? firstChild : node;
}
8 changes: 5 additions & 3 deletions packages/core/src/utilities/rect/getRect.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {isHTMLElement, isWindow} from '@dnd-kit/utilities';

import type {Coordinates, ClientRect, LayoutRect, ViewRect} from '../../types';
import {getScrollableAncestors, getScrollOffsets} from '../scroll';
import {defaultCoordinates} from '../coordinates';
Expand All @@ -7,7 +9,7 @@ function getEdgeOffset(
parent: (Node & ParentNode) | null,
offset = defaultCoordinates
): Coordinates {
if (!node || !(node instanceof HTMLElement)) {
if (!node || !isHTMLElement(node)) {
return offset;
}

Expand Down Expand Up @@ -49,9 +51,9 @@ export function getViewportLayoutRect(element: HTMLElement): LayoutRect {
}

export function getBoundingClientRect(
element: HTMLElement | Window
element: HTMLElement | typeof window
): ClientRect {
if (element instanceof Window) {
if (isWindow(element)) {
const width = window.innerWidth;
const height = window.innerHeight;

Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/utilities/scroll/getScrollCoordinates.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {isWindow} from '@dnd-kit/utilities';

import type {Coordinates} from '../../types';

export function getScrollCoordinates(
element: Element | typeof window
): Coordinates {
if (element instanceof Window) {
if (isWindow(element)) {
return {
x: element.scrollX,
y: element.scrollY,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/utilities/scroll/getScrollableAncestors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {isDocument, isHTMLElement, isSVGElement} from '@dnd-kit/utilities';

import {isFixed} from './isFixed';
import {isScrollable} from './isScrollable';

Expand All @@ -10,7 +12,7 @@ export function getScrollableAncestors(element: Node | null): Element[] {
}

if (
node instanceof Document &&
isDocument(node) &&
node.scrollingElement != null &&
!scrollParents.includes(node.scrollingElement)
) {
Expand All @@ -19,7 +21,7 @@ export function getScrollableAncestors(element: Node | null): Element[] {
return scrollParents;
}

if (!(node instanceof HTMLElement) || node instanceof SVGElement) {
if (!isHTMLElement(node) || isSVGElement(node)) {
return scrollParents;
}

Expand Down
26 changes: 22 additions & 4 deletions packages/core/src/utilities/scroll/getScrollableElement.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
import {canUseDOM} from '@dnd-kit/utilities';
import {
canUseDOM,
isHTMLElement,
isDocument,
getOwnerDocument,
isNode,
isWindow,
} from '@dnd-kit/utilities';

export function getScrollableElement(element: EventTarget | null) {
if (!canUseDOM) {
if (!canUseDOM || !element) {
return null;
}

if (element === document.scrollingElement || element instanceof Document) {
if (isWindow(element)) {
return element;
}

if (!isNode(element)) {
return null;
}

if (
isDocument(element) ||
element === getOwnerDocument(element).scrollingElement
) {
return window;
}

if (element instanceof HTMLElement) {
if (isHTMLElement(element)) {
return element;
}

Expand Down
1 change: 1 addition & 0 deletions packages/utilities/src/event/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export {hasViewportRelativeCoordinates} from './hasViewportRelativeCoordinates';
export {isKeyboardEvent} from './isKeyboardEvent';
export {isTouchEvent} from './isTouchEvent';
13 changes: 13 additions & 0 deletions packages/utilities/src/event/isKeyboardEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {getWindow} from '../execution-context';

export function isKeyboardEvent(
event: Event | undefined | null
): event is KeyboardEvent {
if (!event) {
return false;
}

const {KeyboardEvent} = getWindow(event.target);

return KeyboardEvent && event instanceof KeyboardEvent;
}
14 changes: 12 additions & 2 deletions packages/utilities/src/event/isTouchEvent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
export function isTouchEvent(event: Event): event is TouchEvent {
return window?.TouchEvent && event instanceof TouchEvent;
import {getWindow} from '../execution-context';

export function isTouchEvent(
event: Event | undefined | null
): event is TouchEvent {
if (!event) {
return false;
}

const {TouchEvent} = getWindow(event.target);

return TouchEvent && event instanceof TouchEvent;
}
File renamed without changes.
25 changes: 25 additions & 0 deletions packages/utilities/src/execution-context/getOwnerDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {isWindow, isHTMLElement, isDocument, isNode} from '../type-guards';

export function getOwnerDocument(target: Event['target']): Document {
if (!target) {
return document;
}

if (isWindow(target)) {
return target.document;
}

if (!isNode(target)) {
return document;
}

if (isDocument(target)) {
return target;
}

if (isHTMLElement(target)) {
return target.ownerDocument;
}

return document;
}
18 changes: 18 additions & 0 deletions packages/utilities/src/execution-context/getWindow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {isWindow} from '../type-guards/isWindow';
import {isNode} from '../type-guards/isNode';

export function getWindow(target: Event['target']): typeof window {
if (!target) {
return window;
}

if (isWindow(target)) {
return target;
}

if (!isNode(target)) {
return window;
}

return target.ownerDocument?.defaultView ?? window;
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export {canUseDOM} from './canUseDOM';
export {getOwnerDocument} from './getOwnerDocument';
export {getWindow} from './getWindow';

0 comments on commit 1c6369e

Please sign in to comment.