mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 11:55:03 +03:00
Made LoadingIndicator
component more flexible (#18145)
refs https://github.com/TryGhost/Product/issues/3849 - Added small and medium variants of `LoadingIndicator` component - Added `Loading` prop to `Button` component, which uses `LoadingIndicator` - Updated instances of `Modal` component so they use the new `LoadingIndicator`
This commit is contained in:
parent
a79de45392
commit
82e9aae4cc
@ -81,4 +81,12 @@ export const IconSmall: Story = {
|
|||||||
color: 'green',
|
color: 'green',
|
||||||
iconColorClass: 'text-white'
|
iconColorClass: 'text-white'
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
args: {
|
||||||
|
loading: true,
|
||||||
|
color: 'green',
|
||||||
|
label: 'Button'
|
||||||
|
}
|
||||||
};
|
};
|
@ -1,5 +1,6 @@
|
|||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
import React, {HTMLProps} from 'react';
|
import React, {HTMLProps} from 'react';
|
||||||
|
import {LoadingIndicator, LoadingIndicatorColor, LoadingIndicatorSize} from './LoadingIndicator';
|
||||||
|
|
||||||
export type ButtonColor = 'clear' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline';
|
export type ButtonColor = 'clear' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline';
|
||||||
export type ButtonSize = 'sm' | 'md';
|
export type ButtonSize = 'sm' | 'md';
|
||||||
@ -18,6 +19,9 @@ export interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, 'label'
|
|||||||
unstyled?: boolean;
|
unstyled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
loadingIndicatorSize?: LoadingIndicatorSize;
|
||||||
|
loadingIndicatorColor?: LoadingIndicatorColor;
|
||||||
onClick?: (e?:React.MouseEvent<HTMLElement>) => void;
|
onClick?: (e?:React.MouseEvent<HTMLElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,6 +38,8 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
unstyled = false,
|
unstyled = false,
|
||||||
className = '',
|
className = '',
|
||||||
tag = 'button',
|
tag = 'button',
|
||||||
|
loading = false,
|
||||||
|
loadingIndicatorColor,
|
||||||
onClick,
|
onClick,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
@ -51,24 +57,31 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
switch (color) {
|
switch (color) {
|
||||||
case 'black':
|
case 'black':
|
||||||
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-black text-white dark:bg-white dark:text-black ${!disabled && 'hover:bg-grey-900'}`;
|
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-black text-white dark:bg-white dark:text-black ${!disabled && 'hover:bg-grey-900'}`;
|
||||||
|
loadingIndicatorColor = 'light';
|
||||||
break;
|
break;
|
||||||
case 'grey':
|
case 'grey':
|
||||||
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`;
|
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`;
|
||||||
|
loadingIndicatorColor = 'dark';
|
||||||
break;
|
break;
|
||||||
case 'green':
|
case 'green':
|
||||||
styles += link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`;
|
styles += link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`;
|
||||||
|
loadingIndicatorColor = 'light';
|
||||||
break;
|
break;
|
||||||
case 'red':
|
case 'red':
|
||||||
styles += link ? ' text-red hover:text-red-400' : ` bg-red text-white ${!disabled && 'hover:bg-red-400'}`;
|
styles += link ? ' text-red hover:text-red-400' : ` bg-red text-white ${!disabled && 'hover:bg-red-400'}`;
|
||||||
|
loadingIndicatorColor = 'light';
|
||||||
break;
|
break;
|
||||||
case 'white':
|
case 'white':
|
||||||
styles += link ? ' text-white hover:text-white dark:text-black dark:hover:text-grey-800' : ` bg-white dark:bg-black text-black dark:text-white`;
|
styles += link ? ' text-white hover:text-white dark:text-black dark:hover:text-grey-800' : ` bg-white dark:bg-black text-black dark:text-white`;
|
||||||
|
loadingIndicatorColor = 'dark';
|
||||||
break;
|
break;
|
||||||
case 'outline':
|
case 'outline':
|
||||||
styles += link ? ' text-black dark:text-white hover:text-grey-800' : `text-black border border-grey-300 bg-transparent dark:border-grey-800 dark:text-white ${!disabled && 'hover:!border-black dark:hover:!border-white'}`;
|
styles += link ? ' text-black dark:text-white hover:text-grey-800' : `text-black border border-grey-300 bg-transparent dark:border-grey-800 dark:text-white ${!disabled && 'hover:!border-black dark:hover:!border-white'}`;
|
||||||
|
loadingIndicatorColor = 'dark';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` text-black dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200'}`;
|
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` text-black dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200'}`;
|
||||||
|
loadingIndicatorColor = 'dark';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,10 +93,16 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
|
|
||||||
const iconClasses = label && icon && !hideLabel ? 'mr-1.5' : '';
|
const iconClasses = label && icon && !hideLabel ? 'mr-1.5' : '';
|
||||||
|
|
||||||
|
let labelClasses = '';
|
||||||
|
labelClasses += (label && hideLabel) ? 'sr-only' : '';
|
||||||
|
labelClasses += loading ? 'invisible' : '';
|
||||||
|
|
||||||
const buttonChildren = <>
|
const buttonChildren = <>
|
||||||
{icon && <Icon className={iconClasses} colorClass={iconColorClass} name={icon} size={size === 'sm' ? 'sm' : 'md'} />}
|
{icon && <Icon className={iconClasses} colorClass={iconColorClass} name={icon} size={size === 'sm' ? 'sm' : 'md'} />}
|
||||||
{(label && hideLabel) ? <span className="sr-only">{label}</span> : label}
|
<span className={labelClasses}>{label}</span>
|
||||||
|
{loading && <div className='absolute flex'><LoadingIndicator color={loadingIndicatorColor} size={size}/><span className='sr-only'>Loading...</span></div>}
|
||||||
</>;
|
</>;
|
||||||
|
|
||||||
const buttonElement = React.createElement(tag, {className: styles,
|
const buttonElement = React.createElement(tag, {className: styles,
|
||||||
disabled: disabled,
|
disabled: disabled,
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
@ -1,21 +1,66 @@
|
|||||||
import type {Meta, StoryObj} from '@storybook/react';
|
import type {Meta, StoryObj} from '@storybook/react';
|
||||||
|
|
||||||
import {CenteredLoadingIndicator} from './LoadingIndicator';
|
import {LoadingIndicator} from './LoadingIndicator';
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Global / Loading indicator',
|
title: 'Global / Loading indicator',
|
||||||
component: CenteredLoadingIndicator,
|
component: LoadingIndicator,
|
||||||
tags: ['autodocs']
|
tags: ['autodocs']
|
||||||
} satisfies Meta<typeof CenteredLoadingIndicator>;
|
} satisfies Meta<typeof LoadingIndicator>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof CenteredLoadingIndicator>;
|
type Story = StoryObj<typeof LoadingIndicator>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
delay: 1000,
|
delay: 1000,
|
||||||
style: {
|
style: {
|
||||||
height: '400px'
|
height: '400px'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Small: Story = {
|
||||||
|
args: {
|
||||||
|
delay: 1000,
|
||||||
|
size: 'sm',
|
||||||
|
color: 'dark',
|
||||||
|
style: {
|
||||||
|
height: '400px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Medium: Story = {
|
||||||
|
args: {
|
||||||
|
delay: 1000,
|
||||||
|
size: 'md',
|
||||||
|
color: 'dark',
|
||||||
|
style: {
|
||||||
|
height: '400px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large: Story = {
|
||||||
|
args: {
|
||||||
|
delay: 1000,
|
||||||
|
size: 'lg',
|
||||||
|
color: 'dark',
|
||||||
|
style: {
|
||||||
|
height: '400px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LightColor: Story = {
|
||||||
|
args: {
|
||||||
|
delay: 1000,
|
||||||
|
size: 'lg',
|
||||||
|
color: 'light',
|
||||||
|
style: {
|
||||||
|
height: '400px',
|
||||||
|
backgroundColor: 'tomato'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -1,19 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export const LoadingIndicator: React.FC = () => {
|
export type LoadingIndicatorSize = 'sm' | 'md' | 'lg';
|
||||||
return (
|
export type LoadingIndicatorColor = 'light' | 'dark';
|
||||||
<div>
|
|
||||||
<div className="relative mx-0 my-[-0.5] box-border inline-block h-[50px] w-[50px] animate-spin rounded-full border border-[rgba(0,0,0,0.1)] before:z-10 before:mt-[7px] before:block before:h-[7px] before:w-[7px] before:rounded-full before:bg-[#4C5156] before:content-['']"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type CenteredLoadingIndicatorProps = {
|
type LoadingIndicatorProps = {
|
||||||
|
size?: LoadingIndicatorSize;
|
||||||
|
color?: LoadingIndicatorColor;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CenteredLoadingIndicator: React.FC<CenteredLoadingIndicatorProps> = ({delay, style}) => {
|
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({size, color, delay, style}) => {
|
||||||
const [show, setShow] = React.useState(!delay);
|
const [show, setShow] = React.useState(!delay);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -27,9 +24,40 @@ export const CenteredLoadingIndicator: React.FC<CenteredLoadingIndicatorProps> =
|
|||||||
}
|
}
|
||||||
}, [delay]);
|
}, [delay]);
|
||||||
|
|
||||||
return (
|
let styles = `relative mx-0 my-[-0.5] box-border inline-block animate-spin rounded-full before:z-10 before:block before:rounded-full before:content-[''] `;
|
||||||
<div className={`flex h-64 items-center justify-center transition-opacity ${show ? 'opacity-100' : 'opacity-0'}`} style={style}>
|
|
||||||
<LoadingIndicator />
|
switch (size) {
|
||||||
</div>
|
case 'sm':
|
||||||
);
|
styles += ' h-[16px] w-[16px] border-2 before:mt-[10px] before:h-[3px] before:w-[3px] ';
|
||||||
};
|
break;
|
||||||
|
case 'md':
|
||||||
|
styles += ' h-[20px] w-[20px] border-2 before:mt-[13px] before:h-[3px] before:w-[3px] ';
|
||||||
|
break;
|
||||||
|
case 'lg':
|
||||||
|
default:
|
||||||
|
styles += ' h-[50px] w-[50px] border before:mt-[7px] before:h-[7px] before:w-[7px] ';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (color) {
|
||||||
|
case 'light':
|
||||||
|
styles += ' border-white/20 before:bg-white ';
|
||||||
|
break;
|
||||||
|
case 'dark':
|
||||||
|
default:
|
||||||
|
styles += ' border-black/10 before:bg-black ';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size === 'lg') {
|
||||||
|
return (
|
||||||
|
<div className={`flex h-64 items-center justify-center transition-opacity ${show ? 'opacity-100' : 'opacity-0'}`} style={style}>
|
||||||
|
<div className={styles}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={styles}></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -30,6 +30,22 @@ const tableRows = (
|
|||||||
<TableCell>Jamie Larson</TableCell>
|
<TableCell>Jamie Larson</TableCell>
|
||||||
<TableCell>jamie@example.com</TableCell>
|
<TableCell>jamie@example.com</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Jamie Larson</TableCell>
|
||||||
|
<TableCell>jamie@example.com</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Jamie Larson</TableCell>
|
||||||
|
<TableCell>jamie@example.com</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Jamie Larson</TableCell>
|
||||||
|
<TableCell>jamie@example.com</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Jamie Larson</TableCell>
|
||||||
|
<TableCell>jamie@example.com</TableCell>
|
||||||
|
</TableRow>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -42,3 +58,19 @@ export const Default: Story = {
|
|||||||
},
|
},
|
||||||
decorators: [(_story: () => ReactNode) => (<div style={{maxWidth: '600px'}}>{_story()}</div>)]
|
decorators: [(_story: () => ReactNode) => (<div style={{maxWidth: '600px'}}>{_story()}</div>)]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithPageTitle: Story = {
|
||||||
|
args: {
|
||||||
|
pageTitle: 'This is a page title',
|
||||||
|
children: tableRows
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
args: {
|
||||||
|
children: tableRows,
|
||||||
|
isLoading: true,
|
||||||
|
hint: 'This is a hint',
|
||||||
|
hintSeparator: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -4,7 +4,7 @@ import Pagination from './Pagination';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Separator from './Separator';
|
import Separator from './Separator';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {CenteredLoadingIndicator} from './LoadingIndicator';
|
import {LoadingIndicator} from './LoadingIndicator';
|
||||||
import {PaginationData} from '../../hooks/usePagination';
|
import {PaginationData} from '../../hooks/usePagination';
|
||||||
|
|
||||||
interface TableProps {
|
interface TableProps {
|
||||||
@ -69,6 +69,7 @@ const Table: React.FC<TableProps> = ({children, borderTop, hint, hintSeparator,
|
|||||||
<>
|
<>
|
||||||
<div className='w-full overflow-x-auto'>
|
<div className='w-full overflow-x-auto'>
|
||||||
{pageTitle && <Heading>{pageTitle}</Heading>}
|
{pageTitle && <Heading>{pageTitle}</Heading>}
|
||||||
|
|
||||||
{/* TODO: make this div have the same height across all pages */}
|
{/* TODO: make this div have the same height across all pages */}
|
||||||
<div>
|
<div>
|
||||||
{!isLoading && <table ref={table} className={tableClasses}>
|
{!isLoading && <table ref={table} className={tableClasses}>
|
||||||
@ -76,12 +77,14 @@ const Table: React.FC<TableProps> = ({children, borderTop, hint, hintSeparator,
|
|||||||
{children}
|
{children}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>}
|
</table>}
|
||||||
{isLoading && <CenteredLoadingIndicator delay={200} style={loadingStyle} />}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isLoading && <LoadingIndicator delay={200} size='lg' style={loadingStyle} />}
|
||||||
|
|
||||||
{(hint || pagination) &&
|
{(hint || pagination) &&
|
||||||
<div className="-mt-px">
|
<div className="-mt-px">
|
||||||
{(hintSeparator || pagination) && <Separator />}
|
{(hintSeparator || pagination) && <Separator />}
|
||||||
<div className="flex flex-col-reverse items-start justify-between gap-1 pt-2 md:flex-row md:items-center md:gap-0 md:pt-0 ">
|
<div className="flex flex-col-reverse items-start justify-between gap-1 pt-2 md:flex-row md:items-center md:gap-0 md:pt-0">
|
||||||
<Hint>{hint ?? ' '}</Hint>
|
<Hint>{hint ?? ' '}</Hint>
|
||||||
<OptionalPagination pagination={pagination} />
|
<OptionalPagination pagination={pagination} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,6 +21,7 @@ export interface ModalProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
okLabel?: string;
|
okLabel?: string;
|
||||||
okColor?: ButtonColor;
|
okColor?: ButtonColor;
|
||||||
|
okLoading?: boolean;
|
||||||
cancelLabel?: string;
|
cancelLabel?: string;
|
||||||
leftButtonProps?: ButtonProps;
|
leftButtonProps?: ButtonProps;
|
||||||
buttonsDisabled?: boolean;
|
buttonsDisabled?: boolean;
|
||||||
@ -48,6 +49,7 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
testId,
|
testId,
|
||||||
title,
|
title,
|
||||||
okLabel = 'OK',
|
okLabel = 'OK',
|
||||||
|
okLoading = false,
|
||||||
cancelLabel = 'Cancel',
|
cancelLabel = 'Cancel',
|
||||||
footer,
|
footer,
|
||||||
leftButtonProps,
|
leftButtonProps,
|
||||||
@ -137,7 +139,8 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
color: okColor,
|
color: okColor,
|
||||||
className: 'min-w-[80px]',
|
className: 'min-w-[80px]',
|
||||||
onClick: onOk,
|
onClick: onOk,
|
||||||
disabled: buttonsDisabled
|
disabled: buttonsDisabled,
|
||||||
|
loading: okLoading
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,9 +106,10 @@ const AddRecommendationModal: React.FC<AddRecommendationModalProps> = ({recommen
|
|||||||
});
|
});
|
||||||
|
|
||||||
let okLabel = 'Next';
|
let okLabel = 'Next';
|
||||||
|
let loadingState = false;
|
||||||
|
|
||||||
if (saveState === 'saving') {
|
if (saveState === 'saving') {
|
||||||
okLabel = 'Checking...';
|
loadingState = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
@ -120,6 +121,7 @@ const AddRecommendationModal: React.FC<AddRecommendationModalProps> = ({recommen
|
|||||||
backDropClick={false}
|
backDropClick={false}
|
||||||
okColor='black'
|
okColor='black'
|
||||||
okLabel={okLabel}
|
okLabel={okLabel}
|
||||||
|
okLoading={loadingState}
|
||||||
size='sm'
|
size='sm'
|
||||||
testId='add-recommendation-modal'
|
testId='add-recommendation-modal'
|
||||||
title='Add recommendation'
|
title='Add recommendation'
|
||||||
|
@ -41,9 +41,10 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
|
|||||||
});
|
});
|
||||||
|
|
||||||
let okLabel = 'Add';
|
let okLabel = 'Add';
|
||||||
|
let loadingState = false;
|
||||||
|
|
||||||
if (saveState === 'saving') {
|
if (saveState === 'saving') {
|
||||||
okLabel = 'Adding...';
|
loadingState = true;
|
||||||
} else if (saveState === 'saved') {
|
} else if (saveState === 'saved') {
|
||||||
okLabel = 'Added';
|
okLabel = 'Added';
|
||||||
}
|
}
|
||||||
@ -81,6 +82,7 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
|
|||||||
leftButtonProps={leftButtonProps}
|
leftButtonProps={leftButtonProps}
|
||||||
okColor='black'
|
okColor='black'
|
||||||
okLabel={okLabel}
|
okLabel={okLabel}
|
||||||
|
okLoading={loadingState}
|
||||||
size='sm'
|
size='sm'
|
||||||
testId='add-recommendation-modal'
|
testId='add-recommendation-modal'
|
||||||
title={'Add recommendation'}
|
title={'Add recommendation'}
|
||||||
|
Loading…
Reference in New Issue
Block a user