diff --git a/UNRELEASED.md b/UNRELEASED.md index 477d7d9d910..d06d29acd6a 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -8,6 +8,7 @@ - Fixed `TextField` to no longer render `aria-invalid="false"` ([#2282](https://github.com/Shopify/polaris-react/pull/2282)) - Fixed `TextField` to only render `min` ,`max` and `step` attributes when explicitly passed ([#2282](https://github.com/Shopify/polaris-react/pull/2282)) +- Removed reference to `document` in `DropZone` ([#2560](https://github.com/Shopify/polaris-react/pull/2560)) ### Documentation diff --git a/src/components/DropZone/DropZone.tsx b/src/components/DropZone/DropZone.tsx index 441062930d4..a5d9c73b94b 100755 --- a/src/components/DropZone/DropZone.tsx +++ b/src/components/DropZone/DropZone.tsx @@ -7,6 +7,10 @@ import React, { useEffect, } from 'react'; import debounce from 'lodash/debounce'; +import { + addEventListener, + removeEventListener, +} from '@shopify/javascript-utilities/events'; import { DragDropMajorMonotone, CircleAlertMajorMonotone, @@ -21,7 +25,7 @@ import {DisplayText} from '../DisplayText'; import {VisuallyHidden} from '../VisuallyHidden'; import {Labelled, Action} from '../Labelled'; import {useI18n} from '../../utilities/i18n'; -import {useEventListener} from '../../utilities/use-event-listener'; +import {isServer} from '../../utilities/target'; import {useUniqueId} from '../../utilities/unique-id'; import {useComponentDidMount} from '../../utilities/use-component-did-mount'; import {useToggle} from '../../utilities/use-toggle'; @@ -180,7 +184,6 @@ export const DropZone: React.FunctionComponent & { const [measuring, setMeasuring] = useState(true); const i18n = useI18n(); - const dropNode = dropOnPage ? document : node; const getValidatedFiles = useCallback( (files: File[] | DataTransferItem[]) => { @@ -282,8 +285,8 @@ export const DropZone: React.FunctionComponent & { if (disabled || (!allowMultiple && numFiles > 0)) return; dragTargets.current = dragTargets.current.filter((el: Node) => { - const compareNode = - dropNode && 'current' in dropNode ? dropNode.current : document; + const compareNode = dropOnPage && !isServer ? document : node.current; + return el !== event.target && compareNode && compareNode.contains(el); }); @@ -294,14 +297,35 @@ export const DropZone: React.FunctionComponent & { onDragLeave && onDragLeave(); }, - [allowMultiple, disabled, dropNode, numFiles, onDragLeave], + [allowMultiple, dropOnPage, disabled, numFiles, onDragLeave], ); - useEventListener(dropNode, 'drop', handleDrop); - useEventListener(dropNode, 'dragover', handleDragOver); - useEventListener(dropNode, 'dragenter', handleDragEnter); - useEventListener(dropNode, 'dragleave', handleDragLeave); - useEventListener(null, 'resize', adjustSize, {}, {defaultToWindow: true}); + useEffect(() => { + const dropNode = dropOnPage ? document : node.current; + + if (!dropNode) return; + + addEventListener(dropNode, 'drop', handleDrop); + addEventListener(dropNode, 'dragover', handleDragOver); + addEventListener(dropNode, 'dragenter', handleDragEnter); + addEventListener(dropNode, 'dragleave', handleDragLeave); + addEventListener(window, 'resize', adjustSize); + + return () => { + removeEventListener(dropNode, 'drop', handleDrop); + removeEventListener(dropNode, 'dragover', handleDragOver); + removeEventListener(dropNode, 'dragenter', handleDragEnter); + removeEventListener(dropNode, 'dragleave', handleDragLeave); + removeEventListener(window, 'resize', adjustSize); + }; + }, [ + dropOnPage, + handleDrop, + handleDragOver, + handleDragEnter, + handleDragLeave, + adjustSize, + ]); useComponentDidMount(() => { adjustSize(); diff --git a/src/utilities/tests/use-event-listener.test.tsx b/src/utilities/tests/use-event-listener.test.tsx deleted file mode 100644 index b3be49505f5..00000000000 --- a/src/utilities/tests/use-event-listener.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, {useRef, useEffect} from 'react'; -import {mount} from 'test-utilities'; -import {useEventListener} from '../use-event-listener'; - -describe('useEventListener', () => { - let addEventListenerSpy: jest.SpyInstance; - let removeEventListenerSpy: jest.SpyInstance; - - beforeEach(() => { - addEventListenerSpy = jest.spyOn(document, 'addEventListener'); - removeEventListenerSpy = jest.spyOn(document, 'removeEventListener'); - }); - - afterEach(() => { - addEventListenerSpy.mockRestore(); - removeEventListenerSpy.mockRestore(); - }); - - it('adds event to react refs', () => { - const spy = jest.fn(); - function Component() { - const div = useRef(null); - - useEventListener(div, 'blur', spy); - - useEffect(() => { - div.current!.dispatchEvent(new Event('blur')); - }); - - return
; - } - - mount(); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('invokes the provided handler when the type of event is triggered', () => { - const spy = jest.fn(); - function Component() { - useEventListener(document, 'reset', spy); - - return null; - } - - mount(); - document.dispatchEvent(new Event('reset')); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('passes type, handler and options to the listener', () => { - const options = {passive: true}; - const noop = () => {}; - function Component() { - useEventListener(document, 'reset', noop, options); - - return null; - } - - mount(); - expect(addEventListenerSpy).toHaveBeenCalledWith('reset', noop, options); - }); - - it('removes the event listener when unmounting', () => { - function Component() { - useEventListener(document, 'reset', () => {}); - - return null; - } - - const component = mount(); - expect(removeEventListenerSpy).toHaveBeenCalledTimes(0); - component.unmount(); - expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); - }); - - it('defaults to window when the option is set', () => { - const spy = jest.fn(); - function Component() { - const div = useRef(null); - - useEventListener(div, 'blur', spy, {}, {defaultToWindow: true}); - - useEffect(() => { - window.dispatchEvent(new Event('blur')); - }); - - return
; - } - - mount(); - expect(spy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/utilities/use-event-listener.ts b/src/utilities/use-event-listener.ts deleted file mode 100644 index a84048e072d..00000000000 --- a/src/utilities/use-event-listener.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {useEffect, RefObject} from 'react'; - -interface Options { - defaultToWindow: boolean; -} - -/** - * Attaches and removes event listeners from the target - * @param target Defines a target for the listener to be placed on. Defaults to window. - * @param type Defines the type of event, i.e blur or focus - * @param handler Defines a callback to be invoked when the event type occurs - * @param listenerOptions Object that specifies event properties - * @param options Object that defines properties used in the hook - * interface Options { - * // Uses window as a back up event target when the current - * // target is falsy - * defaultToWindow: boolean; - * } - * @example - * function Playground() { - * useEventListener(window, 'resize', () => console.log('resize')); - * - * return null; - * } - */ -export function useEventListener( - target: RefObject | Window | Document | null, - type: K, - handler: (ev: WindowEventMap[K]) => any, - listenerOptions?: boolean | AddEventListenerOptions, - options?: Options, -) { - useEffect(() => { - let eventTarget = target && 'current' in target ? target.current : target; - if (!eventTarget && options && options.defaultToWindow) { - eventTarget = window; - } - - if (!eventTarget) return; - - eventTarget.addEventListener(type, handler, listenerOptions); - return () => { - eventTarget && - eventTarget.removeEventListener(type, handler, listenerOptions); - }; - }, [handler, listenerOptions, options, target, type]); -}