Change prettier rules

This commit is contained in:
ErickSkrauch
2020-05-24 02:08:24 +03:00
parent 73f0c37a6a
commit f85b9d8d35
382 changed files with 24137 additions and 26046 deletions

View File

@@ -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>
</>
));

View File

@@ -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>
);
}
}

View File

@@ -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 });
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
};
}

View File

@@ -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 });
}

View File

@@ -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() {}
}

View File

@@ -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;

View File

@@ -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 });
}
}

View File

@@ -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));
}
}

View File

@@ -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');
});
});

View File

@@ -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
}
};
}

View File

@@ -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)} />;
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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%;
}
/**

View File

@@ -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 };