Commit 84eec9f3 authored by Tasso Evangelista's avatar Tasso Evangelista Committed by Renato Becker

[IMPROVE] Technical debts of components (#202)

* Update dependencies

* Update Alert component

* Update Avatar component

* Split ButtonGroup from Button

* Add support for icon and badge on Button component

* Embed ChatButton into Screen component

* Update FilesDropTarget component

* Don't export Sound component as default

* Don't export Composer component as default

* Don't export Footer component as default

* Don't export Form component as default

* Update Composer component

* Recover submit event from some forms

* Update TextInput, PasswordInput, and SelectInput components

* Remove FileUploadInput and move its behavior to FilesDropTarget

* Fix memo() helper

* Add white background to Storybook

* Add memedIpsum() helper for stories

* Expose MemoizedComponent helper

* Update Form components

* Update history module initialization

* Remove Storybook's console addon

* Update Messages components

* Fix MessageList scroll positioning

* Update Message attachment handling
parent bb189ca8
import '@storybook/addon-a11y/register'; import '@storybook/addon-a11y/register';
import '@storybook/addon-actions/register'; import '@storybook/addon-actions/register';
import '@storybook/addon-backgrounds/register';
import '@storybook/addon-knobs/register'; import '@storybook/addon-knobs/register';
import '@storybook/addon-options/register'; import '@storybook/addon-options/register';
import '@storybook/addon-viewport/register'; import '@storybook/addon-viewport/register';
import { addDecorator, configure, addParameters } from '@storybook/react'; import { addDecorator, configure, addParameters } from '@storybook/react';
import { withA11y } from '@storybook/addon-a11y'; import { withA11y } from '@storybook/addon-a11y';
import { setConsoleOptions } from '@storybook/addon-console';
import { withOptions } from '@storybook/addon-options'; import { withOptions } from '@storybook/addon-options';
import { create } from '@storybook/theming'; import { create } from '@storybook/theming';
...@@ -16,14 +15,17 @@ addParameters({ ...@@ -16,14 +15,17 @@ addParameters({
hierarchySeparator: /\//, hierarchySeparator: /\//,
hierarchyRootSeparator: /\|/, hierarchyRootSeparator: /\|/,
}, },
backgrounds: [
{
name: 'white',
value: 'white',
default: true,
},
],
}); });
addDecorator(withA11y); addDecorator(withA11y);
setConsoleOptions({
panelExclude: [],
});
function loadStories() { function loadStories() {
require('../src/styles/index.scss'); require('../src/styles/index.scss');
const req = require.context('../src', true, /(stories|story)\.js$/); const req = require.context('../src', true, /(stories|story)\.js$/);
......
...@@ -3,36 +3,47 @@ import { createClassName } from '../helpers'; ...@@ -3,36 +3,47 @@ import { createClassName } from '../helpers';
import CloseIcon from '../../icons/close.svg'; import CloseIcon from '../../icons/close.svg';
import styles from './styles'; import styles from './styles';
export const Alert = ({ children, success, warning, error, color, onDismiss, ...props }) => (
<div className={createClassName(styles, 'alert', { success, warning, error })} style={color && { backgroundColor: color }} {...props}>
<div className={createClassName(styles, 'alert__content')}>
{children}
</div>
<button
onClick={onDismiss}
className={createClassName(styles, 'alert__close')}
aria-label={I18n.t('Dismiss this alert')}
>
<CloseIcon width={20} />
</button>
</div>
);
class AlertContainer extends Component { export class Alert extends Component {
static defaultProps = { static defaultProps = {
timeout: 3000, timeout: 3000,
} }
handleDismiss = () => this.props.onDismiss && this.props.onDismiss(this.props.id) handleDismiss = () => {
const { onDismiss, id } = this.props;
onDismiss && onDismiss(id);
}
componentDidMount() { componentDidMount() {
const { timeout } = this.props; const { timeout } = this.props;
if (Number.isFinite(timeout) && timeout > 0) { if (Number.isFinite(timeout) && timeout > 0) {
setTimeout(() => this.handleDismiss(), timeout); this.dismissTimeout = setTimeout(this.handleDismiss, timeout);
} }
} }
render = ({ children, ...props }) => <Alert {...props} onDismiss={this.handleDismiss}>{children}</Alert> componentWillUnmount() {
} clearTimeout(this.dismissTimeout);
}
export default AlertContainer; render = ({ success, warning, error, color, className, style = {}, children }) => (
<div
role="alert"
className={createClassName(styles, 'alert', { success, warning, error }, [className])}
style={{
...style,
...(color && { backgroundColor: color }),
}}
>
<div className={createClassName(styles, 'alert__content')}>
{children}
</div>
<button
onClick={this.handleDismiss}
className={createClassName(styles, 'alert__close')}
aria-label={I18n.t('Dismiss this alert')}
>
<CloseIcon width={20} />
</button>
</div>
)
}
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { withKnobs, boolean, text, color } from '@storybook/addon-knobs'; import { withKnobs, boolean, color, text } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import { screenCentered, memedIpsum } from '../../helpers.stories';
import { Alert } from '.'; import { Alert } from '.';
const alertText = 'A simple alert';
storiesOf('Components|Alert', module) storiesOf('Components|Alert', module)
.addDecorator(withKnobs) .addDecorator(withKnobs)
.addDecorator(screenCentered)
.add('default', () => (
<Alert
success={boolean('success', false)}
warning={boolean('warning', false)}
error={boolean('error', false)}
onDismiss={action('dismiss')}
>
{text('text', memedIpsum({ count: 3, units: 'words' }))}
</Alert>
))
.add('success', () => ( .add('success', () => (
<Alert <Alert
success={boolean('success', true)} success={boolean('success', true)}
warning={boolean('warning', false)} warning={boolean('warning', false)}
error={boolean('error', false)} error={boolean('error', false)}
onDismiss={action('clicked')} onDismiss={action('dismiss')}
> >
{text('text', alertText)} {text('text', memedIpsum({ count: 3, units: 'words' }))}
</Alert> </Alert>
)) ))
.add('warning', () => ( .add('warning', () => (
...@@ -22,9 +33,9 @@ storiesOf('Components|Alert', module) ...@@ -22,9 +33,9 @@ storiesOf('Components|Alert', module)
success={boolean('success', false)} success={boolean('success', false)}
warning={boolean('warning', true)} warning={boolean('warning', true)}
error={boolean('error', false)} error={boolean('error', false)}
onDismiss={action('clicked')} onDismiss={action('dismiss')}
> >
{text('text', alertText)} {text('text', memedIpsum({ count: 3, units: 'words' }))}
</Alert> </Alert>
)) ))
.add('error', () => ( .add('error', () => (
...@@ -32,9 +43,9 @@ storiesOf('Components|Alert', module) ...@@ -32,9 +43,9 @@ storiesOf('Components|Alert', module)
success={boolean('success', false)} success={boolean('success', false)}
warning={boolean('warning', false)} warning={boolean('warning', false)}
error={boolean('error', true)} error={boolean('error', true)}
onDismiss={action('clicked')} onDismiss={action('dismiss')}
> >
{text('text', alertText)} {text('text', memedIpsum({ count: 3, units: 'words' }))}
</Alert> </Alert>
)) ))
.add('custom color', () => ( .add('custom color', () => (
...@@ -43,9 +54,9 @@ storiesOf('Components|Alert', module) ...@@ -43,9 +54,9 @@ storiesOf('Components|Alert', module)
warning={boolean('warning', false)} warning={boolean('warning', false)}
error={boolean('error', false)} error={boolean('error', false)}
color={color('color', '#175CC4')} color={color('color', '#175CC4')}
onDismiss={action('clicked')} onDismiss={action('dismiss')}
> >
{text('text', alertText)} {text('text', memedIpsum({ count: 3, units: 'words' }))}
</Alert> </Alert>
)) ))
.add('with long text content', () => ( .add('with long text content', () => (
...@@ -53,8 +64,20 @@ storiesOf('Components|Alert', module) ...@@ -53,8 +64,20 @@ storiesOf('Components|Alert', module)
success={boolean('success', false)} success={boolean('success', false)}
warning={boolean('warning', false)} warning={boolean('warning', false)}
error={boolean('error', false)} error={boolean('error', false)}
onDismiss={action('clicked')} onDismiss={action('dismiss')}
> >
{text('text', alertText.repeat(100))} {text('text', memedIpsum({ count: 30, units: 'words' }))}
</Alert> </Alert>
)); ))
.add('without timeout', () => (
<Alert
success={boolean('success', false)}
warning={boolean('warning', false)}
error={boolean('error', false)}
timeout={0}
onDismiss={action('dismiss')}
>
{text('text', memedIpsum({ count: 3, units: 'words' }))}
</Alert>
))
;
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
$alert-color: $color-text-lighter; $alert-color: $color-text-lighter;
$alert-font-family: $font-family; $alert-font-family: $font-family;
$alert-background-color: $bg-color-darker;
$alert-success-background-color: $color-green; $alert-success-background-color: $color-green;
$alert-warning-background-color: $color-yellow; $alert-warning-background-color: $color-yellow;
$alert-error-background-color: $color-red; $alert-error-background-color: $color-red;
...@@ -20,7 +21,7 @@ $alert-error-background-color: $color-red; ...@@ -20,7 +21,7 @@ $alert-error-background-color: $color-red;
overflow: hidden; overflow: hidden;
letter-spacing: 0; letter-spacing: 0;
line-height: 16px; line-height: 16px;
background-color: $alert-success-background-color; background-color: $alert-background-color;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
...@@ -31,18 +32,6 @@ $alert-error-background-color: $color-red; ...@@ -31,18 +32,6 @@ $alert-error-background-color: $color-red;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
&--success {
background-color: $alert-success-background-color;
}
&--warning {
background-color: $alert-warning-background-color;
}
&--error {
background-color: $alert-error-background-color;
}
&__close { &__close {
padding: 0; padding: 0;
background: none; background: none;
...@@ -55,4 +44,15 @@ $alert-error-background-color: $color-red; ...@@ -55,4 +44,15 @@ $alert-error-background-color: $color-red;
@include pressable-button(1px); @include pressable-button(1px);
} }
&--success {
background-color: $alert-success-background-color;
}
&--warning {
background-color: $alert-warning-background-color;
}
&--error {
background-color: $alert-error-background-color;
}
} }
import { Component } from 'preact'; import { Component } from 'preact';
import styles from './styles';
import { createClassName } from '../helpers'; import { createClassName } from '../helpers';
import StatusIndicator from '../StatusIndicator'; import styles from './styles';
export class Avatar extends Component { export class Avatar extends Component {
...@@ -13,17 +12,18 @@ export class Avatar extends Component { ...@@ -13,17 +12,18 @@ export class Avatar extends Component {
this.setState({ errored: true }); this.setState({ errored: true });
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps({ src: nextSrc }) {
if (nextProps.src !== this.props.src) { const { src } = this.props;
if (nextSrc !== src) {
this.setState({ errored: false }); this.setState({ errored: false });
} }
} }
render = ({ small, large, src, description, status, className, ...args }, { errored }) => ( render = ({ small, large, src, description, status, className, style }, { errored }) => (
<div <div
aria-label="User picture" aria-label="User picture"
className={createClassName(styles, 'avatar', { small, large, nobg: src && !errored }, [className])} className={createClassName(styles, 'avatar', { small, large, nobg: src && !errored }, [className])}
{...args} style={style}
> >
{(src && !errored) && ( {(src && !errored) && (
<img <img
...@@ -35,12 +35,8 @@ export class Avatar extends Component { ...@@ -35,12 +35,8 @@ export class Avatar extends Component {
)} )}
{status && ( {status && (
<div className={createClassName(styles, 'avatar__status', { small, large })}> <span className={createClassName(styles, 'avatar__status', { small, large, status })} />
<StatusIndicator status={status} small={small} large={large} bordered />
</div>
)} )}
</div> </div>
) )
} }
export default Avatar;
import centered from '@storybook/addon-centered/react'; import centered from '@storybook/addon-centered/react';
import { withKnobs, boolean, text, select } from '@storybook/addon-knobs'; import { withKnobs, boolean, text, select } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import Avatar from '.'; import { avatarResolver } from '../../helpers.stories';
import bertieBartonAvatar from './bertieBarton.png'; import { Avatar } from '.';
const avatarDescription = 'user description'; const defaultSrc = avatarResolver('guilherme.gazzo');
const avatarStatuses = { const defaultDescription = 'user description';
None: null, const statuses = [null, 'offline', 'away', 'busy', 'online'];
OffLine: 'offline',
Away: 'away',
Busy: 'busy',
OnLine: 'online',
};
storiesOf('Components|Avatar', module) storiesOf('Components|Avatar', module)
.addDecorator(centered) .addDecorator(centered)
.addDecorator(withKnobs) .addDecorator(withKnobs)
.add('large', () => ( .add('default', () => (
<Avatar <Avatar
src={text('src', bertieBartonAvatar)} src={text('src', defaultSrc)}
small={boolean('small', false)} small={boolean('small', false)}
large={boolean('large', true)} large={boolean('large', false)}
description={text('description', avatarDescription)} description={text('description', defaultDescription)}
status={select('status', avatarStatuses, null)} status={select('status', statuses, null)}
/> />
)) ))
.add('medium', () => ( .add('large', () => (
<Avatar <Avatar
src={text('src', bertieBartonAvatar)} src={text('src', defaultSrc)}
small={boolean('small', false)} small={boolean('small', false)}
large={boolean('large', false)} large={boolean('large', true)}
description={text('description', avatarDescription)} description={text('description', defaultDescription)}
status={select('status', avatarStatuses, null)} status={select('status', statuses, null)}
/> />
)) ))
.add('small', () => ( .add('small', () => (
<Avatar <Avatar
src={text('src', bertieBartonAvatar)} src={text('src', defaultSrc)}
small={boolean('small', true)} small={boolean('small', true)}
large={boolean('large', false)} large={boolean('large', false)}
description={text('description', avatarDescription)} description={text('description', defaultDescription)}
status={select('status', avatarStatuses, null)} status={select('status', statuses, null)}
/> />
)) ))
.add('as placeholder', () => ( .add('as placeholder', () => (
...@@ -49,54 +44,51 @@ storiesOf('Components|Avatar', module) ...@@ -49,54 +44,51 @@ storiesOf('Components|Avatar', module)
<Avatar <Avatar
src={text('src', '')} src={text('src', '')}
large large
description={text('description', avatarDescription)} description={text('description', defaultDescription)}
status={select('status', avatarStatuses, null)} status={select('status', statuses, null)}
style={{ margin: '0.5rem' }} style={{ margin: '0.5rem' }}
/> />
<Avatar <Avatar
src={text('src', '')} src={text('src', '')}
description={text('description', avatarDescription)} description={text('description', defaultDescription)}
status={select('status', avatarStatuses, null)} status={select('status', statuses, null)}
style={{ margin: '0.5rem' }} style={{ margin: '0.5rem' }}
/> />
<Avatar <Avatar
src={text('src', '')} src={text('src', '')}
small small
description={text('description', avatarDescription)} description={text('description', defaultDescription)}
status={select('status', avatarStatuses, null)} status={select('status', statuses, null)}
style={{ margin: '0.5rem' }} style={{ margin: '0.5rem' }}
/> />
</div> </div>
)) ))
.add('with status indicator', () => ( .add('with status indicator', () => (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-around' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-around' }}>
<Avatar <Avatar
src={text('src', bertieBartonAvatar)} src={text('src', defaultSrc)}
large description={text('description', defaultDescription)}
description={text('description', avatarDescription)}
status={'offline'} status={'offline'}
style={{ margin: '0.5rem' }} style={{ margin: '0.5rem' }}
/> />
<Avatar <Avatar
src={text('src', bertieBartonAvatar)} src={text('src', defaultSrc)}
large description={text('description', defaultDescription)}
description={text('description', avatarDescription)}
status={'away'} status={'away'}
style={{ margin: '0.5rem' }} style={{ margin: '0.5rem' }}
/> />
<Avatar <Avatar
src={text('src', bertieBartonAvatar)} src={text('src', defaultSrc)}
large description={text('description', defaultDescription)}
description={text('description', avatarDescription)}
status={'busy'} status={'busy'}
style={{ margin: '0.5rem' }} style={{ margin: '0.5rem' }}
/> />
<Avatar <Avatar
src={text('src', bertieBartonAvatar)} src={text('src', defaultSrc)}
large description={text('description', defaultDescription)}
description={text('description', avatarDescription)}
status={'online'} status={'online'}
style={{ margin: '0.5rem' }} style={{ margin: '0.5rem' }}
/> />
</div> </div>
)); ))
;
...@@ -3,12 +3,15 @@ ...@@ -3,12 +3,15 @@
$avatar-size-small: 20px; $avatar-size-small: 20px;
$avatar-status-indicator-size-small: 10px;
$avatar-size-medium: 32px; $avatar-size-medium: 32px;
$avatar-status-indicator-size-medium: 12px;
$avatar-size-large: 46px; $avatar-size-large: 46px;
$avatar-status-indicator-size-large: 14px;
.avatar { .avatar {
flex: 0 0 auto; flex: 0 0 auto;
background-color: $bg-color-grey; background-color: #000000;
background-image: url(./profile.png); background-image: url(./profile.png);
background-position: right; background-position: right;
background-repeat: no-repeat; background-repeat: no-repeat;
...@@ -18,21 +21,7 @@ $avatar-size-large: 46px; ...@@ -18,21 +21,7 @@ $avatar-size-large: 46px;
width: $avatar-size-medium; width: $avatar-size-medium;
height: $avatar-size-medium; height: $avatar-size-medium;
&--nobg { &__image {
background: none;
}
&--small {
width: $avatar-size-small;
height: $avatar-size-small;
}
&--large {
width: $avatar-size-large;
height: $avatar-size-large;
}
& &__image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit