mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Implemented strict mode for the project (broken tests, hundreds of @ts-ignore and new errors are included) [skip ci]
This commit is contained in:
committed by
SleepWalker
parent
10e8b77acf
commit
96049ad4ad
@@ -91,9 +91,7 @@ export default class Box {
|
||||
endY: number;
|
||||
}> = [];
|
||||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const i in boxPoints) {
|
||||
const point = boxPoints[i];
|
||||
Object.values(boxPoints).forEach(point => {
|
||||
const angle = Math.atan2(light.y - point.y, light.x - point.x);
|
||||
const endX = point.x + shadowLength * Math.sin(-angle - Math.PI / 2);
|
||||
const endY = point.y + shadowLength * Math.cos(-angle - Math.PI / 2);
|
||||
@@ -103,7 +101,7 @@ export default class Box {
|
||||
endX,
|
||||
endY,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = points.length - 1; i >= 0; i--) {
|
||||
const n = i === 3 ? 0 : i + 1;
|
||||
|
||||
@@ -18,8 +18,7 @@ class BsodMiddleware implements Middleware {
|
||||
async catch<T extends Resp<any>>(
|
||||
resp?: T | InternalServerError | Error,
|
||||
): Promise<T> {
|
||||
const { originalResponse }: { originalResponse?: Resp<any> } = (resp ||
|
||||
{}) as InternalServerError;
|
||||
const { originalResponse } = (resp || {}) as InternalServerError;
|
||||
|
||||
if (
|
||||
resp &&
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Action } from 'redux';
|
||||
import { Action as ReduxAction } from 'redux';
|
||||
|
||||
export const BSOD = 'BSOD';
|
||||
interface BSoDAction extends ReduxAction {
|
||||
type: 'BSOD';
|
||||
}
|
||||
|
||||
export function bsod(): Action {
|
||||
export function bsod(): BSoDAction {
|
||||
return {
|
||||
type: BSOD,
|
||||
type: 'BSOD',
|
||||
};
|
||||
}
|
||||
|
||||
export type Action = BSoDAction;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BSOD } from './actions';
|
||||
import { Action } from './actions';
|
||||
|
||||
export type State = boolean;
|
||||
|
||||
export default function(state: State = false, { type }): State {
|
||||
if (type === BSOD) {
|
||||
export default function(state: State = false, { type }: Action): State {
|
||||
if (type === 'BSOD') {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { InputHTMLAttributes } from 'react';
|
||||
import React, { InputHTMLAttributes, MouseEventHandler } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { MessageDescriptor } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
@@ -40,10 +40,12 @@ export default class Dropdown extends FormInputComponent<Props, State> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ export default class Dropdown extends FormInputComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
onSelectItem(item: OptionItem) {
|
||||
onSelectItem(item: OptionItem): MouseEventHandler {
|
||||
return event => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -141,11 +143,12 @@ export default class Dropdown extends FormInputComponent<Props, State> {
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
onBodyClick = (event: MouseEvent) => {
|
||||
onBodyClick: MouseEventHandler = event => {
|
||||
if (this.state.isActive) {
|
||||
const el = ReactDOM.findDOMNode(this);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const el = ReactDOM.findDOMNode(this)!;
|
||||
|
||||
if (!el.contains(event.target) && el !== event.target) {
|
||||
if (!el.contains(event.target as HTMLElement) && el !== event.target) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import logger from 'app/services/logger';
|
||||
|
||||
import FormModel from './FormModel';
|
||||
import styles from './form.scss';
|
||||
|
||||
interface Props {
|
||||
interface BaseProps {
|
||||
id: string;
|
||||
isLoading: boolean;
|
||||
form?: FormModel;
|
||||
onSubmit: (form: FormModel | FormData) => void | Promise<void>;
|
||||
onInvalid: (errors: { [errorKey: string]: string }) => void;
|
||||
onInvalid: (errors: Record<string, string>) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface PropsWithoutForm extends BaseProps {
|
||||
onSubmit: (form: FormData) => Promise<void> | void;
|
||||
}
|
||||
|
||||
interface PropsWithForm extends BaseProps {
|
||||
form: FormModel;
|
||||
onSubmit: (form: FormModel) => Promise<void> | void;
|
||||
}
|
||||
|
||||
type Props = PropsWithoutForm | PropsWithForm;
|
||||
|
||||
function hasForm(props: Props): props is PropsWithForm {
|
||||
return 'form' in props;
|
||||
}
|
||||
|
||||
interface State {
|
||||
id: string; // just to track value for derived updates
|
||||
isTouched: boolean;
|
||||
@@ -39,7 +54,7 @@ export default class Form extends React.Component<Props, State> {
|
||||
mounted = false;
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.form) {
|
||||
if (hasForm(this.props)) {
|
||||
this.props.form.addLoadingListener(this.onLoading);
|
||||
}
|
||||
|
||||
@@ -65,8 +80,8 @@ export default class Form extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const nextForm = this.props.form;
|
||||
const prevForm = prevProps.form;
|
||||
const nextForm = hasForm(this.props) ? this.props.form : undefined;
|
||||
const prevForm = hasForm(prevProps) ? prevProps.form : undefined;
|
||||
|
||||
if (nextForm !== prevForm) {
|
||||
if (prevForm) {
|
||||
@@ -80,7 +95,7 @@ export default class Form extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.form) {
|
||||
if (hasForm(this.props)) {
|
||||
this.props.form.removeLoadingListener(this.onLoading);
|
||||
}
|
||||
|
||||
@@ -119,15 +134,19 @@ export default class Form extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
if (form.checkValidity()) {
|
||||
const result = this.props.onSubmit(
|
||||
this.props.form ? this.props.form : new FormData(form),
|
||||
);
|
||||
let result: Promise<void> | void;
|
||||
|
||||
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: { [key: string]: string }) => {
|
||||
.catch((errors: Record<string, string>) => {
|
||||
this.setErrors(errors);
|
||||
})
|
||||
.finally(() => this.mounted && this.setState({ isLoading: false }));
|
||||
@@ -136,10 +155,10 @@ export default class Form extends React.Component<Props, State> {
|
||||
const invalidEls: NodeListOf<InputElement> = form.querySelectorAll(
|
||||
':invalid',
|
||||
);
|
||||
const errors = {};
|
||||
const errors: Record<string, string> = {};
|
||||
invalidEls[0].focus(); // focus on first error
|
||||
|
||||
Array.from(invalidEls).reduce((acc, el: InputElement) => {
|
||||
Array.from(invalidEls).reduce((acc, el) => {
|
||||
if (!el.name) {
|
||||
logger.warn('Found an element without name', { el });
|
||||
|
||||
@@ -164,7 +183,10 @@ export default class Form extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
setErrors(errors: { [key: string]: string }) {
|
||||
this.props.form && this.props.form.setErrors(errors);
|
||||
if (hasForm(this.props)) {
|
||||
this.props.form.setErrors(errors);
|
||||
}
|
||||
|
||||
this.props.onInvalid(errors);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { MessageDescriptor } from 'react-intl';
|
||||
import i18n from 'app/services/i18n';
|
||||
|
||||
class FormComponent<P, S = {}> extends React.Component<P, S> {
|
||||
export default class FormComponent<P, S = {}> extends React.Component<P, S> {
|
||||
/**
|
||||
* Formats message resolving intl translations
|
||||
*
|
||||
@@ -37,5 +37,3 @@ class FormComponent<P, S = {}> extends React.Component<P, S> {
|
||||
*/
|
||||
onFormInvalid() {}
|
||||
}
|
||||
|
||||
export default FormComponent;
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import errorsDict from 'app/services/errorsDict';
|
||||
import React, { ComponentType, ReactNode } from 'react';
|
||||
import { resolve as resolveError } from 'app/services/errorsDict';
|
||||
import { MessageDescriptor } from 'react-intl';
|
||||
|
||||
import styles from './form.scss';
|
||||
|
||||
export default function FormError({
|
||||
error,
|
||||
}: {
|
||||
error?:
|
||||
| string
|
||||
| React.ReactNode
|
||||
| {
|
||||
type: string;
|
||||
payload: { [key: string]: any };
|
||||
};
|
||||
}) {
|
||||
return error ? (
|
||||
<div className={styles.fieldError}>{errorsDict.resolve(error)}</div>
|
||||
) : null;
|
||||
interface Props {
|
||||
error?: Parameters<typeof resolveError>[0] | MessageDescriptor | null;
|
||||
}
|
||||
|
||||
FormError.propTypes = {
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
type: PropTypes.string.isRequired,
|
||||
payload: PropTypes.object.isRequired,
|
||||
}),
|
||||
]),
|
||||
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;
|
||||
}
|
||||
|
||||
let content: ReactNode;
|
||||
|
||||
if (isMessageDescriptor(error)) {
|
||||
content = error;
|
||||
} else {
|
||||
content = resolveError(error);
|
||||
}
|
||||
|
||||
return <div className={styles.fieldError}>{content}</div>;
|
||||
};
|
||||
|
||||
export default FormError;
|
||||
|
||||
@@ -6,15 +6,13 @@ export type ValidationError =
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
payload?: { [key: string]: any };
|
||||
payload?: Record<string, any>;
|
||||
};
|
||||
|
||||
export default class FormModel {
|
||||
fields = {};
|
||||
errors: {
|
||||
[fieldId: string]: ValidationError;
|
||||
} = {};
|
||||
handlers: LoadingListener[] = [];
|
||||
fields: Record<string, any> = {};
|
||||
errors: Record<string, ValidationError> = {};
|
||||
handlers: Array<LoadingListener> = [];
|
||||
renderErrors: boolean;
|
||||
_isLoading: boolean;
|
||||
|
||||
@@ -27,7 +25,7 @@ export default class FormModel {
|
||||
this.renderErrors = options.renderErrors !== false;
|
||||
}
|
||||
|
||||
hasField(fieldId: string) {
|
||||
hasField(fieldId: string): boolean {
|
||||
return !!this.fields[fieldId];
|
||||
}
|
||||
|
||||
@@ -83,7 +81,7 @@ export default class FormModel {
|
||||
*
|
||||
* @param {string} fieldId - an id of field to focus
|
||||
*/
|
||||
focus(fieldId: string) {
|
||||
focus(fieldId: string): void {
|
||||
if (!this.fields[fieldId]) {
|
||||
throw new Error(
|
||||
`Can not focus. The field with an id ${fieldId} does not exists`,
|
||||
@@ -100,7 +98,7 @@ export default class FormModel {
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
value(fieldId: string) {
|
||||
value(fieldId: string): string {
|
||||
const field = this.fields[fieldId];
|
||||
|
||||
if (!field) {
|
||||
@@ -124,7 +122,7 @@ export default class FormModel {
|
||||
*
|
||||
* @param {object} errors - object maping {fieldId: errorType}
|
||||
*/
|
||||
setErrors(errors: { [key: string]: ValidationError }) {
|
||||
setErrors(errors: Record<string, ValidationError>): void {
|
||||
if (typeof errors !== 'object' || errors === null) {
|
||||
throw new Error('Errors must be an object');
|
||||
}
|
||||
@@ -151,21 +149,11 @@ export default class FormModel {
|
||||
return error || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error by id
|
||||
*
|
||||
* @param {string} fieldId - an id of field to get error for
|
||||
*
|
||||
* @returns {string|object|null}
|
||||
*/
|
||||
getError(fieldId: string) {
|
||||
getError(fieldId: string): ValidationError | null {
|
||||
return this.errors[fieldId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool}
|
||||
*/
|
||||
hasErrors() {
|
||||
hasErrors(): boolean {
|
||||
return Object.keys(this.errors).length > 0;
|
||||
}
|
||||
|
||||
@@ -174,7 +162,7 @@ export default class FormModel {
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
serialize(): { [key: string]: any } {
|
||||
serialize(): Record<string, any> {
|
||||
return Object.keys(this.fields).reduce((acc, fieldId) => {
|
||||
const field = this.fields[fieldId];
|
||||
|
||||
@@ -185,7 +173,7 @@ export default class FormModel {
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}, {} as Record<string, any>);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +181,7 @@ export default class FormModel {
|
||||
*
|
||||
* @param {Function} fn
|
||||
*/
|
||||
addLoadingListener(fn: LoadingListener) {
|
||||
addLoadingListener(fn: LoadingListener): void {
|
||||
this.removeLoadingListener(fn);
|
||||
this.handlers.push(fn);
|
||||
}
|
||||
@@ -203,14 +191,14 @@ export default class FormModel {
|
||||
*
|
||||
* @param {Function} fn
|
||||
*/
|
||||
removeLoadingListener(fn: LoadingListener) {
|
||||
removeLoadingListener(fn: LoadingListener): void {
|
||||
this.handlers = this.handlers.filter(handler => handler !== fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch form in loading state
|
||||
*/
|
||||
beginLoading() {
|
||||
beginLoading(): void {
|
||||
this._isLoading = true;
|
||||
this.notifyHandlers();
|
||||
}
|
||||
@@ -218,12 +206,12 @@ export default class FormModel {
|
||||
/**
|
||||
* Disable loading state
|
||||
*/
|
||||
endLoading() {
|
||||
endLoading(): void {
|
||||
this._isLoading = false;
|
||||
this.notifyHandlers();
|
||||
}
|
||||
|
||||
private notifyHandlers() {
|
||||
private notifyHandlers(): void {
|
||||
this.handlers.forEach(fn => fn(this._isLoading));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('Input', () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('input[name="test"]').getDOMNode().value,
|
||||
wrapper.find('input[name="test"]').getDOMNode<HTMLInputElement>().value,
|
||||
'to equal',
|
||||
'foo',
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { MessageDescriptor } from 'react-intl';
|
||||
import TextareaAutosize 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';
|
||||
@@ -8,22 +10,15 @@ import { SKIN_DARK, COLOR_GREEN, Skin, Color } from 'app/components/ui';
|
||||
import styles from './form.scss';
|
||||
import FormInputComponent from './FormInputComponent';
|
||||
|
||||
type TextareaAutosizeProps = {
|
||||
onHeightChange?: (number, TextareaAutosizeProps) => void;
|
||||
useCacheForDOMMeasurements?: boolean;
|
||||
minRows?: number;
|
||||
maxRows?: number;
|
||||
inputRef?: (el?: HTMLTextAreaElement) => void;
|
||||
};
|
||||
interface OwnProps {
|
||||
placeholder?: string | MessageDescriptor;
|
||||
label?: string | MessageDescriptor;
|
||||
skin: Skin;
|
||||
color: Color;
|
||||
}
|
||||
|
||||
export default class TextArea extends FormInputComponent<
|
||||
{
|
||||
placeholder?: string | MessageDescriptor;
|
||||
label?: string | MessageDescriptor;
|
||||
skin: Skin;
|
||||
color: Color;
|
||||
} & TextareaAutosizeProps &
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
OwnProps & Omit<TextareaAutosizeProps, keyof OwnProps>
|
||||
> {
|
||||
static defaultProps = {
|
||||
color: COLOR_GREEN,
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import React from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Skin } from 'app/components/ui';
|
||||
|
||||
import styles from './componentLoader.scss';
|
||||
|
||||
// TODO: add mode to not show loader until first ~150ms
|
||||
|
||||
function ComponentLoader({ skin = 'dark' }: { skin?: Skin }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.componentLoader, styles[`${skin}ComponentLoader`])}
|
||||
>
|
||||
<div className={styles.spins}>
|
||||
{new Array(5).fill(0).map((_, index) => (
|
||||
<div
|
||||
className={clsx(styles.spin, styles[`spin${index}`])}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
interface Props {
|
||||
skin?: Skin;
|
||||
}
|
||||
|
||||
const ComponentLoader: ComponentType<Props> = ({ skin = 'dark' }) => (
|
||||
<div
|
||||
className={clsx(styles.componentLoader, styles[`${skin}ComponentLoader`])}
|
||||
>
|
||||
<div className={styles.spins}>
|
||||
{new Array(5).fill(0).map((_, index) => (
|
||||
<div
|
||||
className={clsx(styles.spin, styles[`spin${index}`])}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ComponentLoader;
|
||||
|
||||
@@ -1,71 +1,64 @@
|
||||
import React from 'react';
|
||||
import React, { ComponentType, useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { ComponentLoader } from 'app/components/ui/loader';
|
||||
|
||||
import { SKIN_LIGHT } from 'app/components/ui';
|
||||
|
||||
import ComponentLoader from './ComponentLoader';
|
||||
import styles from './imageLoader.scss';
|
||||
|
||||
export default class ImageLoader extends React.Component<
|
||||
{
|
||||
src: string;
|
||||
alt: string;
|
||||
ratio: number; // width:height ratio
|
||||
onLoad?: Function;
|
||||
},
|
||||
{
|
||||
isLoading: boolean;
|
||||
}
|
||||
> {
|
||||
state = {
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.preloadImage();
|
||||
}
|
||||
|
||||
preloadImage() {
|
||||
const img = new Image();
|
||||
img.onload = () => this.imageLoaded();
|
||||
img.onerror = () => this.preloadImage();
|
||||
img.src = this.props.src;
|
||||
}
|
||||
|
||||
imageLoaded() {
|
||||
this.setState({ isLoading: false });
|
||||
|
||||
if (this.props.onLoad) {
|
||||
this.props.onLoad();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading } = this.state;
|
||||
const { src, alt, ratio } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div
|
||||
style={{
|
||||
height: 0,
|
||||
paddingBottom: `${ratio * 100}%`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<div className={styles.loader}>
|
||||
<ComponentLoader skin={SKIN_LIGHT} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(styles.image, {
|
||||
[styles.imageLoaded]: !isLoading,
|
||||
})}
|
||||
>
|
||||
<img src={src} alt={alt} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
interface Props {
|
||||
src: string;
|
||||
alt: string;
|
||||
ratio: number; // width:height ratio
|
||||
onLoad?: () => void;
|
||||
}
|
||||
|
||||
const ImageLoader: ComponentType<Props> = ({
|
||||
src,
|
||||
alt,
|
||||
ratio,
|
||||
onLoad = () => {},
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
function preloadImage() {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setIsLoading(false);
|
||||
onLoad();
|
||||
};
|
||||
img.onerror = preloadImage;
|
||||
img.src = src;
|
||||
}
|
||||
|
||||
preloadImage();
|
||||
}, [src]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div
|
||||
style={{
|
||||
height: 0,
|
||||
paddingBottom: `${ratio * 100}%`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<div className={styles.loader}>
|
||||
<ComponentLoader skin={SKIN_LIGHT} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(styles.image, {
|
||||
[styles.imageLoaded]: !isLoading,
|
||||
})}
|
||||
>
|
||||
<img src={src} alt={alt} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageLoader;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
|
||||
import MeasureHeight from 'app/components/MeasureHeight';
|
||||
|
||||
import styles from './slide-motion.scss';
|
||||
@@ -10,18 +11,19 @@ interface Props {
|
||||
}
|
||||
|
||||
interface State {
|
||||
// [stepHeight: string]: number;
|
||||
version: string;
|
||||
prevChildren: React.ReactNode | undefined;
|
||||
stepsHeights: Record<Props['activeStep'], number>;
|
||||
}
|
||||
|
||||
class SlideMotion extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
prevChildren: undefined, // to track version updates
|
||||
version: `${this.props.activeStep}.0`,
|
||||
stepsHeights: [],
|
||||
};
|
||||
|
||||
isHeightMeasured: boolean;
|
||||
private isHeightMeasured: boolean;
|
||||
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
let [, version] = state.version.split('.').map(Number);
|
||||
@@ -42,7 +44,7 @@ class SlideMotion extends React.PureComponent<Props, State> {
|
||||
|
||||
const { version } = this.state;
|
||||
|
||||
const activeStepHeight = this.state[`step${activeStep}Height`] || 0;
|
||||
const activeStepHeight = this.state.stepsHeights[activeStep] || 0;
|
||||
|
||||
// a hack to disable height animation on first render
|
||||
const { isHeightMeasured } = this;
|
||||
@@ -65,7 +67,7 @@ class SlideMotion extends React.PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<Motion style={motionStyle}>
|
||||
{(interpolatingStyle: { height: number; transform: string }) => (
|
||||
{interpolatingStyle => (
|
||||
<div
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
@@ -96,13 +98,14 @@ class SlideMotion extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
onStepMeasure(step: number) {
|
||||
return (height: number) =>
|
||||
// @ts-ignore
|
||||
this.setState({
|
||||
[`step${step}Height`]: height,
|
||||
});
|
||||
}
|
||||
onStepMeasure = (step: number) => (height: number) => {
|
||||
this.setState({
|
||||
stepsHeights: {
|
||||
...this.state.stepsHeights,
|
||||
[step]: height,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default SlideMotion;
|
||||
|
||||
@@ -5,10 +5,10 @@ import React from 'react';
|
||||
|
||||
import { shallow, mount } from 'enzyme';
|
||||
|
||||
import { PopupStack } from 'app/components/ui/popup/PopupStack';
|
||||
import PopupStack from 'app/components/ui/popup/PopupStack';
|
||||
import styles from 'app/components/ui/popup/popup.scss';
|
||||
|
||||
function DummyPopup(/** @type {{[key: string]: any}} */ _props) {
|
||||
function DummyPopup(_props: Record<string, any>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('<PopupStack />', () => {
|
||||
destroy: () => {},
|
||||
popups: [
|
||||
{
|
||||
Popup: (props = {}) => {
|
||||
Popup: (props = { onClose: Function }) => {
|
||||
// eslint-disable-next-line
|
||||
expect(props.onClose, 'to be a', 'function');
|
||||
|
||||
@@ -2,16 +2,19 @@ import React from 'react';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import { browserHistory } from 'app/services/history';
|
||||
import { connect } from 'react-redux';
|
||||
import { Location } from 'history';
|
||||
import { RootState } from 'app/reducers';
|
||||
|
||||
import { PopupConfig } from './reducer';
|
||||
import { destroy } from './actions';
|
||||
import styles from './popup.scss';
|
||||
|
||||
export class PopupStack extends React.Component<{
|
||||
interface Props {
|
||||
popups: PopupConfig[];
|
||||
destroy: (popup: PopupConfig) => void;
|
||||
}> {
|
||||
}
|
||||
|
||||
export class PopupStack extends React.Component<Props> {
|
||||
unlistenTransition: () => void;
|
||||
|
||||
componentDidMount() {
|
||||
@@ -87,7 +90,7 @@ export class PopupStack extends React.Component<{
|
||||
}
|
||||
};
|
||||
|
||||
onRouteLeave = nextLocation => {
|
||||
onRouteLeave = (nextLocation: Location) => {
|
||||
if (nextLocation) {
|
||||
this.popStack();
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { Action as ReduxPopup } from 'redux';
|
||||
import { PopupConfig } from './reducer';
|
||||
|
||||
export const POPUP_CREATE = 'POPUP_CREATE';
|
||||
export function create(payload: PopupConfig) {
|
||||
return {
|
||||
type: POPUP_CREATE,
|
||||
payload,
|
||||
};
|
||||
interface PopupCreateAction extends ReduxPopup {
|
||||
type: 'POPUP_CREATE';
|
||||
payload: PopupConfig;
|
||||
}
|
||||
|
||||
export const POPUP_DESTROY = 'POPUP_DESTROY';
|
||||
export function destroy(popup: PopupConfig) {
|
||||
export function create(popup: PopupConfig): PopupCreateAction {
|
||||
return {
|
||||
type: POPUP_DESTROY,
|
||||
type: 'POPUP_CREATE',
|
||||
payload: popup,
|
||||
};
|
||||
}
|
||||
|
||||
interface PopupDestroyAction extends ReduxPopup {
|
||||
type: 'POPUP_DESTROY';
|
||||
payload: PopupConfig;
|
||||
}
|
||||
|
||||
export function destroy(popup: PopupConfig): PopupDestroyAction {
|
||||
return {
|
||||
type: 'POPUP_DESTROY',
|
||||
payload: popup,
|
||||
};
|
||||
}
|
||||
|
||||
export type Action = PopupCreateAction | PopupDestroyAction;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import expect from 'app/test/unexpected';
|
||||
import React from 'react';
|
||||
import reducer from 'app/components/ui/popup/reducer';
|
||||
import { create, destroy } from 'app/components/ui/popup/actions';
|
||||
|
||||
import reducer, { PopupConfig, State } from './reducer';
|
||||
import { create, destroy } from './actions';
|
||||
|
||||
const FakeComponent: ComponentType = () => <span />;
|
||||
|
||||
describe('popup/reducer', () => {
|
||||
it('should have no popups by default', () => {
|
||||
@@ -61,12 +65,12 @@ describe('popup/reducer', () => {
|
||||
});
|
||||
|
||||
describe('#destroy', () => {
|
||||
let state;
|
||||
let popup;
|
||||
let state: State;
|
||||
let popup: PopupConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
state = reducer(state, create({ Popup: FakeComponent }));
|
||||
popup = state.popups[0];
|
||||
[popup] = state.popups;
|
||||
});
|
||||
|
||||
it('should remove popup', () => {
|
||||
@@ -92,7 +96,3 @@ describe('popup/reducer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function FakeComponent() {
|
||||
return <span />;
|
||||
}
|
||||
@@ -1,35 +1,33 @@
|
||||
import React from 'react';
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { POPUP_CREATE, POPUP_DESTROY } from './actions';
|
||||
import { Action } from './actions';
|
||||
|
||||
export interface PopupConfig {
|
||||
Popup: React.ElementType;
|
||||
props?: { [key: string]: any };
|
||||
// do not allow hiding popup
|
||||
props?: Record<string, any>;
|
||||
// Don't allow hiding popup
|
||||
disableOverlayClose?: boolean;
|
||||
}
|
||||
|
||||
export type State = {
|
||||
popups: PopupConfig[];
|
||||
popups: Array<PopupConfig>;
|
||||
};
|
||||
|
||||
export default combineReducers<State>({
|
||||
popups,
|
||||
});
|
||||
|
||||
function popups(state: PopupConfig[] = [], { type, payload }) {
|
||||
function popups(state: Array<PopupConfig> = [], { type, payload }: Action) {
|
||||
switch (type) {
|
||||
case POPUP_CREATE:
|
||||
case 'POPUP_CREATE':
|
||||
if (!payload.Popup) {
|
||||
throw new Error('Popup is required');
|
||||
}
|
||||
|
||||
return state.concat(payload);
|
||||
|
||||
case POPUP_DESTROY:
|
||||
case 'POPUP_DESTROY':
|
||||
return state.filter(popup => popup !== payload);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,18 @@ import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { restoreScroll } from './scroll';
|
||||
|
||||
class ScrollIntoView extends React.PureComponent<
|
||||
RouteComponentProps & {
|
||||
top?: boolean; // do not touch any DOM and simply scroll to top on location change
|
||||
}
|
||||
> {
|
||||
interface OwnProps {
|
||||
top?: boolean; // don't touch any DOM and simply scroll to top on location change
|
||||
}
|
||||
|
||||
type Props = RouteComponentProps & OwnProps;
|
||||
|
||||
class ScrollIntoView extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.onPageUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (this.props.location !== prevProps.location) {
|
||||
this.onPageUpdate();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user