Skip to content

Commit

Permalink
Resolves #163: add custom renderers config for images and texts (#164)
Browse files Browse the repository at this point in the history
* closes #163: add custom button renderer setting

* move endPoint from Chat props to TockSettings
  • Loading branch information
Fabilin authored Oct 4, 2024
1 parent 1487322 commit f5c99a2
Show file tree
Hide file tree
Showing 19 changed files with 236 additions and 89 deletions.
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

0 comments on commit f5c99a2

Please sign in to comment.