Skip to content

Commit

Permalink
feat(file-uploader): add button for a11y and refactor to hooks (#5964)
Browse files Browse the repository at this point in the history
* test(utilities): add getByLabel test utility

* feat(file-uploader): add button for a11y and refactor to hooks

* test(react): update public API snapshot
  • Loading branch information
joshblack authored May 4, 2020
1 parent 57c2f28 commit 28bd804
Show file tree
Hide file tree
Showing 11 changed files with 392 additions and 299 deletions.
12 changes: 0 additions & 12 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2390,18 +2390,6 @@ Map {
},
},
"FileUploaderButton" => Object {
"defaultProps": Object {
"accept": Array [],
"buttonKind": "primary",
"disableLabelChanges": false,
"disabled": false,
"labelText": "Add file",
"multiple": false,
"onChange": [Function],
"onClick": [Function],
"role": "button",
"tabIndex": 0,
},
"propTypes": Object {
"accept": Object {
"args": Array [
Expand Down
254 changes: 6 additions & 248 deletions packages/react/src/components/FileUploader/FileUploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,260 +5,18 @@
* LICENSE file in the root directory of this source tree.
*/

/* eslint react/no-multi-comp: "off" */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { settings } from 'carbon-components';
import {
Close16,
WarningFilled16,
CheckmarkFilled16,
} from '@carbon/icons-react';
import Loading from '../Loading';
import uid from '../../tools/uniqueId';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Filename from './Filename';
import FileUploaderButton from './FileUploaderButton';
import { ButtonKinds } from '../../prop-types/types';
import { keys, matches } from '../../internal/keyboard';

const { prefix } = settings;

export class FileUploaderButton extends Component {
state = {};

static propTypes = {
/**
* Provide a custom className to be applied to the container node
*/
className: PropTypes.string,

/**
* Specify whether you want to disable any updates to the FileUploaderButton
* label
*/
disableLabelChanges: PropTypes.bool,

/**
* Provide a unique id for the underlying <input> node
*/
id: PropTypes.string,

/**
* Provide the label text to be read by screen readers when interacting with
* this control
*/
labelText: PropTypes.node,

/**
* Specify whether you want the component to list the files that have been
* submitted to be uploaded
*/
listFiles: PropTypes.bool,

/**
* Specify if the component should accept multiple files to upload
*/
multiple: PropTypes.bool,

/**
* Provide a name for the underlying <input> node
*/
name: PropTypes.string,

/**
* Provide an optional `onChange` hook that is called each time the <input>
* value changes
*/
onChange: PropTypes.func,

/**
* Provide an optional `onClick` hook that is called each time the button is
* clicked
*/
onClick: PropTypes.func,

/**
* Provide an accessibility role for the <FileUploaderButton>
*/
role: PropTypes.string,

/**
* Provide a custom tabIndex value for the <FileUploaderButton>
*/
tabIndex: PropTypes.number,

/**
* Specify the type of underlying button
*/
buttonKind: PropTypes.oneOf(ButtonKinds),

/**
* Specify the types of files that this input should be able to receive
*/
accept: PropTypes.arrayOf(PropTypes.string),

/**
* Specify whether file input is disabled
*/
disabled: PropTypes.bool,

/**
* Specify the size of the button, from a list of available sizes.
* For `default` buttons, this prop can remain unspecified.
*/
size: PropTypes.oneOf(['default', 'field', 'small']),
};

static defaultProps = {
tabIndex: 0,
disableLabelChanges: false,
labelText: 'Add file',
buttonKind: 'primary',
multiple: false,
onChange: () => {},
onClick: () => {},
accept: [],
disabled: false,
role: 'button',
};

static getDerivedStateFromProps({ labelText }, state) {
const { prevLabelText } = state;
return prevLabelText === labelText
? null
: {
labelText,
prevLabelText: labelText,
};
}

handleChange = evt => {
const files = evt.target.files;
const length = evt.target.files.length;
if (files && !this.props.disableLabelChanges) {
if (length > 1) {
this.setState({ labelText: `${length} files` });
} else if (length === 1) {
this.setState({ labelText: files[0].name });
}
}
this.props.onChange(evt);
};

render() {
const {
className,
disableLabelChanges, // eslint-disable-line
labelText, // eslint-disable-line
multiple,
role,
tabIndex,
buttonKind,
accept,
name,
disabled,
size,
...other
} = this.props;
const classes = classNames(`${prefix}--btn`, className, {
[`${prefix}--btn--${buttonKind}`]: buttonKind,
[`${prefix}--btn--disabled`]: disabled,
[`${prefix}--btn--field`]: size === 'field',
[`${prefix}--btn--sm`]: size === 'small',
});

this.uid = this.props.id || uid();

return (
<>
<label
tabIndex={disabled ? -1 : tabIndex || 0}
aria-disabled={disabled}
className={classes}
onKeyDown={evt => {
if (evt.which === 13 || evt.which === 32) {
this.input.click();
}
}}
htmlFor={this.uid}
{...other}>
<span role={role}>{this.state.labelText}</span>
</label>
<input
className={`${prefix}--visually-hidden`}
ref={input => (this.input = input)}
id={this.uid}
disabled={disabled}
type="file"
tabIndex="-1"
multiple={multiple}
accept={accept}
name={name}
onChange={this.handleChange}
onClick={evt => {
evt.target.value = null;
}}
/>
</>
);
}
}

export function Filename({ iconDescription, status, invalid, ...other }) {
switch (status) {
case 'uploading':
return (
<Loading description={iconDescription} withOverlay={false} small />
);
case 'edit':
return (
<>
{invalid && <WarningFilled16 className={`${prefix}--file-invalid`} />}
<Close16
className={`${prefix}--file-close`}
aria-label={iconDescription}
{...other}>
{iconDescription && <title>{iconDescription}</title>}
</Close16>
</>
);
case 'complete':
return (
<CheckmarkFilled16
className={`${prefix}--file-complete`}
aria-label={iconDescription}
{...other}>
{iconDescription && <title>{iconDescription}</title>}
</CheckmarkFilled16>
);
default:
return null;
}
}

Filename.propTypes = {
/**
* Provide a description of the SVG icon to denote file upload status
*/
iconDescription: PropTypes.string,

/**
* Status of the file upload
*/
status: PropTypes.oneOf(['edit', 'complete', 'uploading']),

/**
* Provide a custom tabIndex value for the <Filename>
*/
tabIndex: PropTypes.string,
};

Filename.defaultProps = {
iconDescription: 'Uploading file',
status: 'uploading',
tabIndex: '0',
};

export default class FileUploader extends Component {
export default class FileUploader extends React.Component {
static propTypes = {
/**
* Provide a description for the complete/close icon that can be read by screen readers
Expand Down
Loading

0 comments on commit 28bd804

Please sign in to comment.