Skip to content

Commit

Permalink
Rebuild DropZone so it accesses ref.current instead of querySelector (#…
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
alex-page and aaronccasanova authored Mar 6, 2023
1 parent 8a4de81 commit 7c174e4
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 127 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-feet-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris': minor
---

Updated DropZone with a signifigant restructure to remove Class child component
195 changes: 69 additions & 126 deletions polaris-react/src/components/DropZone/DropZone.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React, {
createRef,
useState,
useRef,
useCallback,
FunctionComponent,
useMemo,
useEffect,
Component,
} from 'react';
import {UploadMajor, CircleAlertMajor} from '@shopify/polaris-icons';

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

import {FileUpload} from './components';
import {DropZoneContext} from './context';
import {
DropZoneEvent,
fileAccepted,
getDataTransferFiles,
defaultAllowMultiple,
Expand Down Expand Up @@ -141,6 +141,7 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
onDragLeave,
}: DropZoneProps) {
const node = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const dragTargets = useRef<EventTarget[]>([]);

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

const handleDrop = useCallback(
(event: DragEvent) => {
(event: DropZoneEvent) => {
stopEvent(event);
if (disabled) return;

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

(event.target as HTMLInputElement).value = '';
if (!(event.target && 'value' in event.target)) return;

event.target.value = '';
},
[disabled, getValidatedFiles, onDrop, onDropAccepted, onDropRejected],
);

const handleDragEnter = useCallback(
(event: DragEvent) => {
(event: DropZoneEvent) => {
stopEvent(event);
if (disabled) return;

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

const handleDragOver = useCallback(
(event: DragEvent) => {
(event: DropZoneEvent) => {
stopEvent(event);
if (disabled) return;
onDragOver && onDragOver();
Expand All @@ -263,7 +266,7 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
);

const handleDragLeave = useCallback(
(event: DragEvent) => {
(event: DropZoneEvent) => {
event.preventDefault();

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

useEffect(() => {
const dropNode = dropOnPage ? document : node.current;

if (!dropNode) return;

dropNode.addEventListener('drop', handleDrop);
dropNode.addEventListener('dragover', handleDragOver);
dropNode.addEventListener('dragenter', handleDragEnter);
dropNode.addEventListener('dragleave', handleDragLeave);
window.addEventListener('resize', adjustSize);

return () => {
dropNode.removeEventListener('drop', handleDrop);
dropNode.removeEventListener('dragover', handleDragOver);
dropNode.removeEventListener('dragenter', handleDragEnter);
dropNode.removeEventListener('dragleave', handleDragLeave);
window.removeEventListener('resize', adjustSize);
};
}, [
dropOnPage,
handleDrop,
handleDragOver,
handleDragEnter,
handleDragLeave,
adjustSize,
]);
const dropNode = dropOnPage && !isServer ? document : node.current;

useEventListener('drop', handleDrop, dropNode);
useEventListener('dragover', handleDragOver, dropNode);
useEventListener('dragenter', handleDragEnter, dropNode);
useEventListener('dragleave', handleDragLeave, dropNode);
useEventListener('resize', adjustSize, isServer ? null : window);

useComponentDidMount(() => {
adjustSize();
});

const id = useUniqueId('DropZone', idProp);

const typeSuffix = capitalize(type);
const allowMultipleKey = createAllowMultipleKey(allowMultiple);

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

const inputAttributes = {
id,
accept,
disabled,
type: 'file' as const,
multiple: allowMultiple,
onChange: handleDrop,
onFocus: handleFocus,
onBlur: handleBlur,
};

const classes = classNames(
styles.DropZone,
outline && styles.hasOutline,
Expand Down Expand Up @@ -382,35 +356,15 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
[disabled, focused, measuring, size, type, allowMultiple],
);

return (
<DropZoneContext.Provider value={context}>
<Labelled
id={id}
label={labelValue}
action={labelAction}
labelHidden={labelHiddenValue}
>
<div
ref={node}
className={classes}
aria-disabled={disabled}
onClick={handleClick}
onDragStart={stopEvent}
>
{dragOverlay}
{dragErrorOverlay}
<Text variant="bodySm" as="span" visuallyHidden>
<DropZoneInput
{...inputAttributes}
openFileDialog={openFileDialog}
onFileDialogClose={onFileDialogClose}
/>
</Text>
<div className={styles.Container}>{children}</div>
</div>
</Labelled>
</DropZoneContext.Provider>
);
const open = useCallback(() => {
if (!inputRef.current) return;
inputRef.current.click();
}, [inputRef]);

const triggerFileDialog = useCallback(() => {
open();
onFileDialogClose?.();
}, [open, onFileDialogClose]);

function overlayMarkup(
icon: FunctionComponent,
Expand All @@ -431,68 +385,57 @@ export const DropZone: React.FunctionComponent<DropZoneProps> & {
);
}

function open() {
const fileInputNode = node.current && node.current.querySelector(`#${id}`);
fileInputNode &&
fileInputNode instanceof HTMLElement &&
fileInputNode.click();
}

function handleClick(event: React.MouseEvent<HTMLElement>) {
if (disabled) return;

return onClick ? onClick(event) : open();
}

useEffect(() => {
if (openFileDialog) triggerFileDialog();
}, [openFileDialog, triggerFileDialog]);

return (
<DropZoneContext.Provider value={context}>
<Labelled
id={id}
label={labelValue}
action={labelAction}
labelHidden={labelHiddenValue}
>
<div
ref={node}
className={classes}
aria-disabled={disabled}
onClick={handleClick}
onDragStart={stopEvent}
>
{dragOverlay}
{dragErrorOverlay}
<Text variant="bodySm" as="span" visuallyHidden>
<input
id={id}
accept={accept}
disabled={disabled}
multiple={allowMultiple}
onChange={handleDrop}
onFocus={handleFocus}
onBlur={handleBlur}
type="file"
ref={inputRef}
autoComplete="off"
/>
</Text>
<div className={styles.Container}>{children}</div>
</div>
</Labelled>
</DropZoneContext.Provider>
);
};

function stopEvent(event: DragEvent | React.DragEvent) {
function stopEvent(event: DropZoneEvent | React.DragEvent<HTMLDivElement>) {
event.preventDefault();
event.stopPropagation();
}

DropZone.FileUpload = FileUpload;

interface DropZoneInputProps {
id: string;
accept?: string;
disabled: boolean;
type: DropZoneFileType;
multiple: boolean;
openFileDialog?: boolean;
onChange(event: DragEvent | React.ChangeEvent<HTMLInputElement>): void;
onFocus(): void;
onBlur(): void;
onFileDialogClose?(): void;
}

// Due to security reasons, browsers do not allow file inputs to be opened artificially.
// For example `useEffect(() => { ref.click() })`. Oddly enough react class-based components bi-pass this.
class DropZoneInput extends Component<DropZoneInputProps, never> {
private fileInputNode = createRef<HTMLInputElement>();

componentDidMount() {
this.props.openFileDialog && this.triggerFileDialog();
}

componentDidUpdate() {
this.props.openFileDialog && this.triggerFileDialog();
}

render() {
const {openFileDialog, onFileDialogClose, ...inputProps} = this.props;

return (
<input {...inputProps} ref={this.fileInputNode} autoComplete="off" />
);
}

private triggerFileDialog = () => {
this.open();
this.props.onFileDialogClose && this.props.onFileDialogClose();
};

private open = () => {
if (!this.fileInputNode.current) return;
this.fileInputNode.current.click();
};
}
2 changes: 1 addition & 1 deletion polaris-react/src/components/DropZone/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type DropZoneEvent = DragEvent | React.ChangeEvent<HTMLInputElement>;
export type DropZoneEvent = DragEvent | React.ChangeEvent<HTMLInputElement>;

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

Expand Down

0 comments on commit 7c174e4

Please sign in to comment.