Skip to content
This repository was archived by the owner on Aug 5, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading