Merge pull request #73 from IBM/file-uploader

feat(component): add FileUploader
This commit is contained in:
Eric Liu 2019-12-23 21:02:50 -08:00 committed by GitHub
commit 4481c5a97f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 546 additions and 28 deletions

View file

@ -31,6 +31,11 @@ Currently, the following components are supported:
- Copy
- CopyButton
- DataTableSkeleton
- FileUploader
- FileUploaderButton
- FileUploaderItem
- FileUploaderDropContainer
- Filename
- Form
- FormGroup
- FormItem

View file

@ -70,4 +70,4 @@
}</script><style>#root[hidden],
#docs-root[hidden] {
display: none !important;
}</style></head><body><div class="sb-nopreview sb-wrapper"><div class="sb-nopreview_main"><h1 class="sb-nopreview_heading sb-heading">No Preview</h1><p>Sorry, but you either have no stories or none are selected somehow.</p><ul><li>Please check the Storybook config.</li><li>Try reloading the page.</li></ul><p>If the problem persists, check the browser console, or the terminal you've run Storybook from.</p></div></div><div class="sb-errordisplay sb-wrapper"><pre id="error-message" class="sb-heading"></pre><pre class="sb-errordisplay_code"><code id="error-stack"></code></pre></div><div id="root"></div><div id="docs-root"></div><script src="runtime~main.e24a7c3ee0d97007ea8a.bundle.js"></script><script src="vendors~main.e24a7c3ee0d97007ea8a.bundle.js"></script><script src="main.e24a7c3ee0d97007ea8a.bundle.js"></script></body></html>
}</style></head><body><div class="sb-nopreview sb-wrapper"><div class="sb-nopreview_main"><h1 class="sb-nopreview_heading sb-heading">No Preview</h1><p>Sorry, but you either have no stories or none are selected somehow.</p><ul><li>Please check the Storybook config.</li><li>Try reloading the page.</li></ul><p>If the problem persists, check the browser console, or the terminal you've run Storybook from.</p></div></div><div class="sb-errordisplay sb-wrapper"><pre id="error-message" class="sb-heading"></pre><pre class="sb-errordisplay_code"><code id="error-stack"></code></pre></div><div id="root"></div><div id="docs-root"></div><script src="runtime~main.9ba103da9a704f605f83.bundle.js"></script><script src="vendors~main.9ba103da9a704f605f83.bundle.js"></script><script src="main.9ba103da9a704f605f83.bundle.js"></script></body></html>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"main.9ba103da9a704f605f83.bundle.js","sources":["webpack:///main.9ba103da9a704f605f83.bundle.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
{"version":3,"file":"main.e24a7c3ee0d97007ea8a.bundle.js","sources":["webpack:///main.e24a7c3ee0d97007ea8a.bundle.js"],"mappings":"AAAA","sourceRoot":""}

View file

