Skip to content

Commit

Permalink
feat(button): add loader minimal display interval (#634)
Browse files Browse the repository at this point in the history
* feat(button): add loader minimal display interval

* test(button): add some tests
  • Loading branch information
dmitrsavk committed May 7, 2021
1 parent 5945bd8 commit d2f7edc
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 18 deletions.
22 changes: 16 additions & 6 deletions packages/button/src/Component.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -270,29 +270,39 @@ import { Button } from '@alfalab/core-components-button';

С помощью свойства `loading` можно отобразить состояние загрузки.

Минимальное время отображения лоадера - 500мс, чтобы при быстрых ответах от сервера кнопка не «моргала».

<Preview>
{React.createElement(() => {
const [loading, setLoading] = React.useState({
primary: false,
secondary: false
});
const handleClick = (buttonName) => {
const handleClick = (buttonName, timeout) => {
setLoading({...loading, [buttonName]: true});
setTimeout(() => {
setLoading({...loading, [buttonName]: false});
}, 500)
}, timeout)
}
return (
<Container>
<Row align="middle">
<Col>
<Button view="primary" loading={loading.primary} onClick={() => handleClick('primary')}>
Отправить запрос
<Button
view="primary"
loading={loading.primary}
onClick={() => handleClick('primary', 30)}
>
Отправить быстрый запрос (30мс)
</Button>
</Col>
<Col>
<Button view="secondary" loading={loading.secondary} onClick={() => handleClick('secondary')}>
Отправить запрос
<Button
view="secondary"
loading={loading.secondary}
onClick={() => handleClick('secondary', 1500)}
>
Отправить долгий запрос (1500мс)
</Button>
</Col>
</Row>
Expand Down
81 changes: 73 additions & 8 deletions packages/button/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import React, { MouseEvent } from 'react';
import { render, fireEvent } from '@testing-library/react';
import React, { MouseEvent, useState, FC } from 'react';
import { render, fireEvent, waitFor, waitForElementToBeRemoved } from '@testing-library/react';

import { Button } from './index';
import { Button, ButtonProps, LOADER_MIN_DISPLAY_INTERVAL } from './index';

const dataTestId = 'test-id';

const ButtonWithLoader: FC<ButtonProps & { timeout: number }> = ({ timeout, ...restProps }) => {
const [loading, setLoading] = useState(false);

return (
<Button
{...restProps}
loading={loading}
onClick={() => {
setLoading(true);

setTimeout(() => {
setLoading(false);
}, timeout);
}}
>
Button
</Button>
);
};

describe('Button', () => {
describe('Snapshots tests', () => {
Expand Down Expand Up @@ -36,7 +58,6 @@ describe('Button', () => {

describe('Attributes tests', () => {
it('should set `data-test-id` attribute', () => {
const dataTestId = 'test-id';
const { getByTestId } = render(<Button dataTestId={dataTestId} />);

expect(getByTestId(dataTestId).tagName).toBe('BUTTON');
Expand Down Expand Up @@ -106,7 +127,7 @@ describe('Button', () => {
describe('Callbacks tests', () => {
it('should call `onClick` prop', () => {
const cb = jest.fn();
const dataTestId = 'test-id';

const { getByTestId } = render(<Button onClick={cb} dataTestId={dataTestId} />);

fireEvent.click(getByTestId(dataTestId));
Expand All @@ -116,7 +137,7 @@ describe('Button', () => {

it('should not call `onClick` prop if disabled', () => {
const cb = jest.fn();
const dataTestId = 'test-id';

const { getByTestId } = render(
<Button onClick={cb} dataTestId={dataTestId} disabled={true} />,
);
Expand All @@ -131,7 +152,6 @@ describe('Button', () => {
* Если тест скомпилился - то все ок.
*/
it('target should contain button element', () => {
const dataTestId = 'test-id';
const { getByTestId } = render(<Button onClick={cb} dataTestId={dataTestId} />);

const buttonNode = getByTestId(dataTestId);
Expand All @@ -148,7 +168,6 @@ describe('Button', () => {
* Если тест скомпилился - то все ок.
*/
it('target should contain anchor element', () => {
const dataTestId = 'test-id';
const { getByTestId } = render(
<Button onClick={cb} dataTestId={dataTestId} href='#' />,
);
Expand All @@ -163,6 +182,52 @@ describe('Button', () => {
});
});

describe('Loader tests', () => {
it('should keep loader during LOADER_MIN_DISPLAY_INTERVAL ms', async () => {
const { getByTestId, container } = render(
<ButtonWithLoader timeout={100} dataTestId={dataTestId} />,
);

const button = getByTestId(dataTestId);
const getLoader = () => container.querySelector('svg');

const start = Date.now();

fireEvent.click(button);

await waitFor(() => expect(getLoader()).toBeInTheDocument());

await waitForElementToBeRemoved(getLoader());

const duration = Date.now() - start;

expect(duration).toBeGreaterThanOrEqual(LOADER_MIN_DISPLAY_INTERVAL);
});

it('should keep loader during TIMEOUT ms, if TIMEOUT > LOADER_MIN_DISPLAY_INTERVAL', async () => {
const TIMEOUT = LOADER_MIN_DISPLAY_INTERVAL + 500;

const { getByTestId, container } = render(
<ButtonWithLoader timeout={TIMEOUT} dataTestId={dataTestId} />,
);

const button = getByTestId(dataTestId);
const getLoader = () => container.querySelector('svg');

const start = Date.now();

fireEvent.click(button);

await waitFor(() => expect(getLoader()).toBeInTheDocument());

await waitForElementToBeRemoved(getLoader());

const duration = Date.now() - start;

expect(duration).toBeGreaterThanOrEqual(TIMEOUT);
});
});

it('should unmount without errors', () => {
const { unmount } = render(
<Button leftAddons={<span>Left</span>} rightAddons={<span>Right</span>}>
Expand Down
45 changes: 41 additions & 4 deletions packages/button/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, useRef } from 'react';
import React, {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
useEffect,
useRef,
useState,
} from 'react';
import cn from 'classnames';
import mergeRefs from 'react-merge-refs';

Expand Down Expand Up @@ -61,8 +67,15 @@ export type ComponentProps = {

type AnchorButtonProps = ComponentProps & AnchorHTMLAttributes<HTMLAnchorElement>;
type NativeButtonProps = ComponentProps & ButtonHTMLAttributes<HTMLButtonElement>;

export type ButtonProps = Partial<AnchorButtonProps | NativeButtonProps>;

/**
* Минимальное время отображения лоадера - 500мс,
* чтобы при быстрых ответах от сервера кнопка не «моргала».
*/
export const LOADER_MIN_DISPLAY_INTERVAL = 500;

export const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonProps>(
(
{
Expand All @@ -85,6 +98,12 @@ export const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement, Bu

const [focused] = useFocus(buttonRef, 'keyboard');

const [loaderTimePassed, setLoaderTimePassed] = useState(true);

const timerId = useRef(0);

const showLoader = loading || !loaderTimePassed;

const componentProps = {
className: cn(
styles.component,
Expand All @@ -95,7 +114,7 @@ export const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement, Bu
[styles.block]: block,
[styles.iconOnly]: !children,
[styles.nowrap]: nowrap,
[styles.loading]: loading,
[styles.loading]: showLoader,
},
className,
),
Expand All @@ -114,11 +133,29 @@ export const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement, Bu
{children}
</span>
)}
{loading && <Loader className={styles.loader} />}

{showLoader && <Loader className={styles.loader} />}

{rightAddons && <span className={styles.addons}>{rightAddons}</span>}
</React.Fragment>
);

useEffect(() => {
if (loading) {
setLoaderTimePassed(false);

timerId.current = window.setTimeout(() => {
setLoaderTimePassed(true);
}, LOADER_MIN_DISPLAY_INTERVAL);
}
}, [loading]);

useEffect(() => {
return () => {
window.clearTimeout(timerId.current);
};
}, []);

if (href) {
const { target } = restProps as AnchorHTMLAttributes<HTMLAnchorElement>;

Expand Down Expand Up @@ -146,7 +183,7 @@ export const Button = React.forwardRef<HTMLAnchorElement | HTMLButtonElement, Bu
{...restButtonProps}
// eslint-disable-next-line react/button-has-type
type={type}
disabled={disabled || loading}
disabled={disabled || showLoader}
ref={mergeRefs([buttonRef, ref])}
>
{buttonChildren}
Expand Down

0 comments on commit d2f7edc

Please sign in to comment.