mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +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',
|
||||
iconColorClass: 'text-white'
|
||||
}
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
loading: true,
|
||||
color: 'green',
|
||||
label: 'Button'
|
||||
}
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import Icon from './Icon';
|
||||
import React, {HTMLProps} from 'react';
|
||||
import {LoadingIndicator, LoadingIndicatorColor, LoadingIndicatorSize} from './LoadingIndicator';
|
||||
|
||||
export type ButtonColor = 'clear' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline';
|
||||
export type ButtonSize = 'sm' | 'md';
|
||||
@ -18,6 +19,9 @@ export interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, 'label'
|
||||
unstyled?: boolean;
|
||||
className?: string;
|
||||
tag?: string;
|
||||
loading?: boolean;
|
||||
loadingIndicatorSize?: LoadingIndicatorSize;
|
||||
loadingIndicatorColor?: LoadingIndicatorColor;
|
||||
onClick?: (e?:React.MouseEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
@ -34,6 +38,8 @@ const Button: React.FC<ButtonProps> = ({
|
||||
unstyled = false,
|
||||
className = '',
|
||||
tag = 'button',
|
||||
loading = false,
|
||||
loadingIndicatorColor,
|
||||
onClick,
|
||||
...props
|
||||
}) => {
|
||||
@ -51,24 +57,31 @@ const Button: React.FC<ButtonProps> = ({
|
||||
switch (color) {
|
||||
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'}`;
|
||||
loadingIndicatorColor = 'light';
|
||||
break;
|
||||
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'}`;
|
||||
loadingIndicatorColor = 'dark';
|
||||
break;
|
||||
case 'green':
|
||||
styles += link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`;
|
||||
loadingIndicatorColor = 'light';
|
||||
break;
|
||||
case 'red':
|
||||
styles += link ? ' text-red hover:text-red-400' : ` bg-red text-white ${!disabled && 'hover:bg-red-400'}`;
|
||||
loadingIndicatorColor = 'light';
|
||||
break;
|
||||
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`;
|
||||
loadingIndicatorColor = 'dark';
|
||||
break;
|
||||
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'}`;
|
||||
loadingIndicatorColor = 'dark';
|
||||
break;
|
||||
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'}`;
|
||||
loadingIndicatorColor = 'dark';
|
||||
break;
|
||||
}
|
||||
|
||||
@ -80,10 +93,16 @@ const Button: React.FC<ButtonProps> = ({
|
||||
|
||||
const iconClasses = label && icon && !hideLabel ? 'mr-1.5' : '';
|
||||
|
||||
let labelClasses = '';
|
||||
labelClasses += (label && hideLabel) ? 'sr-only' : '';
|
||||
labelClasses += loading ? 'invisible' : '';
|
||||
|
||||
const buttonChildren = <>
|
||||
{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,
|
||||
disabled: disabled,
|
||||
type: 'button',
|
||||
|
@ -1,21 +1,66 @@
|
||||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import {CenteredLoadingIndicator} from './LoadingIndicator';
|
||||
import {LoadingIndicator} from './LoadingIndicator';
|
||||
|
||||
const meta = {
|
||||
title: 'Global / Loading indicator',
|
||||
component: CenteredLoadingIndicator,
|
||||
component: LoadingIndicator,
|
||||
tags: ['autodocs']
|
||||
} satisfies Meta<typeof CenteredLoadingIndicator>;
|
||||
} satisfies Meta<typeof LoadingIndicator>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CenteredLoadingIndicator>;
|
||||
type Story = StoryObj<typeof LoadingIndicator>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
delay: 1000,
|
||||
delay: 1000,
|
||||
style: {
|
||||
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';
|
||||
|
||||
export const LoadingIndicator: React.FC = () => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
export type LoadingIndicatorSize = 'sm' | 'md' | 'lg';
|
||||
export type LoadingIndicatorColor = 'light' | 'dark';
|
||||
|
||||
type CenteredLoadingIndicatorProps = {
|
||||
type LoadingIndicatorProps = {
|
||||
size?: LoadingIndicatorSize;
|
||||
color?: LoadingIndicatorColor;
|
||||
delay?: number;
|
||||
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);
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -27,9 +24,40 @@ export const CenteredLoadingIndicator: React.FC<CenteredLoadingIndicatorProps> =
|
||||
}
|
||||
}, [delay]);
|
||||
|
||||
return (
|
||||
<div className={`flex h-64 items-center justify-center transition-opacity ${show ? 'opacity-100' : 'opacity-0'}`} style={style}>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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-[''] `;
|
||||
|
||||
switch (size) {
|
||||
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@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>
|
||||
<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>)]
|
||||
};
|
||||
|
||||
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 Separator from './Separator';
|
||||
import clsx from 'clsx';
|
||||
import {CenteredLoadingIndicator} from './LoadingIndicator';
|
||||
import {LoadingIndicator} from './LoadingIndicator';
|
||||
import {PaginationData} from '../../hooks/usePagination';
|
||||
|
||||
interface TableProps {
|
||||
@ -69,6 +69,7 @@ const Table: React.FC<TableProps> = ({children, borderTop, hint, hintSeparator,
|
||||
<>
|
||||
<div className='w-full overflow-x-auto'>
|
||||
{pageTitle && <Heading>{pageTitle}</Heading>}
|
||||
|
||||
{/* TODO: make this div have the same height across all pages */}
|
||||
<div>
|
||||
{!isLoading && <table ref={table} className={tableClasses}>
|
||||
@ -76,12 +77,14 @@ const Table: React.FC<TableProps> = ({children, borderTop, hint, hintSeparator,
|
||||
{children}
|
||||
</tbody>
|
||||
</table>}
|
||||
{isLoading && <CenteredLoadingIndicator delay={200} style={loadingStyle} />}
|
||||
</div>
|
||||
|
||||
{isLoading && <LoadingIndicator delay={200} size='lg' style={loadingStyle} />}
|
||||
|
||||
{(hint || pagination) &&
|
||||
<div className="-mt-px">
|
||||
{(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>
|
||||
<OptionalPagination pagination={pagination} />
|
||||
</div>
|
||||
|
@ -21,6 +21,7 @@ export interface ModalProps {
|
||||
title?: string;
|
||||
okLabel?: string;
|
||||
okColor?: ButtonColor;
|
||||
okLoading?: boolean;
|
||||
cancelLabel?: string;
|
||||
leftButtonProps?: ButtonProps;
|
||||
buttonsDisabled?: boolean;
|
||||
@ -48,6 +49,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
testId,
|
||||
title,
|
||||
okLabel = 'OK',
|
||||
okLoading = false,
|
||||
cancelLabel = 'Cancel',
|
||||
footer,
|
||||
leftButtonProps,
|
||||
@ -137,7 +139,8 @@ const Modal: React.FC<ModalProps> = ({
|
||||
color: okColor,
|
||||
className: 'min-w-[80px]',
|
||||
onClick: onOk,
|
||||
disabled: buttonsDisabled
|
||||
disabled: buttonsDisabled,
|
||||
loading: okLoading
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -106,9 +106,10 @@ const AddRecommendationModal: React.FC<AddRecommendationModalProps> = ({recommen
|
||||
});
|
||||
|
||||
let okLabel = 'Next';
|
||||
let loadingState = false;
|
||||
|
||||
if (saveState === 'saving') {
|
||||
okLabel = 'Checking...';
|
||||
loadingState = true;
|
||||
}
|
||||
|
||||
return <Modal
|
||||
@ -120,6 +121,7 @@ const AddRecommendationModal: React.FC<AddRecommendationModalProps> = ({recommen
|
||||
backDropClick={false}
|
||||
okColor='black'
|
||||
okLabel={okLabel}
|
||||
okLoading={loadingState}
|
||||
size='sm'
|
||||
testId='add-recommendation-modal'
|
||||
title='Add recommendation'
|
||||
|
@ -41,9 +41,10 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
|
||||
});
|
||||
|
||||
let okLabel = 'Add';
|
||||
let loadingState = false;
|
||||
|
||||
if (saveState === 'saving') {
|
||||
okLabel = 'Adding...';
|
||||
loadingState = true;
|
||||
} else if (saveState === 'saved') {
|
||||
okLabel = 'Added';
|
||||
}
|
||||
@ -81,6 +82,7 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
|
||||
leftButtonProps={leftButtonProps}
|
||||
okColor='black'
|
||||
okLabel={okLabel}
|
||||
okLoading={loadingState}
|
||||
size='sm'
|
||||
testId='add-recommendation-modal'
|
||||
title={'Add recommendation'}
|
||||
|
Loading…
Reference in New Issue
Block a user