11import { Link , type LinkProps , NavLink , type NavLinkProps } from "@remix-run/react" ;
2- import React , { forwardRef , type ReactNode , useImperativeHandle , useRef } from "react" ;
2+ import React , {
3+ forwardRef ,
4+ type ReactNode ,
5+ useEffect ,
6+ useImperativeHandle ,
7+ useRef ,
8+ useState ,
9+ } from "react" ;
310import { type ShortcutDefinition , useShortcutKeys } from "~/hooks/useShortcutKeys" ;
411import { cn } from "~/utils/cn" ;
512import { ShortcutKey } from "./ShortcutKey" ;
613import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from "./Tooltip" ;
714import { Icon , type RenderIcon } from "./Icon" ;
15+ import { Spinner } from "./Spinner" ;
816
917const sizes = {
1018 small : {
@@ -180,6 +188,7 @@ export type ButtonContentPropsType = {
180188 tooltip ?: ReactNode ;
181189 iconSpacing ?: string ;
182190 hideShortcutKey ?: boolean ;
191+ isLoading ?: boolean ;
183192} ;
184193
185194export function ButtonContent ( props : ButtonContentPropsType ) {
@@ -196,7 +205,19 @@ export function ButtonContent(props: ButtonContentPropsType) {
196205 tooltip,
197206 iconSpacing,
198207 hideShortcutKey,
208+ isLoading,
199209 } = props ;
210+
211+ const [ showSpinner , setShowSpinner ] = useState ( false ) ;
212+ useEffect ( ( ) => {
213+ if ( ! isLoading ) {
214+ setShowSpinner ( false ) ;
215+ return ;
216+ }
217+ const timer = setTimeout ( ( ) => setShowSpinner ( true ) , 200 ) ;
218+ return ( ) => clearTimeout ( timer ) ;
219+ } , [ isLoading ] ) ;
220+
200221 const variation = allVariants . variant [ props . variant ] ;
201222
202223 const btnClassName = cn ( allVariants . $all , variation . button ) ;
@@ -217,56 +238,64 @@ export function ButtonContent(props: ButtonContentPropsType) {
217238
218239 const buttonContent = (
219240 < div className = { cn ( "flex" , fullWidth ? "" : "w-fit text-xxs" , btnClassName , className ) } >
220- < div
221- className = { cn (
222- textAlignLeft ? "text-left" : "justify-center" ,
223- "flex w-full items-center" ,
224- iconSpacingClassName ,
225- iconSpacing
226- ) }
227- >
228- { LeadingIcon && (
229- < Icon
230- icon = { LeadingIcon }
231- className = { cn (
232- iconClassName ,
233- variation . icon ,
234- leadingIconClassName ,
235- "shrink-0 justify-start"
236- ) }
237- />
238- ) }
241+ < div className = { cn ( "relative" , "flex w-full items-center" ) } >
242+ < div
243+ className = { cn (
244+ textAlignLeft ? "text-left" : "justify-center" ,
245+ "flex w-full items-center" ,
246+ iconSpacingClassName ,
247+ iconSpacing ,
248+ showSpinner && "invisible"
249+ ) }
250+ >
251+ { LeadingIcon && (
252+ < Icon
253+ icon = { LeadingIcon }
254+ className = { cn (
255+ iconClassName ,
256+ variation . icon ,
257+ leadingIconClassName ,
258+ "shrink-0 justify-start"
259+ ) }
260+ />
261+ ) }
239262
240- { text &&
241- ( typeof text === "string" ? (
242- < span className = { cn ( "mx-auto grow self-center truncate" , textColorClassName ) } >
243- { text }
244- </ span >
245- ) : (
246- < > { text } </ >
247- ) ) }
248-
249- { shortcut &&
250- ! tooltip &&
251- props . shortcutPosition === "before-trailing-icon" &&
252- renderShortcutKey ( ) }
253-
254- { TrailingIcon && (
255- < Icon
256- icon = { TrailingIcon }
257- className = { cn (
258- iconClassName ,
259- variation . icon ,
260- trailingIconClassName ,
261- "shrink-0 justify-end"
262- ) }
263- />
264- ) }
263+ { text &&
264+ ( typeof text === "string" ? (
265+ < span className = { cn ( "mx-auto grow self-center truncate" , textColorClassName ) } >
266+ { text }
267+ </ span >
268+ ) : (
269+ < > { text } </ >
270+ ) ) }
271+
272+ { shortcut &&
273+ ! tooltip &&
274+ props . shortcutPosition === "before-trailing-icon" &&
275+ renderShortcutKey ( ) }
265276
266- { shortcut &&
267- ! tooltip &&
268- ( ! props . shortcutPosition || props . shortcutPosition === "after-trailing-icon" ) &&
269- renderShortcutKey ( ) }
277+ { TrailingIcon && (
278+ < Icon
279+ icon = { TrailingIcon }
280+ className = { cn (
281+ iconClassName ,
282+ variation . icon ,
283+ trailingIconClassName ,
284+ "shrink-0 justify-end"
285+ ) }
286+ />
287+ ) }
288+
289+ { shortcut &&
290+ ! tooltip &&
291+ ( ! props . shortcutPosition || props . shortcutPosition === "after-trailing-icon" ) &&
292+ renderShortcutKey ( ) }
293+ </ div >
294+ { showSpinner && (
295+ < span className = "absolute inset-0 flex items-center justify-center" >
296+ < Spinner className = "size-3.5" color = "white" />
297+ </ span >
298+ ) }
270299 </ div >
271300 </ div >
272301 ) ;
@@ -298,6 +327,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
298327 const innerRef = useRef < HTMLButtonElement > ( null ) ;
299328 useImperativeHandle ( ref , ( ) => innerRef . current as HTMLButtonElement ) ;
300329
330+ const isDisabled = disabled || props . isLoading ;
331+
301332 useShortcutKeys ( {
302333 shortcut : props . shortcut ,
303334 action : ( e ) => {
@@ -307,14 +338,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
307338 e . stopPropagation ( ) ;
308339 }
309340 } ,
310- disabled : disabled || ! props . shortcut ,
341+ disabled : isDisabled || ! props . shortcut ,
311342 } ) ;
312343
313344 return (
314345 < button
315346 className = { cn ( "group/button outline-none focus-custom" , props . fullWidth ? "w-full" : "" ) }
316347 type = { type }
317- disabled = { disabled }
348+ disabled = { isDisabled }
318349 onClick = { onClick }
319350 name = { props . name }
320351 value = { props . value }
0 commit comments