Add Media Uploader component (#34181)

* Add media uploader component

* Expand on mocked examples in storybook

* Add styling

* Move MediaUploder out of index

* Add changelog entry

* Handle PR feedback

* Add readme

* Fix missing media utils types

* Rebase and use variant instead of isSecondary for Button

* Fix up lock file

* Fix up lint errors
This commit is contained in:
Joshua T Flowers 2022-08-12 12:58:25 -07:00 committed by GitHub
parent 19853e1577
commit a98f0fef52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 4231 additions and 3257 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add MediaUploader component

View File

@ -46,6 +46,7 @@
"@wordpress/i18n": "^4.3.1",
"@wordpress/icons": "^8.1.0",
"@wordpress/keycodes": "^3.3.1",
"@wordpress/media-utils": "^4.6.0",
"@wordpress/url": "^3.4.1",
"@wordpress/viewport": "^4.1.2",
"classnames": "^2.3.1",
@ -95,9 +96,10 @@
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^13.5.0",
"@types/wordpress__components": "^19.10.1",
"@types/wordpress__viewport": "^2.5.4",
"@types/react": "^17.0.0",
"@types/wordpress__components": "^19.10.1",
"@types/wordpress__media-utils": "^3.0.0",
"@types/wordpress__viewport": "^2.5.4",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",
"@wordpress/browserslist-config": "^4.1.1",

View File

@ -19,6 +19,7 @@ export { H, Section } from './section';
export { default as ImageUpload } from './image-upload';
export { default as Link } from './link';
export { default as List } from './list';
export { MediaUploader } from './media-uploader';
export { default as MenuItem } from './ellipsis-menu/menu-item';
export { default as MenuTitle } from './ellipsis-menu/menu-title';
export { default as OrderStatus } from './order-status';

View File

@ -0,0 +1,29 @@
MediaUploader
===
This component adds an upload button and a dropzone for uploading media to a site.
## Usage
By default this will use the functionality from `@wordpress/media-utils` which provides access and uploads to the WP media library and uses the WP media modal.
```jsx
<MediaUploader
label={ 'Click the button below to upload' }
onSelect={ ( file ) => setImages( [ ...images, file ] ) }
onUpload={ ( files ) => setImages( [ ...images, ...files ] ) }
/>
```
### Props
Name | Type | Default | Description
--- | --- | --- | ---
`allowedMediaTypes` | String[] | `[ 'image ]` | Allowed media types
`buttonText` | String | `Choose images` | Text to use for button
`hasDropZone` | Boolean | `true` | Whether or not to allow the dropzone
`label` | String | `Drag images here or click to upload` | String to use for the text shown inside the component
`MediaUploadComponent` | JSX.Element | `MediaModal` | The component to use for the media uploader
`onError` | Function | `() => null` | Callback function to run when an error occurs
`onUpload` | Function | `() => null` | Callback function to run when an upload occurs aftering dragging and dropping files
`onUpload` | Function | `() => null` | Callback function to run when selecting media from the opened media modal

View File

@ -0,0 +1 @@
export * from './media-uploader';

View File

@ -0,0 +1,78 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, DropZone } from '@wordpress/components';
import { createElement } from 'react';
import {
MediaItem,
MediaUpload,
uploadMedia as wpUploadMedia,
UploadMediaOptions,
UploadMediaErrorCode,
} from '@wordpress/media-utils';
const DEFAULT_ALLOWED_MEDIA_TYPES = [ 'image' ];
type MediaUploaderProps = {
allowedMediaTypes?: string[];
buttonText?: string;
hasDropZone?: boolean;
icon?: JSX.Element;
label?: string;
maxUploadFileSize?: number;
MediaUploadComponent?: < T extends boolean = false >(
props: MediaUpload.Props< T >
) => JSX.Element;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSelect?: ( value: { id: number } & { [ k: string ]: any } ) => void;
onError?: ( error: {
code: UploadMediaErrorCode;
message: string;
file: File;
} ) => void;
onUpload?: ( files: MediaItem[] ) => void;
uploadMedia?: ( options: UploadMediaOptions ) => Promise< void >;
};
export const MediaUploader = ( {
allowedMediaTypes = DEFAULT_ALLOWED_MEDIA_TYPES,
buttonText = __( 'Choose images', 'woocommerce' ),
hasDropZone = true,
label = __( 'Drag images here or click to upload', 'woocommerce' ),
maxUploadFileSize = 10000000,
MediaUploadComponent = MediaUpload,
onError = () => null,
onUpload = () => null,
onSelect = () => null,
uploadMedia = wpUploadMedia,
}: MediaUploaderProps ) => {
return (
<div className="woocommerce-media-uploader">
<div className="woocommerce-media-uploader__label">{ label }</div>
<MediaUploadComponent
onSelect={ onSelect }
allowedTypes={ allowedMediaTypes }
render={ ( { open } ) => (
<Button variant="secondary" onClick={ open }>
{ buttonText }
</Button>
) }
/>
{ hasDropZone && (
<DropZone
onFilesDrop={ ( files ) =>
uploadMedia( {
filesList: files,
onError,
onFileChange: onUpload,
maxUploadFileSize,
} )
}
/>
) }
</div>
);
};

