Skip to content

Commit

Permalink
fix(button.tsx): Relative Spinner size (#868)
Browse files Browse the repository at this point in the history
* fix(button.tsx): button spinner relative size

the Spinner component was not fitting inside the Button.tsx when isProcessing=true. Also a transition was added to the Button component to fit the Spinner.

Fix #850

* docs(button.tsx): added more examples of <Button isProcessing />

* refact(button.tsx): re-run prettier format

* test(button.tsx): added tests for the isProcessing prop
  • Loading branch information
nigellima authored Jul 21, 2023
1 parent ecc3f6e commit 3662d5e
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 15 deletions.
42 changes: 33 additions & 9 deletions app/docs/components/button/button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CodePreview } from '~/app/components/code-preview';
import { Button, theme } from '~/src';
import { HiOutlineArrowRight, HiShoppingCart } from 'react-icons/hi';
import Link from 'next/link';
import { AiOutlineLoading } from 'react-icons/ai';

## Table of Contents

Expand Down Expand Up @@ -185,16 +186,39 @@ Create a button with only icons by adding the `iconOnly` property to the `<Butto

Add a loading state to the button element by adding the `isProcessing` property to the `<Button>` component.

<CodePreview importFlowbiteReact="Button" title="Loader" className="flex flex-wrap items-center gap-2">
<CodePreview title="Loader" className="flex flex-wrap items-center gap-2" importFlowbiteReact="Button">
<div className="flex flex-wrap items-center gap-2">
<div>
<Button isProcessing>Click me!</Button>
</div>
<div>
<Button isProcessing outline>
Click me!
</Button>
</div>
<Button size="xs" isProcessing>
Click me!
</Button>
<Button size="sm" isProcessing gradientDuoTone="purpleToBlue">
Click me!
</Button>
<Button size="md" isProcessing color="red">
Click me!
</Button>
<Button size="lg" isProcessing pill>
Click me!
</Button>
<Button size="xl" isProcessing outline>
Click me!
</Button>
</div>
</CodePreview>

<br />
You can also customize the spinner icon by passing a React node to the `processingSpinner` prop.

<CodePreview
importFlowbiteReact="Button"
title="Loader"
className="flex flex-wrap items-center gap-2"
importExternal="import { AiOutlineLoading } from 'react-icons/ai';"
>
<div className="flex flex-wrap items-center gap-2">
<Button size="md" isProcessing processingSpinner={<AiOutlineLoading className="h-6 w-6 animate-spin" />}>
Click me!
</Button>
</div>
</CodePreview>

Expand Down
19 changes: 19 additions & 0 deletions src/components/Button/Button.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { PropsWithChildren } from 'react';
import { AiOutlineLoading } from 'react-icons/ai';
import { describe, expect, it, vi } from 'vitest';
import { Flowbite } from '../../';
import { Button } from './Button';
Expand Down Expand Up @@ -84,6 +85,24 @@ describe('Components / Button', () => {

expect(button()).toBeDisabled();
});

it('should show <Spinner /> when `isProcessing={true}`', () => {
render(<Button isProcessing>Hi there</Button>);

expect(screen.getByText(/Hi there/)).toBeInTheDocument();
expect(screen.getByRole('status')).toBeInTheDocument();
});

it('should show custom spinner when `isProcessing={true}` and `processingSpinner` is present', () => {
render(
<Button isProcessing processingSpinner={<AiOutlineLoading data-testid="spinner" />}>
Hi there
</Button>,
);

expect(screen.getByText(/Hi there/)).toBeInTheDocument();
expect(screen.getByTestId('spinner')).toBeInTheDocument();
});
});

describe('Rendering', () => {
Expand Down
5 changes: 5 additions & 0 deletions src/components/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ export default {
options: Object.keys(theme.button.color),
control: { type: 'inline-radio' },
},
size: {
options: ['xs', 'sm', 'md', 'lg', 'xl'],
control: { type: 'inline-radio' },
},
},
args: {
disabled: false,
isProcessing: false,
},
} as Meta;

Expand Down
13 changes: 10 additions & 3 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface FlowbiteButtonTheme {
disabled: string;
isProcessing: string;
spinnerSlot: string;
spinnerLeftPosition: ButtonSizes;
gradient: ButtonGradientColors;
gradientDuoTone: ButtonGradientDuoToneColors;
inner: FlowbiteButtonInnerTheme;
Expand All @@ -34,6 +35,7 @@ export interface FlowbiteButtonInnerTheme {
base: string;
position: PositionInButtonGroup;
outline: string;
isProcessingPadding: ButtonSizes;
}

export interface FlowbiteButtonOutlineTheme extends FlowbiteBoolean {
Expand Down Expand Up @@ -91,7 +93,7 @@ const ButtonComponent = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>
fullSized,
isProcessing = false,
processingLabel = 'Loading...',
processingSpinner: SpinnerComponent = <Spinner />,
processingSpinner,
gradientDuoTone,
gradientMonochrome,
label,
Expand Down Expand Up @@ -135,11 +137,16 @@ const ButtonComponent = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>
theme.size[size],
outline && !theme.outline.color[color] && theme.inner.outline,
isProcessing && theme.isProcessing,
isProcessing && theme.inner.isProcessingPadding[size],
theme.inner.position[positionInGroup],
)}
>
<>
{isProcessing && <span className={twMerge(theme.spinnerSlot)}>{SpinnerComponent}</span>}
{isProcessing && (
<span className={twMerge(theme.spinnerSlot, theme.spinnerLeftPosition[size])}>
{processingSpinner || <Spinner size={size} />}
</span>
)}
{typeof children !== 'undefined' ? (
children
) : (
Expand All @@ -153,8 +160,8 @@ const ButtonComponent = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>
);
},
);
ButtonComponent.displayName = 'ButtonComponent';

ButtonComponent.displayName = 'Button';
export const Button = Object.assign(ButtonComponent, {
Group: ButtonGroup,
});
20 changes: 17 additions & 3 deletions src/components/Button/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { FlowbiteButtonTheme } from './Button';
import type { FlowbiteButtonGroupTheme } from './ButtonGroup';

export const buttonTheme: FlowbiteButtonTheme = {
base: 'group flex h-min items-center justify-center p-0.5 text-center font-medium focus:z-10 focus:outline-none',
base: 'group flex h-min items-center justify-center p-0.5 text-center font-medium relative focus:z-10 focus:outline-none',
fullSized: 'w-full',
color: {
dark: 'text-white bg-gray-800 border border-transparent enabled:hover:bg-gray-900 focus:ring-4 focus:ring-gray-300 dark:bg-gray-800 dark:enabled:hover:bg-gray-700 dark:focus:ring-gray-800 dark:border-gray-700',
Expand Down Expand Up @@ -33,7 +33,14 @@ export const buttonTheme: FlowbiteButtonTheme = {
},
disabled: 'cursor-not-allowed opacity-50',
isProcessing: 'cursor-wait',
spinnerSlot: 'mr-3',
spinnerSlot: 'absolute h-full top-0 flex items-center animate-fade-in',
spinnerLeftPosition: {
xs: 'left-2',
sm: 'left-3',
md: 'left-4',
lg: 'left-5',
xl: 'left-6',
},
gradient: {
cyan: 'text-white bg-gradient-to-r from-cyan-400 via-cyan-500 to-cyan-600 enabled:hover:bg-gradient-to-br focus:ring-4 focus:ring-cyan-300 dark:focus:ring-cyan-800',
failure:
Expand Down Expand Up @@ -65,14 +72,21 @@ export const buttonTheme: FlowbiteButtonTheme = {
'text-gray-900 bg-gradient-to-r from-teal-200 to-lime-200 enabled:hover:bg-gradient-to-l enabled:hover:from-teal-200 enabled:hover:to-lime-200 enabled:hover:text-gray-900 focus:ring-4 focus:ring-lime-200 dark:focus:ring-teal-700',
},
inner: {
base: 'flex items-center',
base: 'flex items-stretch transition-all duration-200',
position: {
none: '',
start: 'rounded-r-none',
middle: 'rounded-none',
end: 'rounded-l-none',
},
outline: 'border border-transparent',
isProcessingPadding: {
xs: 'pl-8',
sm: 'pl-10',
md: 'pl-12',
lg: 'pl-16',
xl: 'pl-20',
},
},
label:
'ml-2 inline-flex h-4 w-4 items-center justify-center rounded-full bg-cyan-200 text-xs font-semibold text-cyan-800',
Expand Down
9 changes: 9 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ const config: Config = {
maxWidth: {
'8xl': '90rem',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
},
animation: {
'fade-in': 'fadeIn 200ms ease-in-out',
},
},
fontFamily: {
sans: [
Expand Down

1 comment on commit 3662d5e

@vercel
Copy link

@vercel vercel bot commented on 3662d5e Jul 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.