@ -1,2 +1,2 @@
!function(modules){function webpackJsonpCallback(data){for(var moduleId,chunkId,chunkIds=data[0],moreModules=data[1],executeModules=data[2],i=0,resolves=[];i<chunkIds.length;i++)chunkId=chunkIds[i],Object.prototype.hasOwnProperty.call(installedChunks,chunkId)&&installedChunks[chunkId]&&resolves.push(installedChunks[chunkId][0]),installedChunks[chunkId]=0;for(moduleId in moreModules)Object.prototype.hasOwnProperty.call(moreModules,moduleId)&&(modules[moduleId]=moreModules[moduleId]);for(parentJsonpFunction&&parentJsonpFunction(data);resolves.length;)resolves.shift()();return deferredModules.push.apply(deferredModules,executeModules||[]),checkDeferredModules()}function checkDeferredModules(){for(var result,i=0;i<deferredModules.length;i++){for(var deferredModule=deferredModules[i],fulfilled=!0,j=1;j<deferredModule.length;j++){var depId=deferredModule[j];0!==installedChunks[depId]&&(fulfilled=!1)}fulfilled&&(deferredModules.splice(i--,1),result=__webpack_require__(__webpack_require__.s=deferredModule[0]))}return result}var installedModules={},installedChunks={1:0},deferredModules=[];function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={i:moduleId,l:!1,exports:{}};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.l=!0,module.exports}__webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.d=function(exports,name,getter){__webpack_require__.o(exports,name)||Object.defineProperty(exports,name,{enumerable:!0,get:getter})},__webpack_require__.r=function(exports){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(exports,"__esModule",{value:!0})},__webpack_require__.t=function(value,mode){if(1&mode&&(value=__webpack_require__(value)),8&mode)return value;if(4&mode&&"object"==typeof value&&value&&value.__esModule)return value;var ns=Object.create(null);if(__webpack_require__.r(ns),Object.defineProperty(ns,"default",{enumerable:!0,value:value}),2&mode&&"string"!=typeof value)for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns},__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module.default}:function getModuleExports(){return module};return __webpack_require__.d(getter,"a",getter),getter},__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)},__webpack_require__.p="";var jsonpArray=window.webpackJsonp=window.webpackJsonp||[],oldJsonpFunction=jsonpArray.push.bind(jsonpArray);jsonpArray.push=webpackJsonpCallback,jsonpArray=jsonpArray.slice();for(var i=0;i<jsonpArray.length;i++)webpackJsonpCallback(jsonpArray[i]);var parentJsonpFunction=oldJsonpFunction;checkDeferredModules()}([]);
//# sourceMappingURL=runtime~main.e24a7c3ee0d97007ea8a.bundle.js.map
//# sourceMappingURL=runtime~main.9ba103da9a704f605f83.bundle.js.map

View file

@ -1 +1 @@
{"version":3,"file":"runtime~main.e24a7c3ee0d97007ea8a.bundle.js","sources":["webpack:///runtime~main.ce61f8335d8fdea2cda4.bundle.js"],"mappings":"AAAA","sourceRoot":""}
{"version":3,"file":"runtime~main.9ba103da9a704f605f83.bundle.js","sources":["webpack:///runtime~main.ce61f8335d8fdea2cda4.bundle.js"],"mappings":"AAAA","sourceRoot":""}

View file

@ -0,0 +1 @@
{"version":3,"file":"vendors~main.9ba103da9a704f605f83.bundle.js","sources":["webpack:///vendors~main.9ba103da9a704f605f83.bundle.js"],"mappings":"AAAA;;;;;AAyqeA;;;;;AAi9JA;;;;;AAkkEA;;;;;;;;;AAukBA;;;AA8odA;;;;;;;;AAg/BA;;;;;;;;AAqEA;;;;;;;;AAkTA;;;;;;;AAyrDA;;;;;;;AAy7CA;;;;;;;AAufA;;;;;;;AAfA","sourceRoot":""}

View file

@ -1 +0,0 @@
{"version":3,"file":"vendors~main.e24a7c3ee0d97007ea8a.bundle.js","sources":["webpack:///vendors~main.e24a7c3ee0d97007ea8a.bundle.js"],"mappings":"AAAA;;;;;AAwqeA;;;;;AAi9JA;;;;;AAkkEA;;;;;;;;;AAukBA;;;AA8odA;;;;;;;;AAg/BA;;;;;;;;AAqEA;;;;;;;;AAkTA;;;;;;;AAyrDA;;;;;;;AAy7CA;;;;;;;AAufA;;;;;;;AAfA","sourceRoot":""}

View file

@ -23,13 +23,10 @@
let buttonRef = undefined;
$: {
if (ctx && buttonRef) {
ctx.declareRef({ name: 'buttonRef', ref: buttonRef });
}
$: if (ctx && buttonRef) {
ctx.declareRef({ name: 'buttonRef', ref: buttonRef });
}
const _class = cx(
$: _class = cx(
'--btn',
size === 'field' && '--btn--field',
(size === 'small' || small) && '--btn--sm',
@ -47,7 +44,7 @@
hasIconOnly && tooltipAlignment && `--tooltip--align-${tooltipAlignment}`,
className
);
const buttonProps = {
$: buttonProps = {
role: 'button',
type: href && !disabled ? undefined : type,
tabindex,

View file

@ -25,7 +25,7 @@
);
</script>
<table on:click on:mouseover on:mouseenter on:mouseleave {style} class={_class}>
<table on:click on:mouseover on:mouseenter on:mouseleave class={_class} {style}>
<thead>
<tr>
{#each columns as column, i (column)}

View file

@ -0,0 +1,17 @@
<script>
let className = undefined;
export { className as class };
export let style = undefined;
import SkeletonText from '../SkeletonText';
import { ButtonSkeleton } from '../Button';
import { cx } from '../../lib';
const _class = cx('--form-item', className);
</script>
<div on:click on:mouseover on:mouseenter on:mouseleave class={_class} {style}>
<SkeletonText heading width="100px" />
<SkeletonText width="225px" class={cx('--label-description')} />
<ButtonSkeleton />
</div>

View file

@ -0,0 +1,65 @@
<script>
export let story = undefined;
import Layout from '../../internal/ui/Layout.svelte';
import { cx } from '../../lib';
import Button from '../Button';
import FileUploader from './FileUploader.svelte';
import FileUploaderButton from './FileUploaderButton.svelte';
import FileUploaderItem from './FileUploaderItem.svelte';
import FileUploaderDropContainer from './FileUploaderDropContainer.svelte';
import FileUploaderSkeleton from './FileUploader.Skeleton.svelte';
let files = [];
$: disabled = files.length === 0;
</script>
<Layout>
<div>
{#if story === 'button'}
<FileUploaderButton {...$$props} />
{:else if story === 'drop container'}
<FileUploaderDropContainer
{...$$props}
on:add={({ detail }) => {
console.log(detail);
}} />
{:else if story === 'item'}
<FileUploaderItem
{...$$props}
on:delete={({ detail }) => {
console.log(detail);
}}
on:click={() => {
console.log('click');
}} />
{:else if story === 'uploader'}
<div class={cx('--file__container')}>
<FileUploader
{...$$props}
bind:files
on:add={({ detail }) => {
console.log('add', detail);
}}
on:remove={({ detail }) => {
console.log('remove', detail);
}} />
<Button
kind="secondary"
size="small"
style={'margin-top: 1rem'}
{disabled}
on:click={() => {
files = [];
}}>
Clear File{files.length === 1 ? '' : 's'}
</Button>
</div>
{:else if story === 'skeleton'}
<div style="width: 500px">
<FileUploaderSkeleton />
</div>
{/if}
</div>
</Layout>

View file

@ -0,0 +1,98 @@
import { withKnobs, text, select, boolean, number, array } from '@storybook/addon-knobs';
import Component from './FileUploader.Story.svelte';
export default { title: 'FileUploader', decorators: [withKnobs] };
const buttonKinds = {
'Primary (primary)': 'primary',
'Secondary (secondary)': 'secondary',
'Danger (danger)': 'danger',
'Ghost (ghost)': 'ghost',
'Tertiary (tertiary)': 'tertiary'
};
const filenameStatuses = {
'Edit (edit)': 'edit',
'Complete (complete)': 'complete',
'Uploading (uploading)': 'uploading'
};
export const FileUploaderButton = () => ({
Component,
props: {
story: 'button',
kind: select('Button kind (kind)', buttonKinds, 'primary'),
labelText: text('Label text (labelText)', 'Add files'),
name: text('Form item name: (name)', ''),
multiple: boolean('Supports multiple files (multiple)', true),
disabled: boolean('Disabled (disabled)', false),
disableLabelChanges: boolean(
'Prevent the label from being replaced with file selected file (disableLabelChanges)',
false
),
role: text('ARIA role of the button (role)', 'button'),
tabindex: text('Tab index (tabindex)', '0')
}
});
FileUploaderButton.story = { name: 'FileUploaderButton' };
export const FileUploader = () => ({
Component,
props: {
story: 'uploader',
labelTitle: text('The label title (labelTitle)', 'Upload'),
labelDescription: text(
'The label description (labelDescription)',
'only .jpg files at 500mb or less'
),
buttonLabel: text('The button label (buttonLabel)', 'Add files'),
status: select('Status for file name (status)', filenameStatuses, 'edit'),
accept: array('Accepted file extensions (accept)', ['.jpg', '.png'], ','),
name: text('Form item name: (name)', ''),
multiple: boolean('Supports multiple files (multiple)', true),
iconDescription: text('Close button icon description (iconDescription)', 'Clear file')
}
});
FileUploader.story = { name: 'FileUploader' };
export const FileUploaderItem = () => ({
Component,
props: {
story: 'item',
name: text('Filename (name)', 'README.md'),
status: select('Status for file name (status)', filenameStatuses, 'edit'),
iconDescription: text('Close button icon description (iconDescription)', 'Clear file'),
invalid: boolean('Invalid (invalid)', false),
errorSubject: text('Error subject (errorSubject)', 'File size exceeds limit'),
errorBody: text(
'Error body (errorBody)',
'500kb max file size. Select a new file and try again.'
)
}
});
FileUploaderItem.story = { name: 'FileUploaderItem' };
export const FileUploaderDropContainer = () => ({
Component,
props: {
story: 'drop container',
labelText: text('Label text (labelText)', 'Drag and drop files here or click to upload'),
name: text('Form item name (name)', ''),
multiple: boolean('Supports multiple files (multiple)', true),
accept: array(
'Accepted MIME types or file extensions (accept)',
['image/jpeg', 'image/png'],
','
),
disabled: boolean('Disabled (disabled)', false),
role: text('ARIA role of the button (role)', ''),
tabindex: number('Tab index (tabindex)', '0')
}
});
FileUploaderDropContainer.story = { name: 'FileUploaderDropContainer' };
export const Skeleton = () => ({ Component, props: { story: 'skeleton' } });

View file

@ -0,0 +1,77 @@
<script>
let className = undefined;
export { className as class };
export let files = [];
export let name = '';
export let labelDescription = '';
export let labelTitle = '';
export let iconDescription = 'Provide icon description';
export let status = 'uploading';
export let buttonLabel = '';
export let kind = 'primary';
export let multiple = false;
export let accept = [];
export let style = undefined;
import { createEventDispatcher } from 'svelte';
import { cx } from '../../lib';
import Filename from './Filename.svelte';
import FileUploaderButton from './FileUploaderButton.svelte';
const dispatch = createEventDispatcher();
const _class = cx('--form-item', className);
// let files = [];
let prevFiles = [];
$: {
if (files.length > prevFiles.length) {
dispatch('add', files);
} else {
dispatch(
'remove',
prevFiles.filter(_ => !files.includes(_))
);
}
prevFiles = [...files];
}
</script>
<div on:click on:mouseover on:mouseenter on:mouseleave class={_class} {style}>
<strong class={cx('--file--label')}>{labelTitle}</strong>
<p class={cx('--label-description')}>{labelDescription}</p>
<FileUploaderButton
disableLabelChanges
labelText={buttonLabel}
on:change
on:change={({ target }) => {
files = [...target.files].map(({ name }) => name);
}}
{accept}
{name}
{multiple}
{kind} />
<div class={cx('--file-container')}>
{#each files as name, i (name)}
<span class={cx('--file__selected-file')}>
<p class={cx('--file-filename')}>{name}</p>
<span class={cx('--file__state-container')}>
<Filename
on:keydown
on:keydown={({ key }) => {
if (key === ' ' || key === 'Enter') {
files = files.filter((_, index) => index !== i);
}
}}
on:click
on:click={evt => {
files = files.filter((_, index) => index !== i);
}}
{iconDescription}
{status} />
</span>
</span>
{/each}
</div>
</div>

View file

@ -0,0 +1,64 @@
<script>
let className = undefined;
export { className as class };
export let disableLabelChanges = false;
export let id = Math.random();
export let labelText = 'Add file';
export let multiple = false;
export let name = '';
export let role = 'button';
export let tabindex = '0';
export let kind = 'primary';
export let accept = [];
export let disabled = false;
export let style = undefined;
import { cx } from '../../lib';
const _class = cx(
'--btn',
'--btn--sm',
kind && `--btn--${kind}`,
disabled && '--btn--disabled',
className
);
let inputRef = undefined;
</script>
<label
tabindex={disabled ? '-1' : tabindex}
aria-disabled={disabled}
class={_class}
for={id}
on:keydown
on:keydown={({ key }) => {
if (key === ' ' || key === 'Enter') {
inputRef.click();
}
}}
{style}>
<span {role}>{labelText}</span>
</label>
<input
bind:this={inputRef}
type="file"
tabindex="-1"
class={cx('--visually-hidden')}
on:change|stopPropagation
on:change|stopPropagation={({ target }) => {
const files = target.files;
const length = files.length;
if (files && !disableLabelChanges) {
labelText = length > 1 ? `${length} files` : files[0].name;
}
}}
on:click
on:click={event => {
event.target.value = null;
}}
{id}
{disabled}
{multiple}
{accept}
{name} />

View file

@ -0,0 +1,85 @@
<script>
let className = undefined;
export { className as class };
export let name = '';
export let role = 'button';
export let id = Math.random();
export let disabled = false;
export let tabindex = '0';
export let labelText = 'Add file';
export let multiple = false;
export let accept = [];
export let validateFiles = files => files;
export let style = undefined;
import { createEventDispatcher } from 'svelte';
import SkeletonText from '../SkeletonText';
import { ButtonSkeleton } from '../Button';
import { cx } from '../../lib';
const dispatch = createEventDispatcher();
const _labelClass = cx('--file-browse-btn', disabled && '--file-browse-btn--disabled');
let over = false;
let inputRef = undefined;
$: _class = cx('--file__drop-container', over && '--file__drop-container--drag-over', className);
</script>
<div
class={cx('--file')}
on:dragover
on:dragover|preventDefault|stopPropagation={({ dataTransfer }) => {
if (!disabled) {
over = true;
dataTransfer.dropEffect = 'copy';
}
}}
on:dragleave
on:dragleave|preventDefault|stopPropagation={({ dataTransfer }) => {
if (!disabled) {
over = false;
dataTransfer.dropEffect = 'move';
}
}}
on:drop
on:drop|preventDefault|stopPropagation={({ dataTransfer }) => {
if (!disabled) {
over = false;
dispatch('add', validateFiles(dataTransfer.files));
}
}}
{style}>
<label
class={_labelClass}
for={id}
on:keydown
on:keydown={({ key }) => {
if (key === ' ' || key === 'Enter') {
inputRef.click();
}
}}
{tabindex}>
<div class={_class} {role}>
{labelText}
<input
bind:this={inputRef}
type="file"
tabindex="-1"
class={cx('--file-input')}
on:change
on:change={({ target }) => {
dispatch('add', validateFiles(target.files));
}}
on:click
on:click={({ target }) => {
target.value = null;
}}
{id}
{disabled}
{accept}
{name}
{multiple} />
</div>
</label>
</div>

View file

@ -0,0 +1,49 @@
<script>
let className = undefined;
export { className as class };
export let id = Math.random();
export let status = 'uploading';
export let iconDescription = '';
export let name = '';
export let invalid = false;
export let errorSubject = '';
export let errorBody = '';
export let style = undefined;
import { createEventDispatcher } from 'svelte';
import { cx } from '../../lib';
import Filename from './Filename.svelte';
const dispatch = createEventDispatcher();
const _class = cx(
'--file__selected-file',
invalid && '--file__selected-file--invalid',
className
);
</script>
<span on:mouseover on:mouseenter on:mouseleave class={_class} {style}>
<p class={cx('--file-filename')}>{name}</p>
<span class={cx('--file__state-container')}>
<Filename
{iconDescription}
{status}
{invalid}
on:keydown={({ key }) => {
if (key === ' ' || key === 'Enter') {
dispatch('delete', id);
}
}}
on:click={() => {
dispatch('delete', id);
}} />
</span>
{#if invalid && errorSubject}
<div class={cx('--form-requirement')}>
<div class={cx('--form-requirement__title')}>{errorSubject}</div>
{#if errorBody}
<p class={cx('--form-requirement__supplement')}>{errorBody}</p>
{/if}
</div>
{/if}
</span>

View file

@ -0,0 +1,43 @@
<script>
let className = undefined;
export { className as class };
export let status = 'uploading';
export let iconDescription = '';
export let invalid = false;
export let tabindex = '0';
export let style = undefined;
import Close16 from 'carbon-icons-svelte/lib/Close16';
import CheckmarkFilled16 from 'carbon-icons-svelte/lib/CheckmarkFilled16';
import WarningFilled16 from 'carbon-icons-svelte/lib/WarningFilled16';
import { cx } from '../../lib';
import Loading from '../Loading';
</script>
{#if status === 'uploading'}
<Loading description={iconDescription} withOverlay={false} small class={className} {style} />
{/if}
{#if status === 'edit'}
{#if invalid}
<WarningFilled16 class={cx('--file-invalid')} />
{/if}
<!-- TODO: forward keydown event to Svelte icon -->
<Close16
class={cx('--file-close', className)}
aria-label={iconDescription}
title={iconDescription}
on:click
on:keydown
{tabindex}
{style} />
{/if}
{#if status === 'complete'}
<CheckmarkFilled16
class={cx('--file-complete', className)}
aria-label={iconDescription}
title={iconDescription}
{tabindex}
{style} />
{/if}

View file

@ -0,0 +1,7 @@
import FileUploader from './FileUploader.svelte';
export default FileUploader;
export { default as FileUploaderButton } from './FileUploaderButton.svelte';
export { default as FileUploaderItem } from './FileUploaderItem.svelte';
export { default as FileUploaderDropContainer } from './FileUploaderDropContainer.svelte';
export { default as Filename } from './Filename.svelte';

View file

@ -10,7 +10,7 @@ export const Default = () => ({
disabled: boolean('Disabled (disabled in <Tab>)', false),
href: text('The href for tab (href in <Tab>)', '#'),
role: text('ARIA role (role in <Tab>)', 'presentation'),
tabindex: number('Tab index (tabindex in <Tab>)', 0)
tabindex: text('Tab index (tabindex in <Tab>)', '0')
},
tabsProps: {
className: 'some-class',

View file

@ -48,7 +48,7 @@ export const Expandable = () => ({
Component,
props: {
story: 'expandable',
tabIndex: number('Tab index (tabIndex)', 0),
tabindex: text('Tab index (tabindex)', '0'),
expanded: boolean('Expanded (expanded)', false),
tileMaxHeight: number('Max height (tileMaxHeight)', 0),
tileCollapsedIconText: text(

View file

@ -8,6 +8,12 @@ import CopyButton from './components/CopyButton';
import ComposedModal, { ModalHeader, ModalBody, ModalFooter } from './components/ComposedModal';
import CodeSnippet, { CodeSnippetSkeleton } from './components/CodeSnippet';
import DataTableSkeleton from './components/DataTableSkeleton';
import FileUploader, {
FileUploaderButton,
FileUploaderItem,
FileUploaderDropContainer,
Filename
} from './components/FileUploader';
import Form from './components/Form';
import FormGroup from './components/FormGroup';
import FormItem from './components/FormItem';
@ -86,6 +92,11 @@ export {
Copy,
CopyButton,
DataTableSkeleton,
FileUploader,
FileUploaderButton,
FileUploaderItem,
FileUploaderDropContainer,
Filename,
Form,
FormGroup,
FormItem,