Skip to content

Commit 31b665a

Browse files
committed
Adds a new isLoading prop that shows a spinner in the middle of the button
1 parent 5f359be commit 31b665a

File tree

1 file changed

+82
-51
lines changed

1 file changed

+82
-51
lines changed

apps/webapp/app/components/primitives/Buttons.tsx

Lines changed: 82 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { 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";
310
import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
411
import { cn } from "~/utils/cn";
512
import { ShortcutKey } from "./ShortcutKey";
613
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
714
import { Icon, type RenderIcon } from "./Icon";
15+
import { Spinner } from "./Spinner";
816

917
const sizes = {
1018
small: {
@@ -180,6 +188,7 @@ export type ButtonContentPropsType = {
180188
tooltip?: ReactNode;
181189
iconSpacing?: string;
182190
hideShortcutKey?: boolean;
191+
isLoading?: boolean;
183192
};
184193

185194
export 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

Comments
 (0)