Commit 38ce52a7 authored by Tasso Evangelista's avatar Tasso Evangelista Committed by Guilherme Gazzo

[FIX] Forms (#64)

parent 538718a4
import '@storybook/addon-actions/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-options/register';
import '@storybook/addon-storysource/register';
import '@storybook/addon-viewport/register';
import { configure } from '@storybook/react';
import { setConsoleOptions } from '@storybook/addon-console';
import { setOptions } from '@storybook/addon-options';
setOptions({
name: 'RocketChat Livechat',
url: 'https://github.com/RocketChat/Rocket.Chat.Livechat',
......@@ -8,8 +10,12 @@ setOptions({
hierarchyRootSeparator: /\|/,
});
setConsoleOptions({
panelExclude: [],
});
function loadStories() {
require('../stories/index.js');
require('../src/styles/index.scss');
const req = require.context('../src', true, /(stories|story)\.js$/);
req.keys().forEach(filename => req(filename));
}
......
......@@ -50,12 +50,6 @@ module.exports = (baseConfig, env, defaultConfig) => {
]
}
defaultConfig.module.rules.push({
test: /(stories|story)\.js$/,
loaders: [require.resolve('@storybook/addon-storysource/loader')],
enforce: 'pre',
});
// defaultConfig.module.rules[4].test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/;
......
......@@ -12,9 +12,7 @@ const Button = ({ children, disabled, outline, danger, stack, small, loading, ..
loading,
})}
>
<div className={styles.button__inner}>
{children}
</div>
{children}
</button>
);
......
@import '~styles/colors';
@import '~styles/variables';
@mixin state($color) {
border-color: $color;
background: $color;
&.button--outline {
color: $color;
$button-group-margin: ($default-gap / 4);
&.button--loading::after {
border-color: $color rgba($color, 0.5) rgba($color, 0.5) rgba($color, 0.5);
}
}
}
$button-border-width: $default-border;
$button-border-radius: $default-border-radius;
$button-padding: (0.75 * $default-gap - $default-border) (1.5 * $default-gap - $default-border);
$button-small-padding: (0.25 * $default-gap - $default-border / 2) (1.5 * $default-gap - $default-border);
$button-color: $color-text-lighter;
$button-background-color: $color-blue;
$button-danger-background-color: $color-dark-red;
$button-font-family: $font-family;
$button-font-size: 0.875rem;
$button-font-weight: 500;
$button-line-height: 1.25rem;
$button-disabled-opacity: $disabled-opacity;
$button-loading-border-width: (2 * $default-border);
$button-loading-gap: ($default-gap / 2);
$button-loading-size: $button-line-height;
$button-loading-color: #ffffff;
.group {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
margin: -.25rem;
margin: -$button-group-margin;
.button {
margin: .25rem;
margin: $button-group-margin;
flex-grow: 1;
}
}
$padding: 20px;
.button {
box-sizing: border-box;
position: relative;
@mixin state($color) {
border-color: $color;
background: $color;
transition: color $default-time-animation, background $default-time-animation, trasform $default-time-animation;
&.button--outline {
color: $color;
border: $default-border solid;
&.button--loading::after {
border-color: $color rgba($color, 0.5) rgba($color, 0.5) rgba($color, 0.5);
}
}
}
height: $form-item-height;
border-radius: $default-border-radius;
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
@include state($color-blue);
box-sizing: border-box;
border: $button-border-width solid;
border-radius: $button-border-radius;
padding: $button-padding;
color: white;
font-size: 14px;
color: $button-color;
outline: none;
font-family: $button-font-family;
font-size: $button-font-size;
font-weight: $button-font-weight;
line-height: $button-line-height;
white-space: nowrap;
text-decoration: none;
padding: 0.25rem $padding/2;
cursor: pointer;
transition: color $default-time-animation,
background $default-time-animation,
trasform $default-time-animation;
&__inner {
padding: 0 $padding/2;
display: inline-block;
vertical-align: middle;
width: 100%;
transition: all $default-time-animation;
@include state($button-background-color);
&:focus {
box-shadow: 0 0 5px rgba(#000000, 0.5);
}
&:active {
opacity: .9;
transform: translateY(2px);
}
// COLORS
&--danger {
@include state($color-dark-red);
@include state($button-danger-background-color);
}
// STYLE
&--outline {
background: none;
}
......@@ -75,57 +101,38 @@ $padding: 20px;
}
&--small {
height: $form-item-height-small;
padding: 0.25rem 10px;
&.button--loading::after {
width: 5px;
height: 5px;
border-width: 6px;
}
padding: $button-small-padding;
}
// STATES
&--disabled {
opacity: .5;
opacity: $button-disabled-opacity;
cursor: not-allowed;
}
&--loading {
& .button__inner {
padding-right: 24px;
}
&.button--stack .button__inner {
padding-left: 24px;
}
&::after {
width: 10px;
height: 10px;
border-width: 8px;
border-style: solid;
border-radius: 50%;
animation: loader-rotate 1s linear infinite;
display: inline-block;
vertical-align: middle;
content: "";
display: inline-flex;
box-sizing: border-box;
position: relative;
right: 24px;
left: $button-loading-gap;
border-color: white rgba(white, 0.5) rgba(white, 0.5) rgba(white, 0.5);
border: $button-loading-border-width solid;
border-radius: 50%;
width: $button-line-height;
height: $button-line-height;
border-color: $button-loading-color
rgba($button-loading-color, 0.5)
rgba($button-loading-color, 0.5)
rgba($button-loading-color, 0.5);
animation: button-loading-rotation 1s linear infinite;
}
}
&:active:not(:disabled) {
opacity: .9;
transform: translateY(2px);
}
}
@keyframes loader-rotate {
@keyframes button-loading-rotation {
0% {
transform: rotate(0);
}
......
......@@ -3,7 +3,8 @@ import styles from './styles';
import Logo from './logo.svg';
import { createClassName } from '../helpers';
export const Main = ({ children, ...props }) => (
export const Footer = ({ children, ...props }) => (
<footer className={createClassName(styles, 'footer')} {...props}>
{children}
</footer>
......@@ -26,3 +27,11 @@ export const Options = (props) => (
Options
</button>
);
Footer.Content = Content;
Footer.PoweredBy = PoweredBy;
Footer.Options = Options;
export { Footer as Main };
export default Footer;
import { h, Component } from 'preact';
import styles from './style';
import { asyncForEach, createClassName } from '../helpers';
import {
TextInput,
PasswordInput,
SelectInput,
} from './inputs';
export class Form extends Component {
handleSubmit = (event) => {
event.preventDefault();
}
render() {
const { children, ...props } = this.props;
return (
<form noValidate onSubmit={this.handleSubmit} className={createClassName(styles, 'form')} {...props}>
{children}
</form>
);
}
}
export const Item = ({ children, inline, ...args }) => (
<div className={createClassName(styles, 'form__item', { inline })} {...args}>
{children}
</div>
);
export const Label = ({ children, error, ...args }) => (
<label className={createClassName(styles, 'form__label', { error })} {...args}>
{children}
</label>
);
export const Description = ({ children, error, ...args }) => (
<small className={createClassName(styles, 'form__description', { error })} {...args}>
{children}
</small>
);
export const Error = (props) => <Description error {...props} />;
const builtinValidations = {
notNull: (value) => {
if (!value) {
throw 'Field required';
}
},
email: (value) => {
const re = /^\S+@\S+\.\S+/;
if (value && !re.test(String(value).toLowerCase())) {
throw 'Invalid email';
}
},
};
export class InputField extends Component {
handleChange = (onChange) => (event) => {
onChange && onChange(event);
this.validateAndChangeState();
}
validateAndChangeState = async() => {
try {
await this.validate(true);
this.setState({ error: false });
} catch (error) {
this.setState({ error });
}
}
validate = async(rethrowErrors = false) => {
const { base: { value } } = this.el;
const validations = this.props.validations || [];
if (this.props.required && ![].includes(builtinValidations.notNull)) {
validations.push(builtinValidations.notNull);
}
try {
await asyncForEach(validations, (validation) => {
if (typeof validation === 'string') {
return builtinValidations[validation](value);
}
validation(value);
});
return true;
} catch (error) {
if (rethrowErrors) {
throw error;
}
return false;
}
}
get value() {
const { base: { value } } = this.el;
return value;
}
state = {
error: false,
}
renderLabel = ({ label, required, error }) => (
label && <Label error={!!error}>{label}{required && ' *'}</Label>
)
renderInput = ({ type, error, ...args }) => h({
text: TextInput,
password: PasswordInput,
select: SelectInput,
}[type], { error: !!error, ...args });
renderDescription = ({ description, error }) => (
(error || description) && <Description error={!!error}>{error || description}</Description>
)
render() {
const { type = 'text', name, onChange, inline, ...args } = this.props;
const { error } = this.state;
return (
<Item inline={inline}>
{this.renderLabel({ error, ...args })}
{this.renderInput({
type,
name,
ref: (el) => this.el = el,
error,
onChange: this.handleChange(onChange),
...args,
})}
{this.renderDescription({ error, ...args })}
</Item>
);
}
}
export {
TextInput as Input,
TextInput,
PasswordInput,
SelectInput,
};
Form.Item = Item;
Form.Label = Label;
Form.Description = Description;
Form.TextInput = TextInput;
Form.PasswordInput = PasswordInput;
Form.SelectInput = SelectInput;
export const Validations = {
nonEmpty: (value) => (!value ? 'Field required' : undefined),
email: (value) => (!/^\S+@\S+\.\S+/.test(String(value).toLowerCase()) ? 'Invalid email' : null),
};
export default Form;
import { h, Component } from 'preact';
import Arrow from '../../icons/arrow.svg';
import styles from './style';
import { createClassName } from '../helpers';
export const TextInput = ({
disabled,
error,
small,
multiple = 1,
...args
}) => (
multiple < 2 ? (
<input
type="text"
disabled={disabled}
className={[
createClassName(styles, 'form__input'),
createClassName(styles, 'form__input-text', { disabled, error, small }),
].join(' ')}
{...args}
/>
) : (
<textarea
rows={multiple}
disabled={disabled}
className={[
createClassName(styles, 'form__input'),
createClassName(styles, 'form__input-text', { disabled, error, small }),
].join(' ')}
{...args}
/>
)
);
export const PasswordInput = ({
disabled,
error,
small,
...args
}) => (
<input
type="password"
disabled={disabled}
className={[
createClassName(styles, 'form__input'),
createClassName(styles, 'form__input-password', { disabled, error, small }),
].join(' ')}
{...args}
/>
);
export class SelectInput extends Component {
state = {
value: this.props.value,
}
handleChange = (event) => {
const { onChange } = this.props;
onChange && onChange(event);
if (event.defaultPrevented) {
return;
}
this.setState({ value: event.target.value });
}
componentWillReceiveProps(nextProps) {
if (nextProps.hasOwnProperty('value') && nextProps.value !== this.props.value) {
this.setState({ value: nextProps.value });
}
}
render() {
const {
// eslint-disable-next-line no-unused-vars
value,
// eslint-disable-next-line no-unused-vars
onChange,
options = [],
placeholder,
disabled,
error,
small,
...args
} = this.props;
return (
<div
className={[
createClassName(styles, 'form__input'),
createClassName(styles, 'form__input-select'),
].join(' ')}
>
<select
value={this.state.value}
disabled={disabled}
onChange={this.handleChange}
className={createClassName(styles, 'form__input-select__select', {
disabled,
error,
small,
placeholder: !this.state.value,
})}
{...args}
>
<option value="" disabled hidden>{placeholder}</option>
{Array.from(options).map(({ value, label }) => (
<option value={value} className={createClassName(styles, 'form__input-select__option')}>{label}</option>
))}
</select>
<Arrow className={createClassName(styles, 'form__input-select__arrow')} />
</div>
);
}
}
This diff is collapsed.
@import '~styles/colors';
@import '~styles/variables';
$form-item-margin-bottom: $default-gap;
$form-item-inline-margin-left: $default-gap;
$form-label-margin: ($default-gap / 3) 0;
$form-label-color: $color-text-dark;
$form-label-error-color: $color-red;
$form-label-font-size: 0.75rem;
$form-label-font-weight: 600;
$form-label-line-height: 1rem;
$form-description-margin: ($default-gap / 2) 0 0;
$form-description-color: $color-text-grey;
$form-description-error-color: $color-red;
$form-description-font-size: 0.75rem;
$form-description-font-weight: 500;
$form-description-line-height: 1rem;
$form-input-border-width: $default-border;
$form-input-border-radius: $default-border-radius;
$form-input-padding: (0.75 * $default-gap - $default-border);
$form-input-small-padding: (0.25 * $default-gap - $default-border / 2) (0.75 * $default-gap - $default-border);
$form-input-color: $color-text-dark;
$form-input-placeholder-color: $color-text-light;
$form-input-background-color: $bg-color-white;
$form-input-border-color: $bg-color-grey;
$form-input-focus-border-color: $color-text-dark;
$form-input-hover-border-color: $color-text-light;
$form-input-disabled-background-color: $bg-color-grey;
$form-input-disabled-color: $color-text-light;
$form-input-error-color: $color-red;
$form-input-error-border-color: $color-red;
$form-input-font-family: $font-family;
$form-input-font-size: 0.875rem;
$form-input-font-weight: 500;
$form-input-line-height: 1.25rem;
$form-input-disabled-opacity: $disabled-opacity;
$form-input-select-arrow-size: $form-input-padding;
$form-input-select-arrow-padding: $form-input-padding;
$form-input-select-arrow-color: $color-text-light;
.form__item {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: $form-item-margin-bottom;
&--inline {
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
.form__label {
flex: 1 25%;
}
.form__label + .form__input {
flex: 3 75%;
}
.form__description {
flex: 0 75%;
}
}
}
.form__label {
margin: $form-label-margin;
color: $form-label-color;
font-size: $form-label-font-size;
font-weight: $form-label-font-weight;
text-align: left;
letter-spacing: 0;
line-height: $form-label-line-height;
white-space: nowrap;
text-overflow: ellipsis;
transition: color $default-time-animation;
&--error {
color: $form-label-error-color;
}
}
.form__description {
margin: $form-description-margin;
color: $form-description-color;
font-size: $form-description-font-size;
font-weight: $form-description-font-weight;
line-height: $form-description-line-height;
transition: color $default-time-animation;
&--error {
color: $form-description-error-color;
}
}
@mixin form__input-box {
border: $form-input-border-width solid $form-input-border-color;
border-radius: $form-input-border-radius;
padding: $form-input-padding;
color: $form-input-color;
background-color: $form-input-background-color;
outline: none;
font-family: $form-input-font-family;
font-size: $form-input-font-size;