Skip to content

Commit 0555d1f

Browse files
committed
Add loading states to Button component with shimmer effect
1 parent ef300d9 commit 0555d1f

File tree

1 file changed

+102
-57
lines changed

1 file changed

+102
-57
lines changed

src/components/Button/Button.tsx

Lines changed: 102 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Icon, IconName } from "@/components";
2-
import { styled } from "styled-components";
2+
import { styled, keyframes, css } from "styled-components";
33
import { BaseButton } from "../commonElement";
44
import React from "react";
55

@@ -21,12 +21,10 @@ export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
2121
align?: Alignment;
2222
/** Whether the button should fill the full width of its container */
2323
fillWidth?: boolean;
24-
/** Whether to show a loading spinner */
24+
/** Whether to show a loading state */
2525
loading?: boolean;
2626
/** Whether the button should be focused on mount */
2727
autoFocus?: boolean;
28-
/** Whether to show the label alongside the loading spinner */
29-
showLabelWithLoading?: boolean;
3028
}
3129

3230
export const Button = ({
@@ -39,69 +37,52 @@ export const Button = ({
3937
label,
4038
loading = false,
4139
disabled,
42-
showLabelWithLoading = false,
4340
...delegated
4441
}: ButtonProps) => (
4542
<StyledButton
4643
$styleType={type}
4744
$align={align}
4845
$fillWidth={fillWidth}
46+
$loading={loading}
4947
disabled={disabled || loading}
5048
aria-disabled={disabled || loading}
5149
role="button"
5250
{...delegated}
5351
>
54-
{!loading && (
55-
<>
56-
{iconLeft && (
57-
<ButtonIcon
58-
name={iconLeft}
59-
aria-hidden
60-
size="sm"
61-
/>
62-
)}
63-
64-
{label ?? children}
65-
66-
{iconRight && (
67-
<ButtonIcon
68-
name={iconRight}
69-
aria-hidden
70-
size="sm"
71-
/>
72-
)}
73-
</>
52+
{iconLeft && (
53+
<ButtonIcon
54+
name={iconLeft}
55+
aria-hidden
56+
size="sm"
57+
/>
7458
)}
75-
{loading && (
76-
<LoadingIconWrapper data-testid="click-ui-loading-icon-wrapper">
77-
<Icon
78-
name="loading-animated"
79-
data-testid="click-ui-loading-icon"
80-
aria-label="loading"
81-
></Icon>
82-
{showLabelWithLoading ? (label ?? children) : ""}
83-
</LoadingIconWrapper>
59+
60+
{label ?? children}
61+
62+
{iconRight && (
63+
<ButtonIcon
64+
name={iconRight}
65+
aria-hidden
66+
size="sm"
67+
/>
8468
)}
8569
</StyledButton>
8670
);
8771

88-
const LoadingIconWrapper = styled.div`
89-
background-color: inherit;
90-
top: 0;
91-
left: 0;
92-
bottom: 0;
93-
right: 0;
94-
display: flex;
95-
align-content: center;
96-
justify-content: center;
97-
align-items: center;
98-
gap: 0.5rem;
72+
const shimmer = keyframes`
73+
0% {
74+
background-position: -200px 0;
75+
}
76+
100% {
77+
background-position: 200px 0;
78+
}
9979
`;
10080

10181
const StyledButton = styled(BaseButton)<{
10282
$styleType: ButtonType;
10383
$align?: Alignment;
10484
$fillWidth?: boolean;
85+
$loading?: boolean;
10586
}>`
10687
width: ${({ $fillWidth }) => ($fillWidth ? "100%" : "revert")};
10788
color: ${({ $styleType = "primary", theme }) =>
@@ -117,6 +98,32 @@ const StyledButton = styled(BaseButton)<{
11798
align-items: center;
11899
justify-content: ${({ $align }) => ($align === "left" ? "flex-start" : "center")};
119100
white-space: nowrap;
101+
overflow: hidden;
102+
103+
&::before {
104+
content: "";
105+
position: absolute;
106+
top: 0;
107+
left: 0;
108+
right: 0;
109+
bottom: 0;
110+
pointer-events: none;
111+
background-size: 200px 100%;
112+
opacity: 0;
113+
}
114+
115+
${({ $loading, $styleType, theme }) => {
116+
if (!$loading) return "";
117+
118+
return css`
119+
&::before {
120+
background-image: ${theme.click.button.basic.color[$styleType].background
121+
.loading};
122+
animation: ${shimmer} 1.5s ease-in-out infinite;
123+
opacity: 1;
124+
}
125+
`;
126+
}}
120127
121128
&:hover {
122129
background-color: ${({ $styleType = "primary", theme }) =>
@@ -138,18 +145,56 @@ const StyledButton = styled(BaseButton)<{
138145
font: ${({ theme }) => theme.click.button.basic.typography.label.active};
139146
}
140147
141-
&:disabled,
142-
&:disabled:hover,
143-
&:disabled:active {
144-
background-color: ${({ $styleType = "primary", theme }) =>
145-
theme.click.button.basic.color[$styleType].background.disabled};
146-
color: ${({ $styleType = "primary", theme }) =>
147-
theme.click.button.basic.color[$styleType].text.disabled};
148-
border: ${({ theme }) => theme.click.button.stroke} solid
149-
${({ $styleType = "primary", theme }) =>
150-
theme.click.button.basic.color[$styleType].stroke.disabled};
151-
font: ${({ theme }) => theme.click.button.basic.typography.label.disabled};
152-
}
148+
${({ $loading, $styleType, theme }) => {
149+
if ($loading) return "";
150+
151+
const bgDisabled = theme.click.button.basic.color[$styleType].background.disabled;
152+
const textDisabled = theme.click.button.basic.color[$styleType].text.disabled;
153+
const strokeDisabled = theme.click.button.basic.color[$styleType].stroke.disabled;
154+
const stroke = theme.click.button.stroke;
155+
const fontDisabled = theme.click.button.basic.typography.label.disabled;
156+
const isPrimary = $styleType === "primary";
157+
158+
return css`
159+
&:disabled,
160+
&:disabled:hover,
161+
&:disabled:active {
162+
background-color: ${bgDisabled};
163+
color: ${textDisabled};
164+
border: ${stroke} solid ${strokeDisabled};
165+
font: ${fontDisabled};
166+
cursor: not-allowed;
167+
${isPrimary ? "opacity: 0.6;" : ""}
168+
}
169+
`;
170+
}}
171+
172+
/* Loading state styling */
173+
${({ $loading, $styleType }) => {
174+
if (!$loading) return "";
175+
176+
if ($styleType === "primary") {
177+
// Primary: 60% opacity + shimmer animation
178+
return css`
179+
opacity: 0.6;
180+
cursor: not-allowed;
181+
`;
182+
} else if ($styleType === "secondary" || $styleType === "empty") {
183+
// Secondary & Empty: Full opacity during loading, shimmer only, text dimmed (70%)
184+
return css`
185+
opacity: 0.7;
186+
cursor: not-allowed;
187+
`;
188+
} else if ($styleType === "danger") {
189+
// Destructive: Full opacity during loading, shimmer only, text dimmed (70%)
190+
return css`
191+
opacity: 0.7;
192+
cursor: not-allowed;
193+
`;
194+
}
195+
196+
return "";
197+
}}
153198
`;
154199

155200
const ButtonIcon = styled(Icon)`

0 commit comments

Comments
 (0)