mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Change prettier rules
This commit is contained in:
@@ -4,50 +4,40 @@ import { storiesOf } from '@storybook/react';
|
||||
import Button from './Button';
|
||||
|
||||
storiesOf('UI/Form', module).add('Button', () => (
|
||||
<>
|
||||
<div>
|
||||
<Button label="Green Button" />{' '}
|
||||
<Button label="Blue Button" color="blue" />{' '}
|
||||
<Button label="DarkBlue Button" color="darkBlue" />{' '}
|
||||
<Button label="Violet Button" color="violet" />{' '}
|
||||
<Button label="LightViolet Button" color="lightViolet" />{' '}
|
||||
<Button label="Orange Button" color="orange" />{' '}
|
||||
<Button label="Red Button" color="red" />{' '}
|
||||
<Button label="Black Button" color="black" />{' '}
|
||||
<Button label="White Button" color="white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Disabled buttons</h2>
|
||||
<Button disabled label="Green Button" />{' '}
|
||||
<Button disabled label="Blue Button" color="blue" />{' '}
|
||||
<Button disabled label="DarkBlue Button" color="darkBlue" />{' '}
|
||||
<Button disabled label="Violet Button" color="violet" />{' '}
|
||||
<Button disabled label="LightViolet Button" color="lightViolet" />{' '}
|
||||
<Button disabled label="Orange Button" color="orange" />{' '}
|
||||
<Button disabled label="Red Button" color="red" />{' '}
|
||||
<Button disabled label="Black Button" color="black" />{' '}
|
||||
<Button disabled label="White Button" color="white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Button sizes</h2>
|
||||
<Button label="Default button" /> <Button label="Small button" small />{' '}
|
||||
<br />
|
||||
<br />
|
||||
<Button label="Block button" block />
|
||||
<br />
|
||||
<Button label="Small block button" small block />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Loading button</h2>
|
||||
<Button loading label="Green Button" />{' '}
|
||||
<Button loading label="Blue Button" color="blue" />{' '}
|
||||
<Button loading label="DarkBlue Button" color="darkBlue" />{' '}
|
||||
<Button loading label="Violet Button" color="violet" />{' '}
|
||||
<Button loading label="LightViolet Button" color="lightViolet" />{' '}
|
||||
<Button loading label="Orange Button" color="orange" />{' '}
|
||||
<Button loading label="Red Button" color="red" />{' '}
|
||||
<Button loading label="Black Button" color="black" />{' '}
|
||||
<Button loading label="White Button" color="white" />
|
||||
</div>
|
||||
</>
|
||||
<>
|
||||
<div>
|
||||
<Button label="Green Button" /> <Button label="Blue Button" color="blue" />{' '}
|
||||
<Button label="DarkBlue Button" color="darkBlue" /> <Button label="Violet Button" color="violet" />{' '}
|
||||
<Button label="LightViolet Button" color="lightViolet" /> <Button label="Orange Button" color="orange" />{' '}
|
||||
<Button label="Red Button" color="red" /> <Button label="Black Button" color="black" />{' '}
|
||||
<Button label="White Button" color="white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Disabled buttons</h2>
|
||||
<Button disabled label="Green Button" /> <Button disabled label="Blue Button" color="blue" />{' '}
|
||||
<Button disabled label="DarkBlue Button" color="darkBlue" />{' '}
|
||||
<Button disabled label="Violet Button" color="violet" />{' '}
|
||||
<Button disabled label="LightViolet Button" color="lightViolet" />{' '}
|
||||
<Button disabled label="Orange Button" color="orange" /> <Button disabled label="Red Button" color="red" />{' '}
|
||||
<Button disabled label="Black Button" color="black" />{' '}
|
||||
<Button disabled label="White Button" color="white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Button sizes</h2>
|
||||
<Button label="Default button" /> <Button label="Small button" small /> <br />
|
||||
<br />
|
||||
<Button label="Block button" block />
|
||||
<br />
|
||||
<Button label="Small block button" small block />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Loading button</h2>
|
||||
<Button loading label="Green Button" /> <Button loading label="Blue Button" color="blue" />{' '}
|
||||
<Button loading label="DarkBlue Button" color="darkBlue" />{' '}
|
||||
<Button loading label="Violet Button" color="violet" />{' '}
|
||||
<Button loading label="LightViolet Button" color="lightViolet" />{' '}
|
||||
<Button loading label="Orange Button" color="orange" /> <Button loading label="Red Button" color="red" />{' '}
|
||||
<Button loading label="Black Button" color="black" /> <Button loading label="White Button" color="white" />
|
||||
</div>
|
||||
</>
|
||||
));
|
||||
|
@@ -8,50 +8,48 @@ import buttons from '../buttons.scss';
|
||||
import FormComponent from './FormComponent';
|
||||
|
||||
export default class Button extends FormComponent<
|
||||
{
|
||||
// TODO: drop MessageDescriptor support. It should be React.ReactNode only
|
||||
label: string | MessageDescriptor | React.ReactElement;
|
||||
block?: boolean;
|
||||
small?: boolean;
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
color?: Color;
|
||||
disabled?: boolean;
|
||||
component?: string | React.ComponentType<any>;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
{
|
||||
// TODO: drop MessageDescriptor support. It should be React.ReactNode only
|
||||
label: string | MessageDescriptor | React.ReactElement;
|
||||
block?: boolean;
|
||||
small?: boolean;
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
color?: Color;
|
||||
disabled?: boolean;
|
||||
component?: string | React.ComponentType<any>;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
> {
|
||||
render() {
|
||||
const {
|
||||
color = COLOR_GREEN,
|
||||
block,
|
||||
small,
|
||||
disabled,
|
||||
className,
|
||||
loading,
|
||||
label,
|
||||
component: ComponentProp = 'button',
|
||||
...restProps
|
||||
} = this.props;
|
||||
render() {
|
||||
const {
|
||||
color = COLOR_GREEN,
|
||||
block,
|
||||
small,
|
||||
disabled,
|
||||
className,
|
||||
loading,
|
||||
label,
|
||||
component: ComponentProp = 'button',
|
||||
...restProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ComponentProp
|
||||
className={clsx(
|
||||
buttons[color],
|
||||
{
|
||||
[buttons.loading]: loading,
|
||||
[buttons.block]: block,
|
||||
[buttons.smallButton]: small,
|
||||
[buttons.disabled]: disabled,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{typeof label === 'object' && React.isValidElement(label)
|
||||
? label
|
||||
: this.formatMessage(label)}
|
||||
</ComponentProp>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ComponentProp
|
||||
className={clsx(
|
||||
buttons[color],
|
||||
{
|
||||
[buttons.loading]: loading,
|
||||
[buttons.block]: block,
|
||||
[buttons.smallButton]: small,
|
||||
[buttons.disabled]: disabled,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{typeof label === 'object' && React.isValidElement(label) ? label : this.formatMessage(label)}
|
||||
</ComponentProp>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -10,73 +10,70 @@ import styles from './form.scss';
|
||||
import FormInputComponent from './FormInputComponent';
|
||||
|
||||
export default class Captcha extends FormInputComponent<
|
||||
{
|
||||
delay: number;
|
||||
skin: Skin;
|
||||
},
|
||||
{
|
||||
code: string;
|
||||
}
|
||||
{
|
||||
delay: number;
|
||||
skin: Skin;
|
||||
},
|
||||
{
|
||||
code: string;
|
||||
}
|
||||
> {
|
||||
elRef = React.createRef<HTMLDivElement>();
|
||||
captchaId: CaptchaID;
|
||||
elRef = React.createRef<HTMLDivElement>();
|
||||
captchaId: CaptchaID;
|
||||
|
||||
static defaultProps = {
|
||||
skin: 'dark',
|
||||
delay: 0,
|
||||
};
|
||||
static defaultProps = {
|
||||
skin: 'dark',
|
||||
delay: 0,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
const { current: el } = this.elRef;
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
el &&
|
||||
captcha
|
||||
.render(el, {
|
||||
skin: this.props.skin,
|
||||
onSetCode: this.setCode,
|
||||
})
|
||||
.then((captchaId) => {
|
||||
this.captchaId = captchaId;
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Failed rendering captcha', {
|
||||
error,
|
||||
});
|
||||
});
|
||||
}, this.props.delay);
|
||||
}
|
||||
el &&
|
||||
captcha
|
||||
.render(el, {
|
||||
skin: this.props.skin,
|
||||
onSetCode: this.setCode,
|
||||
})
|
||||
.then((captchaId) => {
|
||||
this.captchaId = captchaId;
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Failed rendering captcha', {
|
||||
error,
|
||||
});
|
||||
});
|
||||
}, this.props.delay);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { skin } = this.props;
|
||||
render() {
|
||||
const { skin } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.captchaContainer}>
|
||||
<div className={styles.captchaLoader}>
|
||||
<ComponentLoader />
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.captchaContainer}>
|
||||
<div className={styles.captchaLoader}>
|
||||
<ComponentLoader />
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={this.elRef}
|
||||
className={clsx(styles.captcha, styles[`${skin}Captcha`])}
|
||||
/>
|
||||
<div ref={this.elRef} className={clsx(styles.captcha, styles[`${skin}Captcha`])} />
|
||||
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
reset() {
|
||||
captcha.reset(this.captchaId);
|
||||
}
|
||||
reset() {
|
||||
captcha.reset(this.captchaId);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.state && this.state.code;
|
||||
}
|
||||
getValue() {
|
||||
return this.state && this.state.code;
|
||||
}
|
||||
|
||||
onFormInvalid() {
|
||||
this.reset();
|
||||
}
|
||||
onFormInvalid() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
setCode = (code: string) => this.setState({ code });
|
||||
setCode = (code: string) => this.setState({ code });
|
||||
}
|
||||
|
@@ -8,58 +8,48 @@ import styles from './form.scss';
|
||||
import FormInputComponent from './FormInputComponent';
|
||||
|
||||
export default class Checkbox extends FormInputComponent<
|
||||
React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
color: Color;
|
||||
skin: Skin;
|
||||
label: string | MessageDescriptor | React.ReactElement;
|
||||
}
|
||||
React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
color: Color;
|
||||
skin: Skin;
|
||||
label: string | MessageDescriptor | React.ReactElement;
|
||||
}
|
||||
> {
|
||||
static defaultProps = {
|
||||
color: COLOR_GREEN,
|
||||
skin: SKIN_DARK,
|
||||
};
|
||||
static defaultProps = {
|
||||
color: COLOR_GREEN,
|
||||
skin: SKIN_DARK,
|
||||
};
|
||||
|
||||
elRef = React.createRef<HTMLInputElement>();
|
||||
elRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
render() {
|
||||
const { color, skin } = this.props;
|
||||
let { label } = this.props;
|
||||
render() {
|
||||
const { color, skin } = this.props;
|
||||
let { label } = this.props;
|
||||
|
||||
label = React.isValidElement(label) ? label : this.formatMessage(label);
|
||||
label = React.isValidElement(label) ? label : this.formatMessage(label);
|
||||
|
||||
const props = omit(this.props, ['color', 'skin', 'label']);
|
||||
const props = omit(this.props, ['color', 'skin', 'label']);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
styles[`${color}MarkableRow`],
|
||||
styles[`${skin}MarkableRow`],
|
||||
)}
|
||||
>
|
||||
<label className={styles.markableContainer}>
|
||||
<input
|
||||
ref={this.elRef}
|
||||
className={styles.markableInput}
|
||||
type="checkbox"
|
||||
{...props}
|
||||
/>
|
||||
<div className={styles.checkbox} />
|
||||
{label}
|
||||
</label>
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={clsx(styles[`${color}MarkableRow`], styles[`${skin}MarkableRow`])}>
|
||||
<label className={styles.markableContainer}>
|
||||
<input ref={this.elRef} className={styles.markableInput} type="checkbox" {...props} />
|
||||
<div className={styles.checkbox} />
|
||||
{label}
|
||||
</label>
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
const { current: el } = this.elRef;
|
||||
getValue() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
return el && el.checked ? 1 : 0;
|
||||
}
|
||||
return el && el.checked ? 1 : 0;
|
||||
}
|
||||
|
||||
focus() {
|
||||
const { current: el } = this.elRef;
|
||||
focus() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
el && el.focus();
|
||||
}
|
||||
el && el.focus();
|
||||
}
|
||||
}
|
||||
|
@@ -11,149 +11,143 @@ type I18nString = string | MessageDescriptor;
|
||||
type ItemLabel = I18nString | React.ReactElement;
|
||||
|
||||
interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label: I18nString;
|
||||
items: { [value: string]: ItemLabel };
|
||||
block?: boolean;
|
||||
color: Color;
|
||||
label: I18nString;
|
||||
items: { [value: string]: ItemLabel };
|
||||
block?: boolean;
|
||||
color: Color;
|
||||
}
|
||||
|
||||
interface OptionItem {
|
||||
label: ItemLabel;
|
||||
value: string;
|
||||
label: ItemLabel;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isActive: boolean;
|
||||
activeItem: OptionItem | null;
|
||||
isActive: boolean;
|
||||
activeItem: OptionItem | null;
|
||||
}
|
||||
|
||||
export default class Dropdown extends FormInputComponent<Props, State> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
color: COLOR_GREEN,
|
||||
};
|
||||
|
||||
state: State = {
|
||||
isActive: false,
|
||||
activeItem: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// listen to capturing phase to ensure, that our event handler will be
|
||||
// called before all other
|
||||
// @ts-ignore
|
||||
document.addEventListener('click', this.onBodyClick, true);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// @ts-ignore
|
||||
document.removeEventListener('click', this.onBodyClick);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { color, block, items, ...restProps } = this.props;
|
||||
const { isActive } = this.state;
|
||||
|
||||
delete restProps.label;
|
||||
|
||||
const activeItem = this.getActiveItem();
|
||||
const label = React.isValidElement(activeItem.label)
|
||||
? activeItem.label
|
||||
: this.formatMessage(activeItem.label);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={clsx(styles[color], {
|
||||
[styles.block]: block,
|
||||
[styles.opened]: isActive,
|
||||
})}
|
||||
data-e2e-select-name={restProps.name}
|
||||
{...restProps}
|
||||
onClick={this.onToggle}
|
||||
>
|
||||
<span className={styles.label} data-testid="select-label">
|
||||
{label}
|
||||
</span>
|
||||
<span className={styles.toggleIcon} />
|
||||
|
||||
<div className={styles.menu}>
|
||||
{Object.entries(items).map(([value, label]) => (
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
key={value}
|
||||
onClick={this.onSelectItem({ value, label })}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.setState({
|
||||
isActive: !this.state.isActive,
|
||||
});
|
||||
}
|
||||
|
||||
onSelectItem(item: OptionItem): MouseEventHandler<HTMLDivElement> {
|
||||
return (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
activeItem: item,
|
||||
});
|
||||
static defaultProps: Partial<Props> = {
|
||||
color: COLOR_GREEN,
|
||||
};
|
||||
}
|
||||
|
||||
getActiveItem(): OptionItem {
|
||||
const { items } = this.props;
|
||||
let { activeItem } = this.state;
|
||||
state: State = {
|
||||
isActive: false,
|
||||
activeItem: null,
|
||||
};
|
||||
|
||||
if (!activeItem) {
|
||||
activeItem = {
|
||||
label: this.props.label,
|
||||
value: '',
|
||||
};
|
||||
|
||||
if (!activeItem.label) {
|
||||
const [[value, label]] = Object.entries(items);
|
||||
|
||||
activeItem = {
|
||||
label,
|
||||
value,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
// listen to capturing phase to ensure, that our event handler will be
|
||||
// called before all other
|
||||
// @ts-ignore
|
||||
document.addEventListener('click', this.onBodyClick, true);
|
||||
}
|
||||
|
||||
return activeItem;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
// @ts-ignore
|
||||
document.removeEventListener('click', this.onBodyClick);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.getActiveItem()?.value;
|
||||
}
|
||||
render() {
|
||||
const { color, block, items, ...restProps } = this.props;
|
||||
const { isActive } = this.state;
|
||||
|
||||
onToggle = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
delete restProps.label;
|
||||
|
||||
this.toggle();
|
||||
};
|
||||
const activeItem = this.getActiveItem();
|
||||
const label = React.isValidElement(activeItem.label) ? activeItem.label : this.formatMessage(activeItem.label);
|
||||
|
||||
onBodyClick: MouseEventHandler = (event) => {
|
||||
if (this.state.isActive) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const el = ReactDOM.findDOMNode(this)!;
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={clsx(styles[color], {
|
||||
[styles.block]: block,
|
||||
[styles.opened]: isActive,
|
||||
})}
|
||||
data-e2e-select-name={restProps.name}
|
||||
{...restProps}
|
||||
onClick={this.onToggle}
|
||||
>
|
||||
<span className={styles.label} data-testid="select-label">
|
||||
{label}
|
||||
</span>
|
||||
<span className={styles.toggleIcon} />
|
||||
|
||||
if (!el.contains(event.target as HTMLElement) && el !== event.target) {
|
||||
<div className={styles.menu}>
|
||||
{Object.entries(items).map(([value, label]) => (
|
||||
<div className={styles.menuItem} key={value} onClick={this.onSelectItem({ value, label })}>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.setState({
|
||||
isActive: !this.state.isActive,
|
||||
});
|
||||
}
|
||||
|
||||
onSelectItem(item: OptionItem): MouseEventHandler<HTMLDivElement> {
|
||||
return (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
activeItem: item,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
getActiveItem(): OptionItem {
|
||||
const { items } = this.props;
|
||||
let { activeItem } = this.state;
|
||||
|
||||
if (!activeItem) {
|
||||
activeItem = {
|
||||
label: this.props.label,
|
||||
value: '',
|
||||
};
|
||||
|
||||
if (!activeItem.label) {
|
||||
const [[value, label]] = Object.entries(items);
|
||||
|
||||
activeItem = {
|
||||
label,
|
||||
value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return activeItem;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.getActiveItem()?.value;
|
||||
}
|
||||
|
||||
onToggle = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
onBodyClick: MouseEventHandler = (event) => {
|
||||
if (this.state.isActive) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const el = ReactDOM.findDOMNode(this)!;
|
||||
|
||||
if (!el.contains(event.target as HTMLElement) && el !== event.target) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -7,194 +7,189 @@ import FormModel from './FormModel';
|
||||
import styles from './form.scss';
|
||||
|
||||
interface BaseProps {
|
||||
id: string;
|
||||
isLoading: boolean;
|
||||
onInvalid: (errors: Record<string, string>) => void;
|
||||
children: React.ReactNode;
|
||||
id: string;
|
||||
isLoading: boolean;
|
||||
onInvalid: (errors: Record<string, string>) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface PropsWithoutForm extends BaseProps {
|
||||
onSubmit: (form: FormData) => Promise<void> | void;
|
||||
onSubmit: (form: FormData) => Promise<void> | void;
|
||||
}
|
||||
|
||||
interface PropsWithForm extends BaseProps {
|
||||
form: FormModel;
|
||||
onSubmit: (form: FormModel) => Promise<void> | void;
|
||||
form: FormModel;
|
||||
onSubmit: (form: FormModel) => Promise<void> | void;
|
||||
}
|
||||
|
||||
type Props = PropsWithoutForm | PropsWithForm;
|
||||
|
||||
function hasForm(props: Props): props is PropsWithForm {
|
||||
return 'form' in props;
|
||||
return 'form' in props;
|
||||
}
|
||||
|
||||
interface State {
|
||||
id: string; // just to track value for derived updates
|
||||
isTouched: boolean;
|
||||
isLoading: boolean;
|
||||
id: string; // just to track value for derived updates
|
||||
isTouched: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
type InputElement = HTMLInputElement | HTMLTextAreaElement;
|
||||
|
||||
export default class Form extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
id: 'default',
|
||||
isLoading: false,
|
||||
onSubmit() {},
|
||||
onInvalid() {},
|
||||
};
|
||||
static defaultProps = {
|
||||
id: 'default',
|
||||
isLoading: false,
|
||||
onSubmit() {},
|
||||
onInvalid() {},
|
||||
};
|
||||
|
||||
state: State = {
|
||||
id: this.props.id,
|
||||
isTouched: false,
|
||||
isLoading: this.props.isLoading || false,
|
||||
};
|
||||
state: State = {
|
||||
id: this.props.id,
|
||||
isTouched: false,
|
||||
isLoading: this.props.isLoading || false,
|
||||
};
|
||||
|
||||
formEl: HTMLFormElement | null;
|
||||
formEl: HTMLFormElement | null;
|
||||
|
||||
mounted = false;
|
||||
mounted = false;
|
||||
|
||||
componentDidMount() {
|
||||
if (hasForm(this.props)) {
|
||||
this.props.form.addLoadingListener(this.onLoading);
|
||||
componentDidMount() {
|
||||
if (hasForm(this.props)) {
|
||||
this.props.form.addLoadingListener(this.onLoading);
|
||||
}
|
||||
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
this.mounted = true;
|
||||
}
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
const patch: Partial<State> = {};
|
||||
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
const patch: Partial<State> = {};
|
||||
if (typeof props.isLoading !== 'undefined' && props.isLoading !== state.isLoading) {
|
||||
patch.isLoading = props.isLoading;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof props.isLoading !== 'undefined' &&
|
||||
props.isLoading !== state.isLoading
|
||||
) {
|
||||
patch.isLoading = props.isLoading;
|
||||
if (props.id !== state.id) {
|
||||
patch.id = props.id;
|
||||
patch.isTouched = true;
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
if (props.id !== state.id) {
|
||||
patch.id = props.id;
|
||||
patch.isTouched = true;
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const nextForm = hasForm(this.props) ? this.props.form : undefined;
|
||||
const prevForm = hasForm(prevProps) ? prevProps.form : undefined;
|
||||
|
||||
if (nextForm !== prevForm) {
|
||||
if (prevForm) {
|
||||
prevForm.removeLoadingListener(this.onLoading);
|
||||
}
|
||||
|
||||
if (nextForm) {
|
||||
nextForm.addLoadingListener(this.onLoading);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
if (hasForm(this.props)) {
|
||||
this.props.form.removeLoadingListener(this.onLoading);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const nextForm = hasForm(this.props) ? this.props.form : undefined;
|
||||
const prevForm = hasForm(prevProps) ? prevProps.form : undefined;
|
||||
|
||||
if (nextForm !== prevForm) {
|
||||
if (prevForm) {
|
||||
prevForm.removeLoadingListener(this.onLoading);
|
||||
}
|
||||
|
||||
if (nextForm) {
|
||||
nextForm.addLoadingListener(this.onLoading);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (hasForm(this.props)) {
|
||||
this.props.form.removeLoadingListener(this.onLoading);
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
render() {
|
||||
const { isLoading } = this.state;
|
||||
|
||||
render() {
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return (
|
||||
<form
|
||||
className={clsx(styles.form, {
|
||||
[styles.isFormLoading]: isLoading,
|
||||
[styles.formTouched]: this.state.isTouched,
|
||||
})}
|
||||
onSubmit={this.onFormSubmit}
|
||||
ref={(el: HTMLFormElement | null) => (this.formEl = el)}
|
||||
noValidate
|
||||
>
|
||||
{this.props.children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (!this.state.isTouched) {
|
||||
this.setState({
|
||||
isTouched: true,
|
||||
});
|
||||
return (
|
||||
<form
|
||||
className={clsx(styles.form, {
|
||||
[styles.isFormLoading]: isLoading,
|
||||
[styles.formTouched]: this.state.isTouched,
|
||||
})}
|
||||
onSubmit={this.onFormSubmit}
|
||||
ref={(el: HTMLFormElement | null) => (this.formEl = el)}
|
||||
noValidate
|
||||
>
|
||||
{this.props.children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const form = this.formEl;
|
||||
submit() {
|
||||
if (!this.state.isTouched) {
|
||||
this.setState({
|
||||
isTouched: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
const form = this.formEl;
|
||||
|
||||
if (form.checkValidity()) {
|
||||
let result: Promise<void> | void;
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasForm(this.props)) {
|
||||
result = this.props.onSubmit(this.props.form);
|
||||
} else {
|
||||
result = this.props.onSubmit(new FormData(form));
|
||||
}
|
||||
if (form.checkValidity()) {
|
||||
let result: Promise<void> | void;
|
||||
|
||||
if (result && result.then) {
|
||||
this.setState({ isLoading: true });
|
||||
if (hasForm(this.props)) {
|
||||
result = this.props.onSubmit(this.props.form);
|
||||
} else {
|
||||
result = this.props.onSubmit(new FormData(form));
|
||||
}
|
||||
|
||||
if (result && result.then) {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
result
|
||||
.catch((errors: Record<string, string>) => {
|
||||
this.setErrors(errors);
|
||||
})
|
||||
.finally(() => this.mounted && this.setState({ isLoading: false }));
|
||||
}
|
||||
} else {
|
||||
const invalidEls: NodeListOf<InputElement> = form.querySelectorAll(':invalid');
|
||||
const errors: Record<string, string> = {};
|
||||
invalidEls[0].focus(); // focus on first error
|
||||
|
||||
Array.from(invalidEls).reduce((acc, el) => {
|
||||
if (!el.name) {
|
||||
logger.warn('Found an element without name', { el });
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
let errorMessage = el.validationMessage;
|
||||
|
||||
if (el.validity.valueMissing) {
|
||||
errorMessage = `error.${el.name}_required`;
|
||||
} else if (el.validity.typeMismatch) {
|
||||
errorMessage = `error.${el.name}_invalid`;
|
||||
}
|
||||
|
||||
acc[el.name] = errorMessage;
|
||||
|
||||
return acc;
|
||||
}, errors);
|
||||
|
||||
result
|
||||
.catch((errors: Record<string, string>) => {
|
||||
this.setErrors(errors);
|
||||
})
|
||||
.finally(() => this.mounted && this.setState({ isLoading: false }));
|
||||
}
|
||||
} else {
|
||||
const invalidEls: NodeListOf<InputElement> = form.querySelectorAll(
|
||||
':invalid',
|
||||
);
|
||||
const errors: Record<string, string> = {};
|
||||
invalidEls[0].focus(); // focus on first error
|
||||
|
||||
Array.from(invalidEls).reduce((acc, el) => {
|
||||
if (!el.name) {
|
||||
logger.warn('Found an element without name', { el });
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
let errorMessage = el.validationMessage;
|
||||
|
||||
if (el.validity.valueMissing) {
|
||||
errorMessage = `error.${el.name}_required`;
|
||||
} else if (el.validity.typeMismatch) {
|
||||
errorMessage = `error.${el.name}_invalid`;
|
||||
}
|
||||
|
||||
acc[el.name] = errorMessage;
|
||||
|
||||
return acc;
|
||||
}, errors);
|
||||
|
||||
this.setErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(errors: { [key: string]: string }) {
|
||||
if (hasForm(this.props)) {
|
||||
this.props.form.setErrors(errors);
|
||||
}
|
||||
|
||||
this.props.onInvalid(errors);
|
||||
}
|
||||
setErrors(errors: { [key: string]: string }) {
|
||||
if (hasForm(this.props)) {
|
||||
this.props.form.setErrors(errors);
|
||||
}
|
||||
|
||||
onFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
this.props.onInvalid(errors);
|
||||
}
|
||||
|
||||
this.submit();
|
||||
};
|
||||
onFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
onLoading = (isLoading: boolean) => this.setState({ isLoading });
|
||||
this.submit();
|
||||
};
|
||||
|
||||
onLoading = (isLoading: boolean) => this.setState({ isLoading });
|
||||
}
|
||||
|
@@ -3,37 +3,37 @@ import { MessageDescriptor } from 'react-intl';
|
||||
import i18n from 'app/services/i18n';
|
||||
|
||||
export default class FormComponent<P, S = {}> extends React.Component<P, S> {
|
||||
/**
|
||||
* Formats message resolving intl translations
|
||||
*
|
||||
* @param {string|object} message - message string, or intl message descriptor with an `id` field
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
formatMessage(message: string | MessageDescriptor): string {
|
||||
if (!message) {
|
||||
throw new Error('A message is required');
|
||||
/**
|
||||
* Formats message resolving intl translations
|
||||
*
|
||||
* @param {string|object} message - message string, or intl message descriptor with an `id` field
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
formatMessage(message: string | MessageDescriptor): string {
|
||||
if (!message) {
|
||||
throw new Error('A message is required');
|
||||
}
|
||||
|
||||
if (typeof message === 'string') {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (!message.id) {
|
||||
throw new Error(`Invalid message format: ${JSON.stringify(message)}`);
|
||||
}
|
||||
|
||||
return i18n.getIntl().formatMessage(message);
|
||||
}
|
||||
|
||||
if (typeof message === 'string') {
|
||||
return message;
|
||||
}
|
||||
/**
|
||||
* Focuses this field
|
||||
*/
|
||||
focus() {}
|
||||
|
||||
if (!message.id) {
|
||||
throw new Error(`Invalid message format: ${JSON.stringify(message)}`);
|
||||
}
|
||||
|
||||
return i18n.getIntl().formatMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses this field
|
||||
*/
|
||||
focus() {}
|
||||
|
||||
/**
|
||||
* A hook, that called, when the form was submitted with invalid data
|
||||
* This is useful for the cases, when some field needs to be refreshed e.g. captcha
|
||||
*/
|
||||
onFormInvalid() {}
|
||||
/**
|
||||
* A hook, that called, when the form was submitted with invalid data
|
||||
* This is useful for the cases, when some field needs to be refreshed e.g. captcha
|
||||
*/
|
||||
onFormInvalid() {}
|
||||
}
|
||||
|
@@ -5,36 +5,31 @@ import { MessageDescriptor } from 'react-intl';
|
||||
import styles from './form.scss';
|
||||
|
||||
interface Props {
|
||||
error?: Parameters<typeof resolveError>[0] | MessageDescriptor | null;
|
||||
error?: Parameters<typeof resolveError>[0] | MessageDescriptor | null;
|
||||
}
|
||||
|
||||
function isMessageDescriptor(
|
||||
message: Props['error'],
|
||||
): message is MessageDescriptor {
|
||||
return (
|
||||
typeof message === 'object' &&
|
||||
typeof (message as MessageDescriptor).id !== 'undefined'
|
||||
);
|
||||
function isMessageDescriptor(message: Props['error']): message is MessageDescriptor {
|
||||
return typeof message === 'object' && typeof (message as MessageDescriptor).id !== 'undefined';
|
||||
}
|
||||
|
||||
const FormError: ComponentType<Props> = ({ error }) => {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let content: ReactNode;
|
||||
let content: ReactNode;
|
||||
|
||||
if (isMessageDescriptor(error)) {
|
||||
content = error;
|
||||
} else {
|
||||
content = resolveError(error);
|
||||
}
|
||||
if (isMessageDescriptor(error)) {
|
||||
content = error;
|
||||
} else {
|
||||
content = resolveError(error);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.fieldError} role="alert">
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.fieldError} role="alert">
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormError;
|
||||
|
@@ -8,29 +8,29 @@ import { ValidationError } from './FormModel';
|
||||
type Error = ValidationError | MessageDescriptor;
|
||||
|
||||
export default class FormInputComponent<P, S = {}> extends FormComponent<
|
||||
P & {
|
||||
error?: Error;
|
||||
},
|
||||
S & {
|
||||
error?: Error;
|
||||
}
|
||||
> {
|
||||
componentDidUpdate() {
|
||||
if (this.state && this.state.error) {
|
||||
this.setState({
|
||||
error: undefined,
|
||||
});
|
||||
P & {
|
||||
error?: Error;
|
||||
},
|
||||
S & {
|
||||
error?: Error;
|
||||
}
|
||||
> {
|
||||
componentDidUpdate() {
|
||||
if (this.state && this.state.error) {
|
||||
this.setState({
|
||||
error: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderError() {
|
||||
const error = (this.state && this.state.error) || this.props.error;
|
||||
renderError() {
|
||||
const error = (this.state && this.state.error) || this.props.error;
|
||||
|
||||
return <FormError error={error} />;
|
||||
}
|
||||
return <FormError error={error} />;
|
||||
}
|
||||
|
||||
setError(error: Error) {
|
||||
// @ts-ignore
|
||||
this.setState({ error });
|
||||
}
|
||||
setError(error: Error) {
|
||||
// @ts-ignore
|
||||
this.setState({ error });
|
||||
}
|
||||
}
|
||||
|
@@ -3,215 +3,211 @@ import FormInputComponent from './FormInputComponent';
|
||||
type LoadingListener = (isLoading: boolean) => void;
|
||||
|
||||
export type ValidationError =
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
payload?: Record<string, any>;
|
||||
};
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
payload?: Record<string, any>;
|
||||
};
|
||||
|
||||
export default class FormModel {
|
||||
fields: Record<string, any> = {};
|
||||
errors: Record<string, ValidationError> = {};
|
||||
handlers: Array<LoadingListener> = [];
|
||||
renderErrors: boolean;
|
||||
_isLoading: boolean;
|
||||
fields: Record<string, any> = {};
|
||||
errors: Record<string, ValidationError> = {};
|
||||
handlers: Array<LoadingListener> = [];
|
||||
renderErrors: boolean;
|
||||
_isLoading: boolean;
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {bool} [options.renderErrors=true] - whether the bound filed should
|
||||
* render their errors
|
||||
*/
|
||||
constructor(options: { renderErrors?: boolean } = {}) {
|
||||
this.renderErrors = options.renderErrors !== false;
|
||||
}
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {bool} [options.renderErrors=true] - whether the bound filed should
|
||||
* render their errors
|
||||
*/
|
||||
constructor(options: { renderErrors?: boolean } = {}) {
|
||||
this.renderErrors = options.renderErrors !== false;
|
||||
}
|
||||
|
||||
hasField(fieldId: string): boolean {
|
||||
return !!this.fields[fieldId];
|
||||
}
|
||||
hasField(fieldId: string): boolean {
|
||||
return !!this.fields[fieldId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects form with React's component
|
||||
*
|
||||
* Usage:
|
||||
* <input {...this.form.bindField('foo')} type="text" />
|
||||
*
|
||||
* @param {string} name - the name of field
|
||||
*
|
||||
* @returns {object} - ref and name props for component
|
||||
*/
|
||||
bindField(
|
||||
name: string,
|
||||
): {
|
||||
name: string;
|
||||
ref: (el: any) => void;
|
||||
error?: ValidationError;
|
||||
} {
|
||||
this.fields[name] = {};
|
||||
/**
|
||||
* Connects form with React's component
|
||||
*
|
||||
* Usage:
|
||||
* <input {...this.form.bindField('foo')} type="text" />
|
||||
*
|
||||
* @param {string} name - the name of field
|
||||
*
|
||||
* @returns {object} - ref and name props for component
|
||||
*/
|
||||
bindField(
|
||||
name: string,
|
||||
): {
|
||||
name: string;
|
||||
ref: (el: any) => void;
|
||||
error?: ValidationError;
|
||||
} {
|
||||
this.fields[name] = {};
|
||||
|
||||
const props: {
|
||||
name: string;
|
||||
ref: (el: any) => void;
|
||||
error?: ValidationError;
|
||||
} = {
|
||||
name,
|
||||
ref: (el: FormInputComponent<any> | null) => {
|
||||
if (el) {
|
||||
if (!(el instanceof FormInputComponent)) {
|
||||
throw new Error('Expected FormInputComponent component');
|
||||
}
|
||||
const props: {
|
||||
name: string;
|
||||
ref: (el: any) => void;
|
||||
error?: ValidationError;
|
||||
} = {
|
||||
name,
|
||||
ref: (el: FormInputComponent<any> | null) => {
|
||||
if (el) {
|
||||
if (!(el instanceof FormInputComponent)) {
|
||||
throw new Error('Expected FormInputComponent component');
|
||||
}
|
||||
|
||||
this.fields[name] = el;
|
||||
} else {
|
||||
delete this.fields[name];
|
||||
this.fields[name] = el;
|
||||
} else {
|
||||
delete this.fields[name];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const error = this.getError(name);
|
||||
|
||||
if (this.renderErrors && error) {
|
||||
props.error = error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const error = this.getError(name);
|
||||
|
||||
if (this.renderErrors && error) {
|
||||
props.error = error;
|
||||
return props;
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses field
|
||||
*
|
||||
* @param {string} fieldId - an id of field to focus
|
||||
*/
|
||||
focus(fieldId: string): void {
|
||||
if (!this.fields[fieldId]) {
|
||||
throw new Error(
|
||||
`Can not focus. The field with an id ${fieldId} does not exists`,
|
||||
);
|
||||
}
|
||||
|
||||
this.fields[fieldId].focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value of field
|
||||
*
|
||||
* @param {string} fieldId - an id of field to get value of
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
value(fieldId: string): string {
|
||||
const field = this.fields[fieldId];
|
||||
|
||||
if (!field) {
|
||||
throw new Error(
|
||||
`Can not get value. The field with an id ${fieldId} does not exists`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!field.getValue) {
|
||||
return ''; // the field was not initialized through ref yet
|
||||
}
|
||||
|
||||
return field.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add errors to form fields
|
||||
*
|
||||
* errorType may be string or object {type: string, payload: object}, where
|
||||
* payload is additional data for errorType
|
||||
*
|
||||
* @param {object} errors - object maping {fieldId: errorType}
|
||||
*/
|
||||
setErrors(errors: Record<string, ValidationError>): void {
|
||||
if (typeof errors !== 'object' || errors === null) {
|
||||
throw new Error('Errors must be an object');
|
||||
}
|
||||
|
||||
const oldErrors = this.errors;
|
||||
this.errors = errors;
|
||||
|
||||
Object.keys(this.fields).forEach((fieldId) => {
|
||||
if (this.renderErrors) {
|
||||
if (oldErrors[fieldId] || errors[fieldId]) {
|
||||
this.fields[fieldId].setError(errors[fieldId] || null);
|
||||
/**
|
||||
* Focuses field
|
||||
*
|
||||
* @param {string} fieldId - an id of field to focus
|
||||
*/
|
||||
focus(fieldId: string): void {
|
||||
if (!this.fields[fieldId]) {
|
||||
throw new Error(`Can not focus. The field with an id ${fieldId} does not exists`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hasErrors()) {
|
||||
this.fields[fieldId].onFormInvalid();
|
||||
}
|
||||
});
|
||||
}
|
||||
this.fields[fieldId].focus();
|
||||
}
|
||||
|
||||
getFirstError(): ValidationError | null {
|
||||
const [error] = Object.values(this.errors);
|
||||
/**
|
||||
* Get a value of field
|
||||
*
|
||||
* @param {string} fieldId - an id of field to get value of
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
value(fieldId: string): string {
|
||||
const field = this.fields[fieldId];
|
||||
|
||||
return error || null;
|
||||
}
|
||||
if (!field) {
|
||||
throw new Error(`Can not get value. The field with an id ${fieldId} does not exists`);
|
||||
}
|
||||
|
||||
getError(fieldId: string): ValidationError | null {
|
||||
return this.errors[fieldId] || null;
|
||||
}
|
||||
if (!field.getValue) {
|
||||
return ''; // the field was not initialized through ref yet
|
||||
}
|
||||
|
||||
hasErrors(): boolean {
|
||||
return Object.keys(this.errors).length > 0;
|
||||
}
|
||||
return field.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert form into key-value object representation
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
serialize(): Record<string, any> {
|
||||
return Object.keys(this.fields).reduce((acc, fieldId) => {
|
||||
const field = this.fields[fieldId];
|
||||
/**
|
||||
* Add errors to form fields
|
||||
*
|
||||
* errorType may be string or object {type: string, payload: object}, where
|
||||
* payload is additional data for errorType
|
||||
*
|
||||
* @param {object} errors - object maping {fieldId: errorType}
|
||||
*/
|
||||
setErrors(errors: Record<string, ValidationError>): void {
|
||||
if (typeof errors !== 'object' || errors === null) {
|
||||
throw new Error('Errors must be an object');
|
||||
}
|
||||
|
||||
if (field) {
|
||||
acc[fieldId] = field.getValue();
|
||||
} else {
|
||||
console.warn('Can not serialize %s field. Because it is null', fieldId);
|
||||
}
|
||||
const oldErrors = this.errors;
|
||||
this.errors = errors;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
}
|
||||
Object.keys(this.fields).forEach((fieldId) => {
|
||||
if (this.renderErrors) {
|
||||
if (oldErrors[fieldId] || errors[fieldId]) {
|
||||
this.fields[fieldId].setError(errors[fieldId] || null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind handler to listen for form loading state change
|
||||
*
|
||||
* @param {Function} fn
|
||||
*/
|
||||
addLoadingListener(fn: LoadingListener): void {
|
||||
this.removeLoadingListener(fn);
|
||||
this.handlers.push(fn);
|
||||
}
|
||||
if (this.hasErrors()) {
|
||||
this.fields[fieldId].onFormInvalid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove form loading state handler
|
||||
*
|
||||
* @param {Function} fn
|
||||
*/
|
||||
removeLoadingListener(fn: LoadingListener): void {
|
||||
this.handlers = this.handlers.filter((handler) => handler !== fn);
|
||||
}
|
||||
getFirstError(): ValidationError | null {
|
||||
const [error] = Object.values(this.errors);
|
||||
|
||||
/**
|
||||
* Switch form in loading state
|
||||
*/
|
||||
beginLoading(): void {
|
||||
this._isLoading = true;
|
||||
this.notifyHandlers();
|
||||
}
|
||||
return error || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable loading state
|
||||
*/
|
||||
endLoading(): void {
|
||||
this._isLoading = false;
|
||||
this.notifyHandlers();
|
||||
}
|
||||
getError(fieldId: string): ValidationError | null {
|
||||
return this.errors[fieldId] || null;
|
||||
}
|
||||
|
||||
private notifyHandlers(): void {
|
||||
this.handlers.forEach((fn) => fn(this._isLoading));
|
||||
}
|
||||
hasErrors(): boolean {
|
||||
return Object.keys(this.errors).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert form into key-value object representation
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
serialize(): Record<string, any> {
|
||||
return Object.keys(this.fields).reduce((acc, fieldId) => {
|
||||
const field = this.fields[fieldId];
|
||||
|
||||
if (field) {
|
||||
acc[fieldId] = field.getValue();
|
||||
} else {
|
||||
console.warn('Can not serialize %s field. Because it is null', fieldId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind handler to listen for form loading state change
|
||||
*
|
||||
* @param {Function} fn
|
||||
*/
|
||||
addLoadingListener(fn: LoadingListener): void {
|
||||
this.removeLoadingListener(fn);
|
||||
this.handlers.push(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove form loading state handler
|
||||
*
|
||||
* @param {Function} fn
|
||||
*/
|
||||
removeLoadingListener(fn: LoadingListener): void {
|
||||
this.handlers = this.handlers.filter((handler) => handler !== fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch form in loading state
|
||||
*/
|
||||
beginLoading(): void {
|
||||
this._isLoading = true;
|
||||
this.notifyHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable loading state
|
||||
*/
|
||||
endLoading(): void {
|
||||
this._isLoading = false;
|
||||
this.notifyHandlers();
|
||||
}
|
||||
|
||||
private notifyHandlers(): void {
|
||||
this.handlers.forEach((fn) => fn(this._isLoading));
|
||||
}
|
||||
}
|
||||
|
@@ -6,22 +6,22 @@ import { IntlProvider } from 'react-intl';
|
||||
import Input from './Input';
|
||||
|
||||
describe('Input', () => {
|
||||
it('should return input value', () => {
|
||||
let component: Input | null = null;
|
||||
it('should return input value', () => {
|
||||
let component: Input | null = null;
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en" defaultLocale="en">
|
||||
<Input
|
||||
defaultValue="foo"
|
||||
name="test"
|
||||
ref={(el) => {
|
||||
component = el;
|
||||
}}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
render(
|
||||
<IntlProvider locale="en" defaultLocale="en">
|
||||
<Input
|
||||
defaultValue="foo"
|
||||
name="test"
|
||||
ref={(el) => {
|
||||
component = el;
|
||||
}}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByDisplayValue('foo'), 'to be a', HTMLElement);
|
||||
expect(component && (component as Input).getValue(), 'to equal', 'foo');
|
||||
});
|
||||
expect(screen.getByDisplayValue('foo'), 'to be a', HTMLElement);
|
||||
expect(component && (component as Input).getValue(), 'to equal', 'foo');
|
||||
});
|
||||
});
|
||||
|
@@ -12,164 +12,141 @@ import FormInputComponent from './FormInputComponent';
|
||||
let copiedStateTimeout: NodeJS.Timeout;
|
||||
|
||||
export default class Input extends FormInputComponent<
|
||||
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'placeholder'> & {
|
||||
skin: Skin;
|
||||
color: Color;
|
||||
center: boolean;
|
||||
disabled: boolean;
|
||||
label?: string | MessageDescriptor;
|
||||
placeholder?: string | MessageDescriptor;
|
||||
icon?: string;
|
||||
copy?: boolean;
|
||||
},
|
||||
{
|
||||
wasCopied: boolean;
|
||||
}
|
||||
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'placeholder'> & {
|
||||
skin: Skin;
|
||||
color: Color;
|
||||
center: boolean;
|
||||
disabled: boolean;
|
||||
label?: string | MessageDescriptor;
|
||||
placeholder?: string | MessageDescriptor;
|
||||
icon?: string;
|
||||
copy?: boolean;
|
||||
},
|
||||
{
|
||||
wasCopied: boolean;
|
||||
}
|
||||
> {
|
||||
static defaultProps = {
|
||||
color: COLOR_GREEN,
|
||||
skin: SKIN_DARK,
|
||||
center: false,
|
||||
disabled: false,
|
||||
};
|
||||
static defaultProps = {
|
||||
color: COLOR_GREEN,
|
||||
skin: SKIN_DARK,
|
||||
center: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
wasCopied: false,
|
||||
};
|
||||
state = {
|
||||
wasCopied: false,
|
||||
};
|
||||
|
||||
elRef = React.createRef<HTMLInputElement>();
|
||||
elRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
render() {
|
||||
const {
|
||||
color,
|
||||
skin,
|
||||
center,
|
||||
icon: iconType,
|
||||
copy: showCopyIcon,
|
||||
placeholder: placeholderText,
|
||||
} = this.props;
|
||||
let { label: labelContent } = this.props;
|
||||
const { wasCopied } = this.state;
|
||||
let placeholder: string | undefined;
|
||||
render() {
|
||||
const { color, skin, center, icon: iconType, copy: showCopyIcon, placeholder: placeholderText } = this.props;
|
||||
let { label: labelContent } = this.props;
|
||||
const { wasCopied } = this.state;
|
||||
let placeholder: string | undefined;
|
||||
|
||||
const props = omit(
|
||||
{
|
||||
type: 'text',
|
||||
...this.props,
|
||||
},
|
||||
[
|
||||
'label',
|
||||
'placeholder',
|
||||
'error',
|
||||
'skin',
|
||||
'color',
|
||||
'center',
|
||||
'icon',
|
||||
'copy',
|
||||
],
|
||||
);
|
||||
const props = omit(
|
||||
{
|
||||
type: 'text',
|
||||
...this.props,
|
||||
},
|
||||
['label', 'placeholder', 'error', 'skin', 'color', 'center', 'icon', 'copy'],
|
||||
);
|
||||
|
||||
let label: React.ReactElement | null = null;
|
||||
let copyIcon: React.ReactElement | null = null;
|
||||
let icon: React.ReactElement | null = null;
|
||||
let label: React.ReactElement | null = null;
|
||||
let copyIcon: React.ReactElement | null = null;
|
||||
let icon: React.ReactElement | null = null;
|
||||
|
||||
if (labelContent) {
|
||||
if (!props.id) {
|
||||
props.id = uniqueId('input');
|
||||
}
|
||||
if (labelContent) {
|
||||
if (!props.id) {
|
||||
props.id = uniqueId('input');
|
||||
}
|
||||
|
||||
labelContent = this.formatMessage(labelContent);
|
||||
labelContent = this.formatMessage(labelContent);
|
||||
|
||||
label = (
|
||||
<label className={styles.textFieldLabel} htmlFor={props.id}>
|
||||
{labelContent}
|
||||
</label>
|
||||
);
|
||||
label = (
|
||||
<label className={styles.textFieldLabel} htmlFor={props.id}>
|
||||
{labelContent}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
if (placeholderText) {
|
||||
placeholder = this.formatMessage(placeholderText);
|
||||
}
|
||||
|
||||
let baseClass = styles.formRow;
|
||||
|
||||
if (iconType) {
|
||||
baseClass = styles.formIconRow;
|
||||
icon = <span className={clsx(styles.textFieldIcon, icons[iconType])} />;
|
||||
}
|
||||
|
||||
if (showCopyIcon) {
|
||||
copyIcon = (
|
||||
<div
|
||||
className={clsx(styles.copyIcon, {
|
||||
[icons.clipboard]: !wasCopied,
|
||||
[icons.checkmark]: wasCopied,
|
||||
[styles.copyCheckmark]: wasCopied,
|
||||
})}
|
||||
onClick={this.onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{label}
|
||||
<div className={styles.textFieldContainer}>
|
||||
<input
|
||||
ref={this.elRef}
|
||||
className={clsx(styles[`${skin}TextField`], styles[`${color}TextField`], {
|
||||
[styles.textFieldCenter]: center,
|
||||
})}
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
/>
|
||||
{icon}
|
||||
{copyIcon}
|
||||
</div>
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (placeholderText) {
|
||||
placeholder = this.formatMessage(placeholderText);
|
||||
getValue() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
return el && el.value;
|
||||
}
|
||||
|
||||
let baseClass = styles.formRow;
|
||||
focus() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
if (iconType) {
|
||||
baseClass = styles.formIconRow;
|
||||
icon = <span className={clsx(styles.textFieldIcon, icons[iconType])} />;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.focus();
|
||||
setTimeout(el.focus.bind(el), 10);
|
||||
}
|
||||
|
||||
if (showCopyIcon) {
|
||||
copyIcon = (
|
||||
<div
|
||||
className={clsx(styles.copyIcon, {
|
||||
[icons.clipboard]: !wasCopied,
|
||||
[icons.checkmark]: wasCopied,
|
||||
[styles.copyCheckmark]: wasCopied,
|
||||
})}
|
||||
onClick={this.onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
onCopy = async () => {
|
||||
const value = this.getValue();
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{label}
|
||||
<div className={styles.textFieldContainer}>
|
||||
<input
|
||||
ref={this.elRef}
|
||||
className={clsx(
|
||||
styles[`${skin}TextField`],
|
||||
styles[`${color}TextField`],
|
||||
{
|
||||
[styles.textFieldCenter]: center,
|
||||
},
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
/>
|
||||
{icon}
|
||||
{copyIcon}
|
||||
</div>
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
const { current: el } = this.elRef;
|
||||
try {
|
||||
clearTimeout(copiedStateTimeout);
|
||||
copiedStateTimeout = setTimeout(() => this.setState({ wasCopied: false }), 2000);
|
||||
|
||||
return el && el.value;
|
||||
}
|
||||
|
||||
focus() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.focus();
|
||||
setTimeout(el.focus.bind(el), 10);
|
||||
}
|
||||
|
||||
onCopy = async () => {
|
||||
const value = this.getValue();
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
clearTimeout(copiedStateTimeout);
|
||||
copiedStateTimeout = setTimeout(
|
||||
() => this.setState({ wasCopied: false }),
|
||||
2000,
|
||||
);
|
||||
|
||||
await copy(value);
|
||||
this.setState({ wasCopied: true });
|
||||
} catch (err) {
|
||||
// it's okay
|
||||
}
|
||||
};
|
||||
await copy(value);
|
||||
this.setState({ wasCopied: true });
|
||||
} catch (err) {
|
||||
// it's okay
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -7,12 +7,7 @@ type ButtonProps = React.ComponentProps<typeof Button>;
|
||||
type LinkProps = React.ComponentProps<typeof Link>;
|
||||
|
||||
export default function LinkButton(props: ButtonProps & LinkProps) {
|
||||
const { to, ...restProps } = props;
|
||||
const { to, ...restProps } = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
component={(linkProps) => <Link {...linkProps} to={to} />}
|
||||
{...(restProps as ButtonProps)}
|
||||
/>
|
||||
);
|
||||
return <Button component={(linkProps) => <Link {...linkProps} to={to} />} {...(restProps as ButtonProps)} />;
|
||||
}
|
||||
|
@@ -9,58 +9,48 @@ import styles from './form.scss';
|
||||
import FormInputComponent from './FormInputComponent';
|
||||
|
||||
export default class Radio extends FormInputComponent<
|
||||
{
|
||||
color: Color;
|
||||
skin: Skin;
|
||||
label: string | MessageDescriptor;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>
|
||||
{
|
||||
color: Color;
|
||||
skin: Skin;
|
||||
label: string | MessageDescriptor;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>
|
||||
> {
|
||||
static defaultProps = {
|
||||
color: COLOR_GREEN,
|
||||
skin: SKIN_DARK,
|
||||
};
|
||||
static defaultProps = {
|
||||
color: COLOR_GREEN,
|
||||
skin: SKIN_DARK,
|
||||
};
|
||||
|
||||
elRef = React.createRef<HTMLInputElement>();
|
||||
elRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
render() {
|
||||
const { color, skin } = this.props;
|
||||
let { label } = this.props;
|
||||
render() {
|
||||
const { color, skin } = this.props;
|
||||
let { label } = this.props;
|
||||
|
||||
label = this.formatMessage(label);
|
||||
label = this.formatMessage(label);
|
||||
|
||||
const props = omit(this.props, ['color', 'skin', 'label']);
|
||||
const props = omit(this.props, ['color', 'skin', 'label']);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
styles[`${color}MarkableRow`],
|
||||
styles[`${skin}MarkableRow`],
|
||||
)}
|
||||
>
|
||||
<label className={styles.markableContainer}>
|
||||
<input
|
||||
ref={this.elRef}
|
||||
className={styles.markableInput}
|
||||
type="radio"
|
||||
{...props}
|
||||
/>
|
||||
<div className={styles.radio} />
|
||||
{label}
|
||||
</label>
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={clsx(styles[`${color}MarkableRow`], styles[`${skin}MarkableRow`])}>
|
||||
<label className={styles.markableContainer}>
|
||||
<input ref={this.elRef} className={styles.markableInput} type="radio" {...props} />
|
||||
<div className={styles.radio} />
|
||||
{label}
|
||||
</label>
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
const { current: el } = this.elRef;
|
||||
getValue() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
return el && el.checked ? 1 : 0;
|
||||
}
|
||||
return el && el.checked ? 1 : 0;
|
||||
}
|
||||
|
||||
focus() {
|
||||
const { current: el } = this.elRef;
|
||||
focus() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
el && el.focus();
|
||||
}
|
||||
el && el.focus();
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { MessageDescriptor } from 'react-intl';
|
||||
import TextareaAutosize, {
|
||||
TextareaAutosizeProps,
|
||||
} from 'react-textarea-autosize';
|
||||
import TextareaAutosize, { TextareaAutosizeProps } from 'react-textarea-autosize';
|
||||
import clsx from 'clsx';
|
||||
import { uniqueId, omit } from 'app/functions';
|
||||
import { SKIN_DARK, COLOR_GREEN, Skin, Color } from 'app/components/ui';
|
||||
@@ -11,90 +9,79 @@ import styles from './form.scss';
|
||||
import FormInputComponent from './FormInputComponent';
|
||||
|
||||
interface OwnProps {
|
||||
placeholder?: string | MessageDescriptor;
|
||||
label?: string | MessageDescriptor;
|
||||
skin: Skin;
|
||||
color: Color;
|
||||
placeholder?: string | MessageDescriptor;
|
||||
label?: string | MessageDescriptor;
|
||||
skin: Skin;
|
||||
color: Color;
|
||||
}
|
||||
|
||||
export default class TextArea extends FormInputComponent<
|
||||
OwnProps & Omit<TextareaAutosizeProps, keyof OwnProps>
|
||||
> {
|
||||
static defaultProps = {
|
||||
color: COLOR_GREEN,
|
||||
skin: SKIN_DARK,
|
||||
};
|
||||
export default class TextArea extends FormInputComponent<OwnProps & Omit<TextareaAutosizeProps, keyof OwnProps>> {
|
||||
static defaultProps = {
|
||||
color: COLOR_GREEN,
|
||||
skin: SKIN_DARK,
|
||||
};
|
||||
|
||||
elRef = React.createRef<HTMLTextAreaElement>();
|
||||
elRef = React.createRef<HTMLTextAreaElement>();
|
||||
|
||||
render() {
|
||||
const {
|
||||
color,
|
||||
skin,
|
||||
label: labelText,
|
||||
placeholder: placeholderText,
|
||||
} = this.props;
|
||||
let label: React.ReactElement | undefined;
|
||||
let placeholder: string | undefined;
|
||||
render() {
|
||||
const { color, skin, label: labelText, placeholder: placeholderText } = this.props;
|
||||
let label: React.ReactElement | undefined;
|
||||
let placeholder: string | undefined;
|
||||
|
||||
const props = omit(
|
||||
{
|
||||
type: 'text',
|
||||
...this.props,
|
||||
},
|
||||
['label', 'placeholder', 'error', 'skin', 'color'],
|
||||
);
|
||||
const props = omit(
|
||||
{
|
||||
type: 'text',
|
||||
...this.props,
|
||||
},
|
||||
['label', 'placeholder', 'error', 'skin', 'color'],
|
||||
);
|
||||
|
||||
if (labelText) {
|
||||
if (!props.id) {
|
||||
props.id = uniqueId('textarea');
|
||||
}
|
||||
if (labelText) {
|
||||
if (!props.id) {
|
||||
props.id = uniqueId('textarea');
|
||||
}
|
||||
|
||||
label = (
|
||||
<label className={styles.textFieldLabel} htmlFor={props.id}>
|
||||
{this.formatMessage(labelText)}
|
||||
</label>
|
||||
);
|
||||
label = (
|
||||
<label className={styles.textFieldLabel} htmlFor={props.id}>
|
||||
{this.formatMessage(labelText)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
if (placeholderText) {
|
||||
placeholder = this.formatMessage(placeholderText);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.formRow}>
|
||||
{label}
|
||||
<div className={styles.textAreaContainer}>
|
||||
<TextareaAutosize
|
||||
inputRef={this.elRef}
|
||||
className={clsx(styles.textArea, styles[`${skin}TextField`], styles[`${color}TextField`])}
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (placeholderText) {
|
||||
placeholder = this.formatMessage(placeholderText);
|
||||
getValue() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
return el && el.value;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.formRow}>
|
||||
{label}
|
||||
<div className={styles.textAreaContainer}>
|
||||
<TextareaAutosize
|
||||
inputRef={this.elRef}
|
||||
className={clsx(
|
||||
styles.textArea,
|
||||
styles[`${skin}TextField`],
|
||||
styles[`${color}TextField`],
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
{this.renderError()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
focus() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
getValue() {
|
||||
const { current: el } = this.elRef;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
return el && el.value;
|
||||
}
|
||||
|
||||
focus() {
|
||||
const { current: el } = this.elRef;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
el.focus();
|
||||
setTimeout(el.focus.bind(el), 10);
|
||||
}
|
||||
|
||||
el.focus();
|
||||
setTimeout(el.focus.bind(el), 10);
|
||||
}
|
||||
}
|
||||
|
@@ -4,126 +4,126 @@
|
||||
$dropdownPadding: 15px;
|
||||
|
||||
@mixin dropdown-theme($themeName, $backgroundColor) {
|
||||
.#{$themeName} {
|
||||
composes: dropdown;
|
||||
.#{$themeName} {
|
||||
composes: dropdown;
|
||||
|
||||
background-color: $backgroundColor;
|
||||
background-color: $backgroundColor;
|
||||
|
||||
.menuItem:hover,
|
||||
&:hover {
|
||||
background-color: lighter($backgroundColor);
|
||||
.menuItem:hover,
|
||||
&:hover {
|
||||
background-color: lighter($backgroundColor);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.opened {
|
||||
background-color: darker($backgroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.opened {
|
||||
background-color: darker($backgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
height: 50px;
|
||||
// 28px - ширина иконки при заданном размере шрифта
|
||||
padding: 0 ($dropdownPadding * 2 + 28px) 0 $dropdownPadding;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
height: 50px;
|
||||
// 28px - ширина иконки при заданном размере шрифта
|
||||
padding: 0 ($dropdownPadding * 2 + 28px) 0 $dropdownPadding;
|
||||
position: relative;
|
||||
|
||||
font-family: $font-family-title;
|
||||
color: $defaultButtonTextColor;
|
||||
font-size: 18px;
|
||||
line-height: 50px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-family: $font-family-title;
|
||||
color: $defaultButtonTextColor;
|
||||
font-size: 18px;
|
||||
line-height: 50px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
transition: background-color 0.25s;
|
||||
transition: background-color 0.25s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.opened {
|
||||
}
|
||||
|
||||
.toggleIcon {
|
||||
composes: selecter from '~app/components/ui/icons.scss';
|
||||
composes: selecter from '~app/components/ui/icons.scss';
|
||||
|
||||
position: absolute;
|
||||
right: $dropdownPadding;
|
||||
top: 16px;
|
||||
font-size: 17px;
|
||||
transition: right 0.3s cubic-bezier(0.23, 1, 0.32, 1); // easeOutQuint
|
||||
position: absolute;
|
||||
right: $dropdownPadding;
|
||||
top: 16px;
|
||||
font-size: 17px;
|
||||
transition: right 0.3s cubic-bezier(0.23, 1, 0.32, 1); // easeOutQuint
|
||||
|
||||
.dropdown:hover & {
|
||||
right: $dropdownPadding - 5px;
|
||||
}
|
||||
.dropdown:hover & {
|
||||
right: $dropdownPadding - 5px;
|
||||
}
|
||||
|
||||
.dropdown:active &,
|
||||
.dropdown.opened & {
|
||||
right: $dropdownPadding + 5px;
|
||||
}
|
||||
.dropdown:active &,
|
||||
.dropdown.opened & {
|
||||
right: $dropdownPadding + 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
.menu {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
width: 120%;
|
||||
width: 120%;
|
||||
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
transition: 0.5s ease;
|
||||
transition-property: opacity, visibility;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: 0.5s ease;
|
||||
transition-property: opacity, visibility;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.dropdown.opened & {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
.dropdown.opened & {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
composes: label;
|
||||
composes: label;
|
||||
|
||||
height: 50px;
|
||||
padding: 0 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: 50px;
|
||||
padding: 0 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
color: #444;
|
||||
line-height: 50px;
|
||||
font-size: 18px;
|
||||
font-family: $font-family-title;
|
||||
color: #444;
|
||||
line-height: 50px;
|
||||
font-size: 18px;
|
||||
font-family: $font-family-title;
|
||||
|
||||
border-bottom: 1px solid #ebe8df;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #ebe8df;
|
||||
cursor: pointer;
|
||||
|
||||
transition: 0.25s;
|
||||
transition: 0.25s;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@include dropdown-theme('green', $green);
|
||||
|
@@ -2,210 +2,210 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
@mixin form-transition() {
|
||||
// Анимация фона должна быть быстрее анимации рамки, т.к. визуально фон заполняется медленнее
|
||||
transition: border-color 0.25s, background-color 0.2s;
|
||||
// Анимация фона должна быть быстрее анимации рамки, т.к. визуально фон заполняется медленнее
|
||||
transition: border-color 0.25s, background-color 0.2s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input
|
||||
*/
|
||||
@mixin input-theme($themeName, $color) {
|
||||
.#{$themeName}TextField {
|
||||
composes: textField;
|
||||
.#{$themeName}TextField {
|
||||
composes: textField;
|
||||
|
||||
&:focus {
|
||||
border-color: $color;
|
||||
&:focus {
|
||||
border-color: $color;
|
||||
|
||||
~ .textFieldIcon {
|
||||
background: $color;
|
||||
border-color: $color;
|
||||
}
|
||||
~ .textFieldIcon {
|
||||
background: $color;
|
||||
border-color: $color;
|
||||
}
|
||||
|
||||
&.lightTextField {
|
||||
color: $color;
|
||||
}
|
||||
&.lightTextField {
|
||||
color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formRow {
|
||||
margin: 10px 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.formIconRow {
|
||||
composes: formRow;
|
||||
composes: formRow;
|
||||
|
||||
.textField {
|
||||
padding-left: 60px;
|
||||
}
|
||||
.textField {
|
||||
padding-left: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.textFieldContainer {
|
||||
position: relative;
|
||||
height: 50px;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
height: 50px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.textField {
|
||||
box-sizing: border-box;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
|
||||
border: 2px solid;
|
||||
border: 2px solid;
|
||||
|
||||
font-size: 18px;
|
||||
color: #aaa;
|
||||
font-family: $font-family-title;
|
||||
padding: 0 10px;
|
||||
font-size: 18px;
|
||||
color: #aaa;
|
||||
font-family: $font-family-title;
|
||||
padding: 0 10px;
|
||||
|
||||
transition: border-color 0.25s;
|
||||
transition: border-color 0.25s;
|
||||
|
||||
&:hover {
|
||||
&,
|
||||
~ .textFieldIcon {
|
||||
border-color: #aaa;
|
||||
&:hover {
|
||||
&,
|
||||
~ .textFieldIcon {
|
||||
border-color: #aaa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
outline: none;
|
||||
&:focus {
|
||||
color: #fff;
|
||||
outline: none;
|
||||
|
||||
~ .textFieldIcon {
|
||||
color: #fff;
|
||||
~ .textFieldIcon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textFieldIcon {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
line-height: 46px;
|
||||
text-align: center;
|
||||
border: 2px solid;
|
||||
color: #444;
|
||||
cursor: default;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
line-height: 46px;
|
||||
text-align: center;
|
||||
border: 2px solid;
|
||||
color: #444;
|
||||
cursor: default;
|
||||
|
||||
@include form-transition();
|
||||
@include form-transition();
|
||||
}
|
||||
|
||||
.copyIcon {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 10px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 10px;
|
||||
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
|
||||
font-size: 20px;
|
||||
font-size: 20px;
|
||||
|
||||
transition: 0.25s;
|
||||
transition: 0.25s;
|
||||
}
|
||||
|
||||
.copyCheckmark {
|
||||
color: $green !important;
|
||||
color: $green !important;
|
||||
}
|
||||
|
||||
.darkTextField {
|
||||
background: $black;
|
||||
|
||||
&::placeholder {
|
||||
opacity: 1;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
&,
|
||||
~ .textFieldIcon {
|
||||
border-color: lighter($black);
|
||||
}
|
||||
|
||||
~ .copyIcon {
|
||||
color: #999;
|
||||
background: $black;
|
||||
|
||||
&:hover {
|
||||
background: lighter($black);
|
||||
&::placeholder {
|
||||
opacity: 1;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
&,
|
||||
~ .textFieldIcon {
|
||||
border-color: lighter($black);
|
||||
}
|
||||
|
||||
~ .copyIcon {
|
||||
color: #999;
|
||||
background: $black;
|
||||
|
||||
&:hover {
|
||||
background: lighter($black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightTextField {
|
||||
background: #fff;
|
||||
|
||||
&:disabled {
|
||||
background: #dcd8cd;
|
||||
|
||||
~ .copyIcon {
|
||||
background: #dcd8ce;
|
||||
|
||||
&:hover {
|
||||
background: #ebe8e2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
opacity: 1;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
&,
|
||||
~ .textFieldIcon {
|
||||
border-color: #dcd8cd;
|
||||
}
|
||||
|
||||
~ .copyIcon {
|
||||
color: #aaa;
|
||||
background: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
&:disabled {
|
||||
background: #dcd8cd;
|
||||
|
||||
~ .copyIcon {
|
||||
background: #dcd8ce;
|
||||
|
||||
&:hover {
|
||||
background: #ebe8e2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
opacity: 1;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
&,
|
||||
~ .textFieldIcon {
|
||||
border-color: #dcd8cd;
|
||||
}
|
||||
|
||||
~ .copyIcon {
|
||||
color: #aaa;
|
||||
background: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textFieldLabel {
|
||||
margin: 10px 0;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
display: block;
|
||||
|
||||
font-family: $font-family-title;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
font-family: $font-family-title;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.fieldError {
|
||||
color: $red;
|
||||
font-size: 12px;
|
||||
margin: 3px 0;
|
||||
|
||||
a {
|
||||
border-bottom-color: rgba($red, 0.75);
|
||||
color: $red;
|
||||
font-size: 12px;
|
||||
margin: 3px 0;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: transparent;
|
||||
a {
|
||||
border-bottom-color: rgba($red, 0.75);
|
||||
color: $red;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textAreaContainer {
|
||||
height: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.textArea {
|
||||
height: auto; // unset .textField height
|
||||
min-height: 50px;
|
||||
padding: 5px 10px;
|
||||
resize: none;
|
||||
position: relative;
|
||||
height: auto; // unset .textField height
|
||||
min-height: 50px;
|
||||
padding: 5px 10px;
|
||||
resize: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.textFieldCenter {
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@include input-theme('green', $green);
|
||||
@@ -219,107 +219,107 @@
|
||||
* Markable is our common name for checkboxes and radio buttons
|
||||
*/
|
||||
@mixin markable-theme($themeName, $color) {
|
||||
.#{$themeName}MarkableRow {
|
||||
composes: markableRow;
|
||||
.#{$themeName}MarkableRow {
|
||||
composes: markableRow;
|
||||
|
||||
.markableContainer {
|
||||
&:hover {
|
||||
.mark {
|
||||
border-color: $color;
|
||||
.markableContainer {
|
||||
&:hover {
|
||||
.mark {
|
||||
border-color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markableInput {
|
||||
&:checked {
|
||||
+ .mark {
|
||||
background: $color;
|
||||
border-color: $color;
|
||||
.markableInput {
|
||||
&:checked {
|
||||
+ .mark {
|
||||
background: $color;
|
||||
border-color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markableRow {
|
||||
height: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.markableContainer {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding-left: 27px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding-left: 27px;
|
||||
|
||||
font-family: $font-family-title;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-family: $font-family-title;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markPosition {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.markableInput {
|
||||
composes: markPosition;
|
||||
opacity: 0;
|
||||
composes: markPosition;
|
||||
opacity: 0;
|
||||
|
||||
&:checked {
|
||||
+ .mark {
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
&:checked {
|
||||
+ .mark {
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mark {
|
||||
composes: markPosition;
|
||||
composes: checkmark from '~app/components/ui/icons.scss';
|
||||
composes: markPosition;
|
||||
composes: checkmark from '~app/components/ui/icons.scss';
|
||||
|
||||
border: 2px #dcd8cd solid;
|
||||
border: 2px #dcd8cd solid;
|
||||
|
||||
font-size: 10px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
|
||||
@include form-transition();
|
||||
@include form-transition();
|
||||
|
||||
&:before {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
&:before {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
composes: mark;
|
||||
composes: mark;
|
||||
}
|
||||
|
||||
.radio {
|
||||
composes: mark;
|
||||
composes: mark;
|
||||
|
||||
border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.lightMarkableRow {
|
||||
.markableContainer {
|
||||
color: #666;
|
||||
}
|
||||
.markableContainer {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.darkMarkableRow {
|
||||
.markableContainer {
|
||||
color: #fff;
|
||||
}
|
||||
.markableContainer {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@include markable-theme('green', $green);
|
||||
@@ -327,66 +327,66 @@
|
||||
@include markable-theme('red', $red);
|
||||
|
||||
.isFormLoading {
|
||||
// TODO: надо бы разнести from и input на отдельные модули,
|
||||
// так как в текущем контексте isLoading немного не логичен,
|
||||
// пришлось юзать isFormLoading
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
// TODO: надо бы разнести from и input на отдельные модули,
|
||||
// так как в текущем контексте isLoading немного не логичен,
|
||||
// пришлось юзать isFormLoading
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[type='submit'] {
|
||||
// TODO: duplicate of .loading from components/ui/buttons
|
||||
background: url('./images/loader_button.gif') #95a5a6 center center !important;
|
||||
[type='submit'] {
|
||||
// TODO: duplicate of .loading from components/ui/buttons
|
||||
background: url('./images/loader_button.gif') #95a5a6 center center !important;
|
||||
|
||||
cursor: default;
|
||||
color: #fff;
|
||||
transition: 0.25s;
|
||||
outline: none;
|
||||
}
|
||||
cursor: default;
|
||||
color: #fff;
|
||||
transition: 0.25s;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.captchaContainer {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 302px;
|
||||
height: 77px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 302px;
|
||||
height: 77px;
|
||||
overflow: hidden;
|
||||
|
||||
border: 2px solid;
|
||||
transition: border-color 0.25s;
|
||||
border: 2px solid;
|
||||
transition: border-color 0.25s;
|
||||
|
||||
> div {
|
||||
margin: -2px;
|
||||
}
|
||||
> div {
|
||||
margin: -2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #aaa;
|
||||
}
|
||||
&:hover {
|
||||
border-color: #aaa;
|
||||
}
|
||||
|
||||
// minimum captcha width is 302px, which can not be changed
|
||||
// using transform to scale down to 296px
|
||||
// transform-origin: 0;
|
||||
// transform: scaleX(0.98);
|
||||
// minimum captcha width is 302px, which can not be changed
|
||||
// using transform to scale down to 296px
|
||||
// transform-origin: 0;
|
||||
// transform: scaleX(0.98);
|
||||
}
|
||||
|
||||
.darkCaptcha {
|
||||
border-color: lighter($black);
|
||||
border-color: lighter($black);
|
||||
}
|
||||
|
||||
.lightCaptcha {
|
||||
border-color: #dcd8cd;
|
||||
border-color: #dcd8cd;
|
||||
}
|
||||
|
||||
.captchaLoader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -10,16 +10,4 @@ import Dropdown from './Dropdown';
|
||||
import Captcha from './Captcha';
|
||||
import FormError from './FormError';
|
||||
|
||||
export {
|
||||
Input,
|
||||
TextArea,
|
||||
Button,
|
||||
LinkButton,
|
||||
Checkbox,
|
||||
Radio,
|
||||
Form,
|
||||
FormModel,
|
||||
Dropdown,
|
||||
Captcha,
|
||||
FormError,
|
||||
};
|
||||
export { Input, TextArea, Button, LinkButton, Checkbox, Radio, Form, FormModel, Dropdown, Captcha, FormError };
|
||||
|
Reference in New Issue
Block a user