Skip to content

Commit 6e4523a

Browse files
alex-pageaaronccasanova
authored andcommitted
Rebuild DropZone so it accesses ref.current instead of querySelector (Shopify#8545)
### WHY are these changes introduced? This is needed when removing the `useUniqId` from v11. ### WHAT is this pull request doing? - [x] Replace addEventListener with `useEventListener` hook - [x] Adjust types so that they work for all the different events - [x] Removes the child class component and moves it's logic into the parent - [x] Removes the `open()` `getElement` code and uses a ref for the input ### How to 🎩 - [ ] Thoroughly test the DropZone storybook examples in Firefox, Chrome and Safari - [ ] In particular make sure the `Upload` button works in `all-components-dropzone--with-custom-file-dialog-trigger` ### 🎩 checklist - [x] Tested on [multiple browsers](https://help.shopify.com/en/manual/shopify-admin/supported-browsers) - [x] Updated the component's `README.md` with documentation changes - [x] [Tophatted documentation](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting%20documentation.md) changes in the style guide --------- Co-authored-by: Aaron Casanova <[email protected]>
1 parent 3c36420 commit 6e4523a

File tree

3 files changed

+75
-127
lines changed

3 files changed

+75
-127
lines changed

.changeset/sharp-feet-bake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/polaris': minor
3+
---
4+
5+
Updated DropZone with a signifigant restructure to remove Class child component

polaris-react/src/components/DropZone/DropZone.tsx

+69-126
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import React, {
2-
createRef,
32
useState,
43
useRef,
54
useCallback,
65
FunctionComponent,
76
useMemo,
87
useEffect,
9-
Component,
108
} from 'react';
119
import {UploadMajor, CircleAlertMajor} from '@shopify/polaris-icons';
1210

@@ -22,10 +20,12 @@ import {useUniqueId} from '../../utilities/unique-id';
2220
import {useComponentDidMount} from '../../utilities/use-component-did-mount';
2321
import {useToggle} from '../../utilities/use-toggle';
2422
import {AlphaStack} from '../AlphaStack';
23+
import {useEventListener} from '../../utilities/use-event-listener';
2524

2625
import {FileUpload} from './components';
2726
import {DropZoneContext} from './context';
2827
import {
28+
DropZoneEvent,
2929
fileAccepted,
3030
getDataTransferFiles,
3131
defaultAllowMultiple,
@@ -141,6 +141,7 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
141141
onDragLeave,
142142
}: DropZoneProps) {
143143
const node = useRef<HTMLDivElement>(null);
144+
const inputRef = useRef<HTMLInputElement>(null);
144145
const dragTargets = useRef<EventTarget[]>([]);
145146

146147
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -208,7 +209,7 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
208209
);
209210

210211
const handleDrop = useCallback(
211-
(event: DragEvent) => {
212+
(event: DropZoneEvent) => {
212213
stopEvent(event);
213214
if (disabled) return;
214215

@@ -225,13 +226,15 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
225226
onDropAccepted && acceptedFiles.length && onDropAccepted(acceptedFiles);
226227
onDropRejected && rejectedFiles.length && onDropRejected(rejectedFiles);
227228

228-
(event.target as HTMLInputElement).value = '';
229+
if (!(event.target && 'value' in event.target)) return;
230+
231+
event.target.value = '';
229232
},
230233
[disabled, getValidatedFiles, onDrop, onDropAccepted, onDropRejected],
231234
);
232235

233236
const handleDragEnter = useCallback(
234-
(event: DragEvent) => {
237+
(event: DropZoneEvent) => {
235238
stopEvent(event);
236239
if (disabled) return;
237240

@@ -254,7 +257,7 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
254257
);
255258

256259
const handleDragOver = useCallback(
257-
(event: DragEvent) => {
260+
(event: DropZoneEvent) => {
258261
stopEvent(event);
259262
if (disabled) return;
260263
onDragOver && onDragOver();
@@ -263,7 +266,7 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
263266
);
264267

265268
const handleDragLeave = useCallback(
266-
(event: DragEvent) => {
269+
(event: DropZoneEvent) => {
267270
event.preventDefault();
268271

269272
if (disabled) return;
@@ -284,38 +287,20 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
284287
[dropOnPage, disabled, onDragLeave],
285288
);
286289

287-
useEffect(() => {
288-
const dropNode = dropOnPage ? document : node.current;
289-
290-
if (!dropNode) return;
291-
292-
dropNode.addEventListener('drop', handleDrop);
293-
dropNode.addEventListener('dragover', handleDragOver);
294-
dropNode.addEventListener('dragenter', handleDragEnter);
295-
dropNode.addEventListener('dragleave', handleDragLeave);
296-
window.addEventListener('resize', adjustSize);
297-
298-
return () => {
299-
dropNode.removeEventListener('drop', handleDrop);
300-
dropNode.removeEventListener('dragover', handleDragOver);
301-
dropNode.removeEventListener('dragenter', handleDragEnter);
302-
dropNode.removeEventListener('dragleave', handleDragLeave);
303-
window.removeEventListener('resize', adjustSize);
304-
};
305-
}, [
306-
dropOnPage,
307-
handleDrop,
308-
handleDragOver,
309-
handleDragEnter,
310-
handleDragLeave,
311-
adjustSize,
312-
]);
290+
const dropNode = dropOnPage && !isServer ? document : node.current;
291+
292+
useEventListener('drop', handleDrop, dropNode);
293+
useEventListener('dragover', handleDragOver, dropNode);
294+
useEventListener('dragenter', handleDragEnter, dropNode);
295+
useEventListener('dragleave', handleDragLeave, dropNode);
296+
useEventListener('resize', adjustSize, isServer ? null : window);
313297

314298
useComponentDidMount(() => {
315299
adjustSize();
316300
});
317301

318302
const id = useUniqueId('DropZone', idProp);
303+
319304
const typeSuffix = capitalize(type);
320305
const allowMultipleKey = createAllowMultipleKey(allowMultiple);
321306

@@ -336,17 +321,6 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
336321
i18n.translate(`Polaris.DropZone.${allowMultipleKey}.label${typeSuffix}`);
337322
const labelHiddenValue = label ? labelHidden : true;
338323

339-
const inputAttributes = {
340-
id,
341-
accept,
342-
disabled,
343-
type: 'file' as const,
344-
multiple: allowMultiple,
345-
onChange: handleDrop,
346-
onFocus: handleFocus,
347-
onBlur: handleBlur,
348-
};
349-
350324
const classes = classNames(
351325
styles.DropZone,
352326
outline && styles.hasOutline,
@@ -382,35 +356,15 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
382356
[disabled, focused, measuring, size, type, allowMultiple],
383357
);
384358

385-
return (
386-
<DropZoneContext.Provider value={context}>
387-
<Labelled
388-
id={id}
389-
label={labelValue}
390-
action={labelAction}
391-
labelHidden={labelHiddenValue}
392-
>
393-
<div
394-
ref={node}
395-
className={classes}
396-
aria-disabled={disabled}
397-
onClick={handleClick}
398-
onDragStart={stopEvent}
399-
>
400-
{dragOverlay}
401-
{dragErrorOverlay}
402-
<Text variant="bodySm" as="span" visuallyHidden>
403-
<DropZoneInput
404-
{...inputAttributes}
405-
openFileDialog={openFileDialog}
406-
onFileDialogClose={onFileDialogClose}
407-
/>
408-
</Text>
409-
<div className={styles.Container}>{children}</div>
410-
</div>
411-
</Labelled>
412-
</DropZoneContext.Provider>
413-
);
359+
const open = useCallback(() => {
360+
if (!inputRef.current) return;
361+
inputRef.current.click();
362+
}, [inputRef]);
363+
364+
const triggerFileDialog = useCallback(() => {
365+
open();
366+
onFileDialogClose?.();
367+
}, [open, onFileDialogClose]);
414368

415369
function overlayMarkup(
416370
icon: FunctionComponent,
@@ -431,68 +385,57 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
431385
);
432386
}
433387

434-
function open() {
435-
const fileInputNode = node.current && node.current.querySelector(`#${id}`);
436-
fileInputNode &&
437-
fileInputNode instanceof HTMLElement &&
438-
fileInputNode.click();
439-
}
440-
441388
function handleClick(event: React.MouseEvent<HTMLElement>) {
442389
if (disabled) return;
443390

444391
return onClick ? onClick(event) : open();
445392
}
393+
394+
useEffect(() => {
395+
if (openFileDialog) triggerFileDialog();
396+
}, [openFileDialog, triggerFileDialog]);
397+
398+
return (
399+
<DropZoneContext.Provider value={context}>
400+
<Labelled
401+
id={id}
402+
label={labelValue}
403+
action={labelAction}
404+
labelHidden={labelHiddenValue}
405+
>
406+
<div
407+
ref={node}
408+
className={classes}
409+
aria-disabled={disabled}
410+
onClick={handleClick}
411+
onDragStart={stopEvent}
412+
>
413+
{dragOverlay}
414+
{dragErrorOverlay}
415+
<Text variant="bodySm" as="span" visuallyHidden>
416+
<input
417+
id={id}
418+
accept={accept}
419+
disabled={disabled}
420+
multiple={allowMultiple}
421+
onChange={handleDrop}
422+
onFocus={handleFocus}
423+
onBlur={handleBlur}
424+
type="file"
425+
ref={inputRef}
426+
autoComplete="off"
427+
/>
428+
</Text>
429+
<div className={styles.Container}>{children}</div>
430+
</div>
431+
</Labelled>
432+
</DropZoneContext.Provider>
433+
);
446434
};
447435

448-
function stopEvent(event: DragEvent | React.DragEvent) {
436+
function stopEvent(event: DropZoneEvent | React.DragEvent<HTMLDivElement>) {
449437
event.preventDefault();
450438
event.stopPropagation();
451439
}
452440

453441
DropZone.FileUpload = FileUpload;
454-
455-
interface DropZoneInputProps {
456-
id: string;
457-
accept?: string;
458-
disabled: boolean;
459-
type: DropZoneFileType;
460-
multiple: boolean;
461-
openFileDialog?: boolean;
462-
onChange(event: DragEvent | React.ChangeEvent<HTMLInputElement>): void;
463-
onFocus(): void;
464-
onBlur(): void;
465-
onFileDialogClose?(): void;
466-
}
467-
468-
// Due to security reasons, browsers do not allow file inputs to be opened artificially.
469-
// For example `useEffect(() => { ref.click() })`. Oddly enough react class-based components bi-pass this.
470-
class DropZoneInput extends Component<DropZoneInputProps, never> {
471-
private fileInputNode = createRef<HTMLInputElement>();
472-
473-
componentDidMount() {
474-
this.props.openFileDialog && this.triggerFileDialog();
475-
}
476-
477-
componentDidUpdate() {
478-
this.props.openFileDialog && this.triggerFileDialog();
479-
}
480-
481-
render() {
482-
const {openFileDialog, onFileDialogClose, ...inputProps} = this.props;
483-
484-
return (
485-
<input {...inputProps} ref={this.fileInputNode} autoComplete="off" />
486-
);
487-
}
488-
489-
private triggerFileDialog = () => {
490-
this.open();
491-
this.props.onFileDialogClose && this.props.onFileDialogClose();
492-
};
493-
494-
private open = () => {
495-
if (!this.fileInputNode.current) return;
496-
this.fileInputNode.current.click();
497-
};
498-
}

polaris-react/src/components/DropZone/utils/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
type DropZoneEvent = DragEvent | React.ChangeEvent<HTMLInputElement>;
1+
export type DropZoneEvent = DragEvent | React.ChangeEvent<HTMLInputElement>;
22

33
const dragEvents = ['dragover', 'dragenter', 'drop'];
44

0 commit comments

Comments
 (0)