Skip to content

Commit

Permalink
feat: add loading state in button and tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
ychhabra-eightfold committed Aug 24, 2022
1 parent 503f0d8 commit 38e331e
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 6 deletions.
12 changes: 10 additions & 2 deletions src/components/Button/BaseButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Icon, IconSize } from '../Icon';
import { Badge } from '../Badge';
import { Breakpoints, useMatchMedia } from '../../hooks/useMatchMedia';
import { mergeClasses } from '../../shared/utilities';
import { Loader } from '../Loader';

import styles from './button.module.scss';

Expand Down Expand Up @@ -46,6 +47,7 @@ export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
theme,
toggle,
type,
loading = false,
...rest
},
ref: Ref<HTMLButtonElement>
Expand Down Expand Up @@ -105,6 +107,7 @@ export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
iconExists && alignIcon === ButtonIconAlign.Right,
},
{ [styles.disabled]: allowDisabledFocus || disabled },
{ [styles.loading]: loading },
]);

const buttonTextClassNames: string = mergeClasses([
Expand Down Expand Up @@ -155,6 +158,9 @@ export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
return iconSize;
};

const getButtonLoader = (): JSX.Element =>
loading && <Loader classNames={styles.loader} />;

const getButtonIcon = (): JSX.Element => (
<Icon
{...iconProps}
Expand All @@ -169,9 +175,10 @@ export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
): JSX.Element => (
<span className={buttonTextClassNames}>
{text ? text : 'Button'}
{counterExists && (
{counterExists && !loading && (
<Badge classNames={badgeClassNames}>{counter}</Badge>
)}
{getButtonLoader()}
</span>
);

Expand All @@ -193,9 +200,10 @@ export const BaseButton: FC<InternalButtonProps> = React.forwardRef(
type={htmlType}
>
{iconExists && !textExists && getButtonIcon()}
{counterExists && !textExists && (
{counterExists && !textExists && !loading && (
<Badge classNames={badgeClassNames}>{counter}</Badge>
)}
{counterExists && !textExists && getButtonLoader()}
{iconExists && textExists && (
<span>
{getButtonIcon()}
Expand Down
1 change: 1 addition & 0 deletions src/components/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ const buttonArgs: Object = {
theme: ButtonTheme.light,
toggle: false,
counter: 0,
loading: false,
};

Primary.args = {
Expand Down
4 changes: 4 additions & 0 deletions src/components/Button/Button.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,8 @@ export interface ButtonProps extends NativeButtonProps {
* @default false
*/
transparent?: boolean;
/**
* If the button is in loading state
*/
loading?: boolean;
}
9 changes: 9 additions & 0 deletions src/components/Button/button.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@
cursor: not-allowed;
}

&.loading {
pointer-events: none;
cursor: not-allowed;
}

&:hover {
.counter {
background-color: var(--button-counter-hover-background-color);
Expand All @@ -178,6 +183,10 @@
&:focus-visible {
outline: none;
}

.loader {
margin-left: $space-xs;
}
}

.button-text-large {
Expand Down
40 changes: 40 additions & 0 deletions src/components/Loader/Loader.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { Stories } from '@storybook/addon-docs';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { Loader, LoaderSize } from './';

export default {
title: 'Loader',
parameters: {
docs: {
page: (): JSX.Element => (
<main>
<article>
<section>
<h1>Loader</h1>
</section>
<section>
<Stories includePrimary title="" />
</section>
</article>
</main>
),
},
},
argTypes: {
size: {
options: [LoaderSize.Large, LoaderSize.Medium, LoaderSize.Small],
control: { type: 'select' },
},
},
} as ComponentMeta<typeof Loader>;

const Loader_Story: ComponentStory<typeof Loader> = (args) => (
<Loader {...args} />
);

export const Default = Loader_Story.bind({});

Default.args = {
classNames: 'my-loader-class',
};
27 changes: 27 additions & 0 deletions src/components/Loader/Loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { FC } from 'react';
import { LoaderProps, LoaderSize } from './Loader.types';
import { Stack } from '../Stack';
import { mergeClasses } from '../../shared/utilities';

import styles from './loader.module.scss';

export const Loader: FC<LoaderProps> = ({
size = LoaderSize.Small,
...rest
}) => {
const dotClasses = mergeClasses([
styles.dot,
{
[styles.small]: size === LoaderSize.Small,
[styles.medium]: size === LoaderSize.Medium,
[styles.large]: size === LoaderSize.Large,
},
]);
return (
<Stack {...rest}>
<div className={dotClasses} />
<div className={dotClasses} />
<div className={dotClasses} />
</Stack>
);
};
11 changes: 11 additions & 0 deletions src/components/Loader/Loader.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { OcBaseProps } from '../OcBase';

export enum LoaderSize {
Large = 'large',
Medium = 'medium',
Small = 'small',
}

export interface LoaderProps extends OcBaseProps<HTMLDivElement> {
size?: LoaderSize;
}
2 changes: 2 additions & 0 deletions src/components/Loader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Loader';
export * from './Loader.types';
107 changes: 107 additions & 0 deletions src/components/Loader/loader.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
.dot {
--translate: -2px;
background: var(--primary-color);
border-radius: 50%;
margin-right: $space-xxs;

&.small {
width: 4px;
height: 4px;
}

&.medium {
--translate: -3px;
width: 6px;
height: 6px;
}

&.large {
--translate: -4px;
width: 8px;
height: 8px;
}

&:nth-child(1) {
animation: 2s linear infinite both firstBall;
}

&:nth-child(2) {
animation: 2s linear infinite both secondBall;
}

&:nth-child(3) {
animation: 2s linear infinite both thirdBall;
}
}

@keyframes firstBall {
0% {
transform: translateY(0);
}
50% {
transform: translateY(0);
}
60% {
transform: translateY(var(--translate));
}
70% {
transform: translateY(0);
}
80% {
transform: translateY(0);
}
90% {
transform: translateY(0);
}
100% {
transform: translateY(0);
}
}

@keyframes secondBall {
0% {
transform: translateY(0);
}
50% {
transform: translateY(0);
}
60% {
transform: translateY(0);
}
70% {
transform: translateY(var(--translate));
}
80% {
transform: translateY(0);
}
90% {
transform: translateY(0);
}
100% {
transform: translateY(0);
}
}

@keyframes thirdBall {
0% {
transform: translateY(0);
}
50% {
transform: translateY(0);
}
60% {
transform: translateY(0);
}
70% {
transform: translateY(0);
}
80% {
transform: translateY(var(--translate));
}
90% {
transform: translateY(0);
}
100% {
transform: translateY(0);
}
}
5 changes: 3 additions & 2 deletions src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const Wrapper: FC<SelectProps> = ({ children }) => {
);
};

const DynamicSelect: FC<SelectProps> = () => {
const DynamicSelect: FC<SelectProps> = (args) => {
const timer = useRef<ReturnType<typeof setTimeout>>(null);
const [options, setOptions] = useState([]);
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -139,11 +139,12 @@ const DynamicSelect: FC<SelectProps> = () => {
};
return (
<Select
{...args}
filterable={true}
isLoading={isLoading}
loadOptions={loadOptions}
options={options}
></Select>
/>
);
};

Expand Down
9 changes: 8 additions & 1 deletion src/components/Tabs/Tab/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useConfig } from '../../ConfigProvider';
import { Badge } from '../../Badge';

import styles from '../tabs.module.scss';
import { Loader } from '../../Loader';

export const Tab: FC<TabProps> = React.forwardRef(
(
Expand All @@ -20,6 +21,7 @@ export const Tab: FC<TabProps> = React.forwardRef(
ariaLabel,
badgeContent,
classNames,
loading,
...rest
},
ref: Ref<HTMLButtonElement>
Expand Down Expand Up @@ -53,12 +55,16 @@ export const Tab: FC<TabProps> = React.forwardRef(
);

const getBadge = (): JSX.Element =>
!!badgeContent && (
!!badgeContent &&
!loading && (
<Badge active={isActive} classNames={styles.badge}>
{badgeContent}
</Badge>
);

const getLoader = (): JSX.Element =>
loading && <Loader classNames={styles.loader} />;

return (
<button
{...rest}
Expand All @@ -74,6 +80,7 @@ export const Tab: FC<TabProps> = React.forwardRef(
{getLabel()}
{getTabIndicator()}
{getBadge()}
{getLoader()}
</button>
);
}
Expand Down
22 changes: 22 additions & 0 deletions src/components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ const Default_Story: ComponentStory<typeof Tabs> = (args) => {

export const Default = Default_Story.bind({});

const Default_Loading_Story: ComponentStory<typeof Tabs> = (args) => {
const [activeTabs, setActiveTabs] = useState({ defaultTab: 'tab1' });
return (
<Tabs
{...args}
onChange={(tab) =>
setActiveTabs({ ...activeTabs, defaultTab: tab })
}
value={activeTabs.defaultTab}
/>
);
};

export const DefaultLoader = Default_Loading_Story.bind({});

const Small_Story: ComponentStory<typeof Tabs> = (args) => {
const [activeTabs, setActiveTabs] = useState({ defaultTab: 'tab1' });
return (
Expand Down Expand Up @@ -245,6 +260,13 @@ Default.args = {
...tabsArgs,
};

DefaultLoader.args = {
...tabsArgs,
children: badgeTabs.map((tab, index) => (
<Tab key={tab.value} {...tab} loading={index % 2 === 0} />
)),
};

Small.args = {
...tabsArgs,
variant: TabVariant.small,
Expand Down
Loading

0 comments on commit 38e331e

Please sign in to comment.