Skip to content
This repository was archived by the owner on Aug 5, 2025. It is now read-only.

Commit 08fd1b2

Browse files
committed
feat: add loading state to launcher with label (COR-4435) (#523)
1 parent b1124a2 commit 08fd1b2

4 files changed

Lines changed: 119 additions & 24 deletions

File tree

packages/chat/src/components/Launcher/Launcher.story.tsx

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,23 @@ export default meta;
2222

2323
const CollapsableLauncher = (props: any) => {
2424
const [isOpen, setIsOpen] = useState(false);
25+
const [counter, setCounter] = useState(0);
26+
const [isDisabled, setIsDisabled] = useState(false);
2527

2628
return (
2729
<Launcher
2830
isOpen={isOpen}
31+
isLoading={isDisabled}
32+
isDisabled={isDisabled}
2933
{...props}
3034
onClick={() => {
3135
setIsOpen((prev) => !prev);
32-
props.onClick?.();
36+
37+
setCounter((prev) => prev + 1);
38+
39+
if (counter % 3 === 0) return;
40+
41+
setIsDisabled(!isDisabled);
3342
}}
3443
/>
3544
);
@@ -51,23 +60,7 @@ export const Loading: Story = { render: () => <CollapsableLauncher isLoading />
5160

5261
export const DisabledAndLoading: Story = {
5362
render: () => {
54-
const [counter, setCounter] = useState(0);
55-
const [isDisabled, setIsDisabled] = useState(true);
56-
57-
return (
58-
<CollapsableLauncher
59-
isLoading={isDisabled}
60-
isDisabled={isDisabled}
61-
image={tiledBg}
62-
onClick={() => {
63-
setCounter((prev) => prev + 1);
64-
65-
if (counter % 3 === 0) return;
66-
67-
setIsDisabled(!isDisabled);
68-
}}
69-
/>
70-
);
63+
return <CollapsableLauncher image={tiledBg} />;
7164
},
7265
};
7366

packages/chat/src/components/Launcher/LauncherWithLabel/index.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ import React from 'react';
55
import { Button } from '@/components/Button';
66
import { ClassName } from '@/constants';
77

8+
import { LoadingSpinner } from '../../LoadingSpinner/LoadingSpinner';
89
import { ChevronIcon } from '../ChevronIcon';
910
import { DEFAULT_ICON } from '../constant';
1011
import { PhoneIcon } from '../PhoneIcon';
11-
import { closeChevron, imageIconStyle, imageIconWrapper, launcherLabelStyles, launcherStyles } from './styles.css';
12+
import {
13+
closeChevron,
14+
containerLoaderStyles,
15+
imageIconStyle,
16+
imageIconWrapper,
17+
launcherLabelStyles,
18+
launcherStyles,
19+
loadingSpinnerStyles,
20+
} from './styles.css';
1221

1322
export interface LauncherProps {
1423
/**
@@ -43,21 +52,51 @@ export interface LauncherProps {
4352
* Flag to use image.
4453
*/
4554
withIcon?: boolean;
55+
56+
/**
57+
* Flag to show loader in the launcher.
58+
*/
59+
isLoading?: boolean;
60+
61+
/**
62+
* Flag to disable the launcher.
63+
*/
64+
isDisabled?: boolean;
4665
}
4766

4867
/**
4968
* A floating action button used to launch the chat widget.
5069
*
5170
* @see {@link https://voiceflow.github.io/react-chat/?path=/story/components-launcher--default}
5271
*/
53-
export const LauncherWithLabel: React.FC<LauncherProps> = ({ isVoice, withIcon, image, isOpen, label, onClick }) => {
72+
export const LauncherWithLabel: React.FC<LauncherProps> = ({
73+
isVoice,
74+
withIcon,
75+
image,
76+
isOpen,
77+
label,
78+
onClick,
79+
isLoading,
80+
isDisabled,
81+
}) => {
5482
const showDefaultPhoneIcon = !image && isVoice;
5583

84+
const loader = (
85+
<div className={containerLoaderStyles}>
86+
<LoadingSpinner className={loadingSpinnerStyles} variant="light" size="large" />
87+
</div>
88+
);
89+
5690
return (
57-
<Button className={clsx(launcherStyles({ isOpen, noImage: !withIcon }), ClassName.LAUNCHER)} onClick={onClick}>
91+
<Button
92+
onClick={onClick}
93+
className={clsx(launcherStyles({ isOpen, noImage: !withIcon, isDisabled, isLoading }), ClassName.LAUNCHER)}
94+
>
5895
<div className={imageIconWrapper({ isOpen, noImage: !withIcon })}>
5996
{withIcon && (
6097
<>
98+
{isLoading && loader}
99+
61100
{showDefaultPhoneIcon && <PhoneIcon className={clsx(imageIconStyle({ isOpen }))} fill="white" />}
62101

63102
{!showDefaultPhoneIcon && (
@@ -68,6 +107,9 @@ export const LauncherWithLabel: React.FC<LauncherProps> = ({ isVoice, withIcon,
68107

69108
<ChevronIcon className={clsx(closeChevron({ isOpen }))} />
70109
</div>
110+
111+
{isLoading && !withIcon && loader}
112+
71113
<div className={launcherLabelStyles}>{label}</div>
72114
</Button>
73115
);

packages/chat/src/components/Launcher/LauncherWithLabel/styles.css.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { keyframes, style } from '@vanilla-extract/css';
1+
import { keyframes, style, styleVariants } from '@vanilla-extract/css';
22
import { recipe } from '@vanilla-extract/recipes';
33

44
import { fadeInSlideUp } from '@/components/UserResponse/styles.css';
@@ -9,6 +9,14 @@ import { transition } from '@/styles/transitions';
99
const LAUNCHER_WITH_LABEL_SIZE = 40;
1010
const BEZIER = 'cubic-bezier(0.4, 0, 0.2, 1)';
1111

12+
const loadingVariant = styleVariants({
13+
true: {},
14+
});
15+
16+
const noImageVariant = styleVariants({
17+
true: {},
18+
});
19+
1220
export const launcherStyles = recipe({
1321
base: {
1422
borderRadius: '9999px',
@@ -68,7 +76,23 @@ export const launcherStyles = recipe({
6876
padding: '8px 16px 8px 12px',
6977
},
7078
},
71-
noImage: { true: {} },
79+
isDisabled: {
80+
true: {
81+
backgroundColor: THEME.colors[300],
82+
83+
':hover': {
84+
transform: 'none',
85+
backgroundColor: THEME.colors[300],
86+
},
87+
':active': {
88+
transform: 'none',
89+
backgroundColor: THEME.colors[300],
90+
},
91+
},
92+
},
93+
94+
noImage: noImageVariant,
95+
isLoading: loadingVariant,
7296
},
7397
compoundVariants: [
7498
{
@@ -90,6 +114,12 @@ export const launcherLabelStyles = style({
90114
textAlign: 'left',
91115
padding: '3px 0 1px 0',
92116
transition: `all ${duration.mid} ${BEZIER}`,
117+
118+
selectors: {
119+
[`${loadingVariant.true}${noImageVariant.true} &`]: {
120+
opacity: 0,
121+
},
122+
},
93123
});
94124

95125
export const twistInAnimation = keyframes({
@@ -116,12 +146,18 @@ export const twistOutAnimation = keyframes({
116146
export const closeChevron = recipe({
117147
base: {
118148
transform: 'rotate(0deg)',
119-
transition: transition(['width']),
149+
transition: transition(['width', 'opacity']),
120150
position: 'absolute',
121151
width: '32px',
122152
height: '32px',
123153
left: 0,
124154
opacity: 0,
155+
156+
selectors: {
157+
[`${loadingVariant.true} &`]: {
158+
opacity: '0 !important',
159+
},
160+
},
125161
},
126162
variants: {
127163
isOpen: {
@@ -148,6 +184,12 @@ export const imageIconStyle = recipe({
148184

149185
flexShrink: 0,
150186
transition: transition(['opacity']),
187+
188+
selectors: {
189+
[`${loadingVariant.true} &`]: {
190+
opacity: 0,
191+
},
192+
},
151193
},
152194
variants: {
153195
isOpen: {
@@ -202,3 +244,19 @@ export const imageIconWrapper = recipe({
202244
},
203245
],
204246
});
247+
248+
export const loadingSpinnerStyles = style({
249+
color: 'white',
250+
height: '24px',
251+
width: '24px',
252+
});
253+
254+
export const containerLoaderStyles = style({
255+
position: 'absolute',
256+
top: '50%',
257+
left: '50%',
258+
259+
height: '24px',
260+
261+
transform: 'translate(-50%, -50%)',
262+
});

packages/chat/src/components/Launcher/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ export const Launcher: React.FC<LauncherProps> = ({
9696
onClick={onClick}
9797
isVoice={isVoice}
9898
withIcon={withIcon}
99+
isLoading={isLoading}
100+
isDisabled={isDisabled}
99101
/>
100102
);
101103
}

0 commit comments

Comments
 (0)