View File

@ -0,0 +1,178 @@
/**
* External dependencies
*/
import React, { createElement } from 'react';
import { Card, CardBody, Modal, Notice } from '@wordpress/components';
import { MediaItem } from '@wordpress/media-utils';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { MediaUploader } from '../';
import { File } from '../types';
declare let Blob: {
prototype: Blob;
new (): Blob;
};
const MockMediaUpload = ( { onSelect, render } ) => {
const [ isOpen, setOpen ] = useState( false );
return (
<>
{ render( {
open: () => setOpen( true ),
} ) }
{ isOpen && (
<Modal
title="Media Modal"
onRequestClose={ () => setOpen( false ) }
>
<p>
Use the default built-in{ ' ' }
<code>MediaUploadComponent</code> prop to render the WP
Media Modal.
</p>
{ Array( ...Array( 3 ) ).map( ( n, i ) => {
return (
<button
key={ i }
onClick={ () => {
onSelect( {
alt: 'Random',
url: `https://picsum.photos/200?i=${ i }`,
} );
setOpen( false );
} }
style={ {
marginRight: '16px',
} }
>
<img
src={ `https://picsum.photos/200?i=${ i }` }
alt="Random"
style={ {
maxWidth: '100px',
} }
/>
</button>
);
} ) }
</Modal>
) }
</>
);
};
const ImageGallery = ( { images }: { images: File[] } ) => {
return (
<div style={ { marginBottom: '16px' } }>
{ images.map( ( image, index ) => {
return (
<img
key={ index }
alt={ image.alt }
src={ image.url }
style={ {
maxWidth: '100px',
marginRight: '16px',
} }
/>
);
} ) }
</div>
);
};
const readImage = ( file: Blob ) => {
return new Promise< MediaItem >( ( resolve ) => {
const fileReader = new FileReader();
fileReader.onload = function ( event ) {
const image = {
alt: 'Temporary image',
url: event?.target?.result,
} as MediaItem;
resolve( image );
};
fileReader.readAsDataURL( file );
} );
};
const mockUploadMedia = async ( { filesList, onFileChange } ) => {
const images = await Promise.all(
filesList.map( ( file ) => readImage( file ) )
);
onFileChange( images );
};
export const Basic: React.FC = () => {
const [ images, setImages ] = useState< File[] >( [] );
return (
<Card size="large">
<CardBody>
<ImageGallery images={ images } />
<MediaUploader
MediaUploadComponent={ MockMediaUpload }
onSelect={ ( file ) => setImages( [ ...images, file ] ) }
onError={ () => null }
onUpload={ ( files ) =>
setImages( [ ...images, ...files ] )
}
uploadMedia={ mockUploadMedia }
/>
</CardBody>
</Card>
);
};
export const DisabledDropZone: React.FC = () => {
const [ images, setImages ] = useState< File[] >( [] );
return (
<Card size="large">
<CardBody>
<ImageGallery images={ images } />
<MediaUploader
hasDropZone={ false }
label={ 'Click the button below to upload' }
MediaUploadComponent={ MockMediaUpload }
onSelect={ ( file ) => setImages( [ ...images, file ] ) }
onError={ () => null }
uploadMedia={ mockUploadMedia }
/>
</CardBody>
</Card>
);
};
export const MaxUploadFileSize: React.FC = () => {
const [ error, setError ] = useState< string | null >( null );
return (
<Card size="large">
<CardBody>
{ error && (
<Notice isDismissible={ false } status={ 'error' }>
{ error }
</Notice>
) }
<MediaUploader
maxUploadFileSize={ 1000 }
MediaUploadComponent={ MockMediaUpload }
onSelect={ () => null }
onError={ ( e ) => setError( e.message ) }
onUpload={ () => null }
/>
</CardBody>
</Card>
);
};
export default {
title: 'WooCommerce Admin/components/MediaUploader',
component: Basic,
};

View File

@ -0,0 +1,8 @@
.woocommerce-media-uploader {
.woocommerce-media-uploader__label {
font-weight: 600;
font-size: 14px;
line-height: 20px;
margin-bottom: $gap-large;
}
}

View File

@ -0,0 +1,13 @@
/**
* External dependencies
*/
import { UploadMediaErrorCode } from '@wordpress/media-utils';
export type ErrorType = {
code: UploadMediaErrorCode;
message: string;
file: File;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type File = { id: number } & { [ k: string ]: any };

View File

@ -21,6 +21,7 @@
@import 'flag/style.scss';
@import 'image-upload/style.scss';
@import 'list/style.scss';
@import 'media-uploader/style.scss';
@import 'order-status/style.scss';
@import 'pagination/style.scss';
@import 'pill/style.scss';

File diff suppressed because it is too large Load Diff