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

Commit

Permalink
feat: add loading state to launcher with label (COR-4435) (#523)
Browse files Browse the repository at this point in the history
  • Loading branch information
sofly committed Jan 13, 2025
1 parent b1124a2 commit 08fd1b2
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 24 deletions.
29 changes: 11 additions & 18 deletions packages/chat/src/components/Launcher/Launcher.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,23 @@ export default meta;

const CollapsableLauncher = (props: any) => {
const [isOpen, setIsOpen] = useState(false);
const [counter, setCounter] = useState(0);
const [isDisabled, setIsDisabled] = useState(false);

return (
<Launcher
isOpen={isOpen}
isLoading={isDisabled}
isDisabled={isDisabled}
{...props}
onClick={() => {
setIsOpen((prev) => !prev);
props.onClick?.();

setCounter((prev) => prev + 1);

if (counter % 3 === 0) return;

setIsDisabled(!isDisabled);
}}
/>
);
Expand All @@ -51,23 +60,7 @@ export const Loading: Story = { render: () => <CollapsableLauncher isLoading />

export const DisabledAndLoading: Story = {
render: () => {
const [counter, setCounter] = useState(0);
const [isDisabled, setIsDisabled] = useState(true);

return (
<CollapsableLauncher
isLoading={isDisabled}
isDisabled={isDisabled}
image={tiledBg}
onClick={() => {
setCounter((prev) => prev + 1);

if (counter % 3 === 0) return;

setIsDisabled(!isDisabled);
}}
/>
);
return <CollapsableLauncher image={tiledBg} />;
},
};

Expand Down
48 changes: 45 additions & 3 deletions packages/chat/src/components/Launcher/LauncherWithLabel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ import React from 'react';
import { Button } from '@/components/Button';
import { ClassName } from '@/constants';

import { LoadingSpinner } from '../../LoadingSpinner/LoadingSpinner';
import { ChevronIcon } from '../ChevronIcon';
import { DEFAULT_ICON } from '../constant';
import { PhoneIcon } from '../PhoneIcon';
import { closeChevron, imageIconStyle, imageIconWrapper, launcherLabelStyles, launcherStyles } from './styles.css';
import {
closeChevron,
containerLoaderStyles,
imageIconStyle,
imageIconWrapper,
launcherLabelStyles,
launcherStyles,
loadingSpinnerStyles,
} from './styles.css';

export interface LauncherProps {
/**
Expand Down Expand Up @@ -43,21 +52,51 @@ export interface LauncherProps {
* Flag to use image.
*/
withIcon?: boolean;

/**
* Flag to show loader in the launcher.
*/
isLoading?: boolean;

/**
* Flag to disable the launcher.
*/
isDisabled?: boolean;
}

/**
* A floating action button used to launch the chat widget.
*
* @see {@link https://voiceflow.github.io/react-chat/?path=/story/components-launcher--default}
*/
export const LauncherWithLabel: React.FC<LauncherProps> = ({ isVoice, withIcon, image, isOpen, label, onClick }) => {
export const LauncherWithLabel: React.FC<LauncherProps> = ({
isVoice,
withIcon,
image,
isOpen,
label,
onClick,
isLoading,
isDisabled,
}) => {
const showDefaultPhoneIcon = !image && isVoice;

const loader = (
<div className={containerLoaderStyles}>
<LoadingSpinner className={loadingSpinnerStyles} variant="light" size="large" />
</div>
);

return (
<Button className={clsx(launcherStyles({ isOpen, noImage: !withIcon }), ClassName.LAUNCHER)} onClick={onClick}>
<Button
onClick={onClick}
className={clsx(launcherStyles({ isOpen, noImage: !withIcon, isDisabled, isLoading }), ClassName.LAUNCHER)}
>
<div className={imageIconWrapper({ isOpen, noImage: !withIcon })}>
{withIcon && (
<>
{isLoading && loader}

{showDefaultPhoneIcon && <PhoneIcon className={clsx(imageIconStyle({ isOpen }))} fill="white" />}

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

<ChevronIcon className={clsx(closeChevron({ isOpen }))} />
</div>

{isLoading && !withIcon && loader}

<div className={launcherLabelStyles}>{label}</div>
</Button>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { keyframes, style } from '@vanilla-extract/css';
import { keyframes, style, styleVariants } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';

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

const loadingVariant = styleVariants({
true: {},
});

const noImageVariant = styleVariants({
true: {},
});

export const launcherStyles = recipe({
base: {
borderRadius: '9999px',
Expand Down Expand Up @@ -68,7 +76,23 @@ export const launcherStyles = recipe({
padding: '8px 16px 8px 12px',
},
},
noImage: { true: {} },
isDisabled: {
true: {
backgroundColor: THEME.colors[300],

':hover': {
transform: 'none',
backgroundColor: THEME.colors[300],
},
':active': {
transform: 'none',
backgroundColor: THEME.colors[300],
},
},
},

noImage: noImageVariant,
isLoading: loadingVariant,
},
compoundVariants: [
{
Expand All @@ -90,6 +114,12 @@ export const launcherLabelStyles = style({
textAlign: 'left',
padding: '3px 0 1px 0',
transition: `all ${duration.mid} ${BEZIER}`,

selectors: {
[`${loadingVariant.true}${noImageVariant.true} &`]: {
opacity: 0,
},
},
});

export const twistInAnimation = keyframes({
Expand All @@ -116,12 +146,18 @@ export const twistOutAnimation = keyframes({
export const closeChevron = recipe({
base: {
transform: 'rotate(0deg)',
transition: transition(['width']),
transition: transition(['width', 'opacity']),
position: 'absolute',
width: '32px',
height: '32px',
left: 0,
opacity: 0,

selectors: {
[`${loadingVariant.true} &`]: {
opacity: '0 !important',
},
},
},
variants: {
isOpen: {
Expand All @@ -148,6 +184,12 @@ export const imageIconStyle = recipe({

flexShrink: 0,
transition: transition(['opacity']),

selectors: {
[`${loadingVariant.true} &`]: {
opacity: 0,
},
},
},
variants: {
isOpen: {
Expand Down Expand Up @@ -202,3 +244,19 @@ export const imageIconWrapper = recipe({
},
],
});

export const loadingSpinnerStyles = style({
color: 'white',
height: '24px',
width: '24px',
});

export const containerLoaderStyles = style({
position: 'absolute',
top: '50%',
left: '50%',

height: '24px',

transform: 'translate(-50%, -50%)',
});
2 changes: 2 additions & 0 deletions packages/chat/src/components/Launcher/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export const Launcher: React.FC<LauncherProps> = ({
onClick={onClick}
isVoice={isVoice}
withIcon={withIcon}
isLoading={isLoading}
isDisabled={isDisabled}
/>
);
}
Expand Down

0 comments on commit 08fd1b2

Please sign in to comment.