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:
parent
19853e1577
commit
a98f0fef52
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add MediaUploader component
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
export * from './media-uploader';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
.woocommerce-media-uploader {
|
||||
.woocommerce-media-uploader__label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-bottom: $gap-large;
|
||||
}
|
||||
}
|
|
@ -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 };
|
|
@ -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';
|
||||
|
|
7169
pnpm-lock.yaml
7169
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue