Skip to content
Merged
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
6 changes: 4 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ module.exports = {
},
},
],
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
'@typescript-eslint/no-unused-vars': [
'error',
{ ignoreRestSiblings: true },
],
},
plugins: ['@emotion'],
};
33 changes: 25 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@ Your bundler must support ESM modules, which is the case for Webpack, Rollup and
import { ThemeProvider } from "@emotion/react";
import { TockContext, Chat, createTheme } from 'tock-react-kit';

<TockContext settings={ /* ... */ }>
<TockContext settings={{
endPoint: "<TOCK_BOT_API_URL>",
}}>
<ThemeProvider theme={createTheme({ /* ... */})}>
<Chat
endPoint="<TOCK_BOT_API_URL>"
/* The following parameters are optional */
referralParameter="referralParameter"
// also accepts all properties from TockOptions, like:
Expand Down Expand Up @@ -166,14 +167,15 @@ If the chat does not suit your needs, there are two main ways to customize the i

### Configure custom renderers

Custom rendering can currently be defined for text and images. Here are some examples of what this enables:
Custom rendering can currently be defined for text, images, and buttons. Here are some examples of what this enables:

- processing custom markup in the text of any component
- stripping harmful HTML tags and attributes when the backend is untrustworthy
- dynamically decorating text messages
- dynamically decorating text messages and buttons
- automatically embedding SVG images into the DOM
- implementing fallback behavior when an image fails to load
- using [metadata](#message-metadata) sent by the server to set image properties like width and height
- setting up redirects for links

See the [`TockSettings`](#renderersettings) API reference for the details of available renderers.

Expand Down Expand Up @@ -334,6 +336,7 @@ A `TockTheme` can be used as a value of a `ThemeProvider` of [`emotion-theming`]

| Property name | Type | Description |
|----------------|-------------------------|------------------------------------------------------|
| `endPoint` | `string` | URL for the bot's web connector endpoint |
| `locale` | `string?` | Optional user language, as an *RFC 5646* code |
| `localStorage` | `LocalStorageSettings?` | Configuration for use of localStorage by the library |
| `renderers` | `RendererSettings?` | Configuration for custom image and text renderers |
Expand All @@ -346,10 +349,24 @@ A `TockTheme` can be used as a value of a `ThemeProvider` of [`emotion-theming`]

#### `RendererSettings`

| Property name | Type | Description |
|------------------|--------------------------|-------------------------------------------------------------------------------|
| `imageRenderers` | `ImageRendererSettings?` | Configuration of renderers for dynamic images displayed in the chat interface |
| `textRenderers` | `TextRendererSettings?` | Configuration of renderers for dynamic text displayed in the chat interface |
| Property name | Type | Description |
|-------------------|--------------------|-------------------------------------------------------------------------------|
| `buttonRenderers` | `ButtonRenderers?` | Configuration of renderers for buttons displayed in the chat interface |
| `imageRenderers` | `ImageRenderers?` | Configuration of renderers for dynamic images displayed in the chat interface |
| `textRenderers` | `TextRenderers?` | Configuration of renderers for dynamic text displayed in the chat interface |

#### `ButtonRendererSettings`

Button renderers all implement some specialization of the `ButtonRenderer` interface.
They are tasked with rendering a graphical component using button-specific data, a class name, and other generic HTML attributes.
The passed in class name provides the default style for the rendered component, as well as applicable [overrides](#overrides).

| Property name | Type | Description |
|---------------|-----------------------------|------------------------------------------------------------------------------------------------------|
| `default` | `ButtonRenderer` | The fallback renderer. By default, renders a single `button` component using the provided properties |
| `url` | `UrlButtonRenderer` | Renders an `UrlButton`. By default, renders a single `a` component using the provided properties |
| `postback` | `PostBackButtonRenderer?` | Renders a `PostBackButton` |
| `quickReply` | `QuickReplyButtonRenderer?` | Renders a `QuickReply` |

#### `ImageRendererSettings`

Expand Down
8 changes: 6 additions & 2 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,17 @@ const Card = forwardRef<HTMLLIElement, CardProps>(function cardRender(
// having the default index-based key is fine since we do not reorder buttons
<li key={index}>
{'url' in button ? (
<UrlButton customStyle={urlButtonStyle} {...button} />
<UrlButton
buttonData={button}
customStyle={urlButtonStyle}
{...(isHidden && { tabIndex: -1 })}
/>
) : (
<PostBackButton
buttonData={button}
customStyle={postBackButtonStyle}
onClick={onAction.bind(null, button)}
onKeyPress={onAction.bind(null, button)}
{...button}
{...(isHidden && { tabIndex: -1 })}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import TockLocalStorage from 'TockLocalStorage';
import PostInitContext from '../../PostInitContext';

export interface ChatProps {
endPoint: string;
endPoint?: string;
referralParameter?: string;
timeoutBetweenMessage?: number;
/** A callback that will be executed once the chat is able to send and receive messages */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ const InlineQuickReplyList = ({
{items.map((child, index) => (
<QuickReply
key={`${child.label}-${index}`}
buttonData={child}
onClick={onItemClick.bind(null, child)}
{...child}
ref={ref.items[index]}
/>
))}
Expand Down
37 changes: 22 additions & 15 deletions src/components/QuickReply/QuickReply.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import React, { DetailedHTMLProps, HTMLAttributes, RefObject } from 'react';
import { QuickReply as QuickReplyData } from '../../model/buttons';

import QuickReplyImage from './QuickReplyImage';
import { useTextRenderer } from '../../settings/RendererSettings';
import {
useButtonRenderer,
useTextRenderer,
} from '../../settings/RendererSettings';

const QuickReplyButtonContainer = styled.li`
list-style: none;
Expand Down Expand Up @@ -46,25 +49,29 @@ const qrButtonCss: Interpolation<Theme> = [
(theme) => theme.overrides?.quickReply,
];

type Props = DetailedHTMLProps<
HTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> &
QuickReplyData;
interface Props
extends DetailedHTMLProps<
HTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
buttonData: QuickReplyData;
}

const QuickReply = React.forwardRef<HTMLButtonElement, Props>(
(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
{ imageUrl, label, payload, nlpText, ...rest }: Props,
ref: RefObject<HTMLButtonElement>,
) => {
({ buttonData, ...rest }: Props, ref: RefObject<HTMLButtonElement>) => {
const TextRenderer = useTextRenderer('default');
const ButtonRenderer = useButtonRenderer('quickReply');
return (
<QuickReplyButtonContainer>
<button ref={ref} css={qrButtonCss} {...rest}>
{imageUrl && <QuickReplyImage src={imageUrl} />}
<TextRenderer text={label} />
</button>
<ButtonRenderer
buttonData={buttonData}
ref={ref}
css={qrButtonCss}
{...rest}
>
{buttonData.imageUrl && <QuickReplyImage src={buttonData.imageUrl} />}
<TextRenderer text={buttonData.label} />
</ButtonRenderer>
</QuickReplyButtonContainer>
);
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/QuickReplyList/QuickReplyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ const QuickReplyList: (props: Props) => JSX.Element = ({
(item: Button, index: number) => (
<QuickReply
key={`${item.label}-${index}`}
buttonData={item}
onClick={onItemClick.bind(null, item)}
{...item}
/>
),
[onItemClick],
Expand Down
7 changes: 5 additions & 2 deletions src/components/buttons/ButtonList/ButtonList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ export const ButtonList: (props: Props) => JSX.Element = ({
// having the default index-based key is fine since we do not reorder buttons
<li key={index}>
{'url' in item ? (
<UrlButton {...item}></UrlButton>
<UrlButton buttonData={item} />
) : (
<PostBackButton onClick={onItemClick.bind(null, item)} {...item} />
<PostBackButton
buttonData={item}
onClick={onItemClick.bind(null, item)}
/>
)}
</li>
);
Expand Down
10 changes: 7 additions & 3 deletions src/components/buttons/PostBackButton/PostBackButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ type Story = StoryObj<typeof PostBackButton>;
export const SimplePostback: Story = {
name: 'PostBack Button',
args: {
label: 'Help',
buttonData: {
label: 'Help',
},
},
};

export const WithImage: Story = {
name: 'PostBack Button with image',
args: {
label: 'Help',
imageUrl: 'https://doc.tock.ai/tock/assets/images/logo.svg',
buttonData: {
label: 'Help',
imageUrl: 'https://doc.tock.ai/tock/assets/images/logo.svg',
},
},
};
34 changes: 19 additions & 15 deletions src/components/buttons/PostBackButton/PostBackButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { quickReplyStyle } from '../../QuickReply/QuickReply';
import { Interpolation, Theme } from '@emotion/react';
import { baseButtonStyle } from '../../QuickReply';
import { DetailedHTMLProps, HTMLAttributes } from 'react';
import { DetailedHTMLProps, HTMLAttributes, JSX } from 'react';
import QuickReplyImage from '../../QuickReply/QuickReplyImage';
import { useTextRenderer } from '../../../settings/RendererSettings';
import {
useButtonRenderer,
useTextRenderer,
} from '../../../settings/RendererSettings';
import { PostBackButtonData } from '../../../index';

const postBackButtonCss: Interpolation<Theme> = [
baseButtonStyle,
Expand All @@ -15,29 +19,29 @@ const postBackButtonCss: Interpolation<Theme> = [
],
];

type Props = DetailedHTMLProps<
HTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {
interface Props
extends DetailedHTMLProps<
HTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
buttonData: PostBackButtonData;
customStyle?: Interpolation<unknown>;
imageUrl?: string;
label: string;
tabIndex?: 0 | -1;
};
}

export const PostBackButton = ({
imageUrl,
label,
buttonData,
customStyle,
...rest
}: Props): JSX.Element => {
// Allow custom override for the Card's button styling
const css = customStyle ? [baseButtonStyle, customStyle] : postBackButtonCss;
const TextRenderer = useTextRenderer('default');
const ButtonRenderer = useButtonRenderer('postback');
return (
<button css={css} {...rest}>
{imageUrl && <QuickReplyImage src={imageUrl} />}
<TextRenderer text={label} />
</button>
<ButtonRenderer buttonData={buttonData} css={css} {...rest}>
{buttonData.imageUrl && <QuickReplyImage src={buttonData.imageUrl} />}
<TextRenderer text={buttonData.label} />
</ButtonRenderer>
);
};
22 changes: 14 additions & 8 deletions src/components/buttons/UrlButton/UrlButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,31 @@ type Story = StoryObj<typeof UrlButton>;
export const SimpleUrl: Story = {
name: 'URL Button',
args: {
label: 'TOCK',
url: 'https://doc.tock.ai',
buttonData: {
label: 'TOCK',
url: 'https://doc.tock.ai',
},
},
};

export const WithImage: Story = {
name: 'URL Button with image',
args: {
label: 'TOCK',
url: 'https://doc.tock.ai',
imageUrl: 'https://doc.tock.ai/tock/assets/images/logo.svg',
buttonData: {
label: 'TOCK',
url: 'https://doc.tock.ai',
imageUrl: 'https://doc.tock.ai/tock/assets/images/logo.svg',
},
},
};

export const WithTarget: Story = {
name: 'URL Button with _self target',
args: {
label: 'TOCK',
url: 'https://doc.tock.ai',
target: '_self',
buttonData: {
label: 'TOCK',
url: 'https://doc.tock.ai',
target: '_self',
},
},
};
31 changes: 18 additions & 13 deletions src/components/buttons/UrlButton/UrlButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { quickReplyStyle } from '../../QuickReply/QuickReply';
import { css, Interpolation, Theme } from '@emotion/react';
import { baseButtonStyle } from '../../QuickReply';
import QuickReplyImage from '../../QuickReply/QuickReplyImage';
import { useTextRenderer } from '../../../settings/RendererSettings';
import {
useButtonRenderer,
useTextRenderer,
} from '../../../settings/RendererSettings';
import { UrlButton as UrlButtonData } from '../../../model/buttons';

type Props = {
customStyle?: Interpolation<unknown>;
imageUrl?: string;
label: string;
target?: string;
url: string;
buttonData: UrlButtonData;
tabIndex?: 0 | -1;
};

Expand All @@ -29,21 +30,25 @@ const defaultUrlButtonCss: Interpolation<Theme> = [
];

export const UrlButton: (props: Props) => JSX.Element = ({
url,
imageUrl,
label,
target = '_blank',
buttonData,
customStyle,
tabIndex,
}: Props) => {
const css = customStyle
? [baseUrlButtonCss, customStyle]
: defaultUrlButtonCss;
const TextRenderer = useTextRenderer('default');
const ButtonRenderer = useButtonRenderer('url');
return (
<a href={url} target={target} css={css} tabIndex={tabIndex}>
{imageUrl && <QuickReplyImage src={imageUrl} />}
<TextRenderer text={label} />
</a>
<ButtonRenderer
buttonData={buttonData}
href={buttonData.url}
target={buttonData.target ?? '_blank'}
css={css}
tabIndex={tabIndex}
>
{buttonData.imageUrl && <QuickReplyImage src={buttonData.imageUrl} />}
<TextRenderer text={buttonData.label} />
</ButtonRenderer>
);
};
Loading