From 3662d5ec0f6318773524ddb8c2a55702574d1963 Mon Sep 17 00:00:00 2001 From: Nigel Lima Date: Fri, 21 Jul 2023 10:03:53 -0300 Subject: [PATCH] fix(button.tsx): Relative Spinner size (#868) * 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 - -
- -
+ + + + + + + + +
+You can also customize the spinner icon by passing a React node to the `processingSpinner` prop. + + +
+
diff --git a/src/components/Button/Button.spec.tsx b/src/components/Button/Button.spec.tsx index 83cf38479..a1a55e1b9 100644 --- a/src/components/Button/Button.spec.tsx +++ b/src/components/Button/Button.spec.tsx @@ -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'; @@ -84,6 +85,24 @@ describe('Components / Button', () => { expect(button()).toBeDisabled(); }); + + it('should show when `isProcessing={true}`', () => { + render(); + + expect(screen.getByText(/Hi there/)).toBeInTheDocument(); + expect(screen.getByRole('status')).toBeInTheDocument(); + }); + + it('should show custom spinner when `isProcessing={true}` and `processingSpinner` is present', () => { + render( + , + ); + + expect(screen.getByText(/Hi there/)).toBeInTheDocument(); + expect(screen.getByTestId('spinner')).toBeInTheDocument(); + }); }); describe('Rendering', () => { diff --git a/src/components/Button/Button.stories.tsx b/src/components/Button/Button.stories.tsx index c1e6e91df..81c6cb527 100644 --- a/src/components/Button/Button.stories.tsx +++ b/src/components/Button/Button.stories.tsx @@ -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; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 046929adf..e26ca3f28 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -21,6 +21,7 @@ export interface FlowbiteButtonTheme { disabled: string; isProcessing: string; spinnerSlot: string; + spinnerLeftPosition: ButtonSizes; gradient: ButtonGradientColors; gradientDuoTone: ButtonGradientDuoToneColors; inner: FlowbiteButtonInnerTheme; @@ -34,6 +35,7 @@ export interface FlowbiteButtonInnerTheme { base: string; position: PositionInButtonGroup; outline: string; + isProcessingPadding: ButtonSizes; } export interface FlowbiteButtonOutlineTheme extends FlowbiteBoolean { @@ -91,7 +93,7 @@ const ButtonComponent = forwardRef fullSized, isProcessing = false, processingLabel = 'Loading...', - processingSpinner: SpinnerComponent = , + processingSpinner, gradientDuoTone, gradientMonochrome, label, @@ -135,11 +137,16 @@ const ButtonComponent = forwardRef theme.size[size], outline && !theme.outline.color[color] && theme.inner.outline, isProcessing && theme.isProcessing, + isProcessing && theme.inner.isProcessingPadding[size], theme.inner.position[positionInGroup], )} > <> - {isProcessing && {SpinnerComponent}} + {isProcessing && ( + + {processingSpinner || } + + )} {typeof children !== 'undefined' ? ( children ) : ( @@ -153,8 +160,8 @@ const ButtonComponent = forwardRef ); }, ); +ButtonComponent.displayName = 'ButtonComponent'; -ButtonComponent.displayName = 'Button'; export const Button = Object.assign(ButtonComponent, { Group: ButtonGroup, }); diff --git a/src/components/Button/theme.ts b/src/components/Button/theme.ts index b46ad84ea..1b9f4833e 100644 --- a/src/components/Button/theme.ts +++ b/src/components/Button/theme.ts @@ -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', @@ -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: @@ -65,7 +72,7 @@ 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', @@ -73,6 +80,13 @@ export const buttonTheme: FlowbiteButtonTheme = { 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', diff --git a/tailwind.config.ts b/tailwind.config.ts index 09a11e8cc..2bc07879d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -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: [