Skip to content

feat(bubble): add extra content support to Bubble component #930

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: feature
Choose a base branch
from
Open
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
23 changes: 22 additions & 1 deletion components/bubble/Bubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export interface BubbleContextProps {
onUpdate?: VoidFunction;
}

type RenderSlotNode =
| BubbleProps<any>['footer']
| BubbleProps<any>['header']
| BubbleProps<any>['extra'];

export const BubbleContext = React.createContext<BubbleContextProps>({});

const Bubble: React.ForwardRefRenderFunction<BubbleRef, BubbleProps> = (props, ref) => {
Expand All @@ -40,6 +45,7 @@ const Bubble: React.ForwardRefRenderFunction<BubbleRef, BubbleProps> = (props, r
onTypingComplete,
header,
footer,
extra,
_key,
...otherHtmlProps
} = props;
Expand Down Expand Up @@ -117,7 +123,7 @@ const Bubble: React.ForwardRefRenderFunction<BubbleRef, BubbleProps> = (props, r
() => (messageRender ? messageRender(typedContent as any) : typedContent),
[typedContent, messageRender],
);
const renderSlot = (node: BubbleProps<any>['footer'] | BubbleProps<any>['header']) =>
const renderSlot = (node: RenderSlotNode) =>
typeof node === 'function' ? node(typedContent, { key: _key }) : node;

// ============================ Render ============================
Expand Down Expand Up @@ -148,6 +154,21 @@ const Bubble: React.ForwardRefRenderFunction<BubbleRef, BubbleProps> = (props, r
)}
>
{contentNode}
{(extra || extra === 0) && (
<div
className={classnames(
`${prefixCls}-extra`,
contextConfig.classNames.extra,
classNames.extra,
)}
style={{
...contextConfig.styles.extra,
...styles.extra,
}}
>
{renderSlot(extra)}
</div>
)}
</div>
);

Expand Down
77 changes: 77 additions & 0 deletions components/bubble/__tests__/__snapshots__/demo-extend.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1432,6 +1432,83 @@ exports[`renders components/bubble/demo/debug-list.tsx extend context correctly

exports[`renders components/bubble/demo/debug-list.tsx extend context correctly 2`] = `[]`;

exports[`renders components/bubble/demo/extra.tsx extend context correctly 1`] = `
<div
class="ant-flex ant-flex-align-stretch ant-flex-gap-small ant-flex-vertical"
>
<div
class="ant-bubble ant-bubble-start"
>
<div
class="ant-bubble-content ant-bubble-content-filled"
>
Hello ice !
<div
class="ant-bubble-extra"
>
<span
aria-label="loading"
class="anticon anticon-loading anticon-spin"
role="img"
>
<svg
aria-hidden="true"
data-icon="loading"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
/>
</svg>
</span>
</div>
</div>
</div>
<div
class="ant-bubble ant-bubble-end"
>
<div
class="ant-bubble-content ant-bubble-content-filled"
>
Hello Ant Design X !
<div
class="ant-bubble-extra"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle"
role="img"
style="color: #ff4d4f;"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M464 688a48 48 0 1096 0 48 48 0 10-96 0zm24-112h48c4.4 0 8-3.6 8-8V296c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8z"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
`;

exports[`renders components/bubble/demo/extra.tsx extend context correctly 2`] = `[]`;

exports[`renders components/bubble/demo/header-and-footer.tsx extend context correctly 1`] = `
<div
class="ant-bubble ant-bubble-start"
Expand Down
75 changes: 75 additions & 0 deletions components/bubble/__tests__/__snapshots__/demo.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,81 @@ exports[`renders components/bubble/demo/debug-list.tsx correctly 1`] = `
</div>
`;

exports[`renders components/bubble/demo/extra.tsx correctly 1`] = `
<div
class="ant-flex ant-flex-align-stretch ant-flex-gap-small ant-flex-vertical"
>
<div
class="ant-bubble ant-bubble-start"
>
<div
class="ant-bubble-content ant-bubble-content-filled"
>
Hello ice !
<div
class="ant-bubble-extra"
>
<span
aria-label="loading"
class="anticon anticon-loading anticon-spin"
role="img"
>
<svg
aria-hidden="true"
data-icon="loading"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
/>
</svg>
</span>
</div>
</div>
</div>
<div
class="ant-bubble ant-bubble-end"
>
<div
class="ant-bubble-content ant-bubble-content-filled"
>
Hello Ant Design X !
<div
class="ant-bubble-extra"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle"
role="img"
style="color:#ff4d4f"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M464 688a48 48 0 1096 0 48 48 0 10-96 0zm24-112h48c4.4 0 8-3.6 8-8V296c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8z"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
`;

exports[`renders components/bubble/demo/header-and-footer.tsx correctly 1`] = `
<div
class="ant-bubble ant-bubble-start"
Expand Down
20 changes: 20 additions & 0 deletions components/bubble/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ describe('bubble', () => {
expect(element?.textContent).toBe('Footer for: Test content');
});

it('should render static extra', () => {
const { container } = render(<Bubble content="extra content" extra="extra" />);
const element = container.querySelector<HTMLSpanElement>('.ant-bubble .ant-bubble-extra');
expect(element).toBeTruthy();
expect(element?.textContent).toBe('extra');
});

it('should render extra with function and get content', () => {
const content = 'extra content';
const extraFunction = (content: BubbleContentType) => (
<div className="test-extra">{`Extra for: ${content}`}</div>
);
const { container } = render(<Bubble content={content} extra={extraFunction} />);
const element = container.querySelector<HTMLSpanElement>(
'.ant-bubble .ant-bubble-extra .test-extra',
);
expect(element).toBeTruthy();
expect(element?.textContent).toBe('Extra for: extra content');
});

it('Bubble support typing', () => {
const { container } = render(<Bubble typing content="test" />);
expect(container.querySelector<HTMLDivElement>('.ant-bubble')).toHaveClass('ant-bubble-typing');
Expand Down
6 changes: 5 additions & 1 deletion components/bubble/demo/_semantic.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CopyOutlined, SyncOutlined, UserOutlined } from '@ant-design/icons';
import { CopyOutlined, LoadingOutlined, SyncOutlined, UserOutlined } from '@ant-design/icons';
import { Bubble } from '@ant-design/x';
import { Avatar, Button, Space, theme } from 'antd';
import React from 'react';
Expand All @@ -11,12 +11,14 @@ const locales = {
header: '头部的容器',
content: '聊天内容的容器',
footer: '底部的容器',
extra: '额外的容器',
},
en: {
avatar: 'Wrapper element of the avatar',
header: 'Wrapper element of the header',
content: 'Wrapper element of the content',
footer: 'Wrapper element of the footer',
extra: 'Wrapper element of the extra',
},
};

Expand All @@ -33,6 +35,7 @@ const App: React.FC = () => {
{ name: 'header', desc: locale.header },
{ name: 'content', desc: locale.content },
{ name: 'footer', desc: locale.footer },
{ name: 'extra', desc: locale.extra },
]}
>
<Bubble
Expand All @@ -45,6 +48,7 @@ const App: React.FC = () => {
<Button color="default" variant="text" size="small" icon={<CopyOutlined />} />
</Space>
}
extra={<LoadingOutlined spin />}
/>
</SemanticPreview>
);
Expand Down
7 changes: 7 additions & 0 deletions components/bubble/demo/extra.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## zh-CN

自定义扩展内容。

## en-US

Customize extra content
17 changes: 17 additions & 0 deletions components/bubble/demo/extra.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ExclamationCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import { Bubble } from '@ant-design/x';
import { Flex } from 'antd';
import React from 'react';

const App = () => (
<Flex vertical gap="small">
<Bubble content="Hello ice !" extra={<LoadingOutlined spin />} />
<Bubble
placement="end"
content="Hello Ant Design X !"
extra={<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />}
/>
</Flex>
);

export default App;
18 changes: 16 additions & 2 deletions components/bubble/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Often used when chatting.
<code src="./demo/basic.tsx">Basic</code>
<code src="./demo/avatar-and-placement.tsx">Placement and avatar</code>
<code src="./demo/header-and-footer.tsx">Header and footer</code>
<code src="./demo/extra.tsx" version="1.5.0">Extra content</code>
<code src="./demo/loading.tsx">Loading</code>
<code src="./demo/typing.tsx">Typing effect</code>
<code src="./demo/custom-content.tsx">Custom rendering content.</code>
Expand All @@ -46,8 +47,9 @@ Common props ref:[Common props](/docs/react/common-props)
| avatar | Avatar component | React.ReactNode | - | - |
| classNames | Semantic DOM class | [Record<SemanticDOM, string>](#semantic-dom) | - | - |
| content | Content of bubble | ContentType | - | - |
| footer | Footer content | React.ReactNode \| (content: ContentType, info:{ key?: string \| number }) => React.ReactNode | - | - |
| header | Header content | React.ReactNode \| (content: ContentType, info:{ key?: string \| number }) => React.ReactNode | - | - |
| footer | Footer content | [SlotRenderType](#slotrendertype) | - | - |
| header | Header content | [SlotRenderType](#slotrendertype) | - | - |
| extra | Extra content | [SlotRenderType](#slotrendertype) | - | 1.5.0 |
| loading | Loading state of Message | boolean | - | |
| placement | Direction of Message | `start` \| `end` | `start` | |
| shape | Shape of bubble | `round` \| `corner` | - | | | styles | Semantic DOM style | [Record<SemanticDOM, CSSProperties>](#semantic-dom) | - | |
Expand All @@ -57,6 +59,18 @@ Common props ref:[Common props](/docs/react/common-props)
| messageRender | Customize display content | <ContentType extends [BubbleContentType](https://github.com/ant-design/x/blob/d3232c925a0dc61ad763c6664e16f07323ebca4a/components/bubble/interface.ts#L21) = string>(content?: ContentType) => ReactNode | - | |
| onTypingComplete | Callback when typing effect is completed. If typing is not set, it will be triggered immediately when rendering. | () => void | - | |

#### SlotRenderType

```typescript
type SlotInfoType = {
key?: string | number;
};

type SlotRenderType<ContentType> =
| React.ReactNode
| ((content: ContentType, info: SlotInfoType) => React.ReactNode);
```

#### ContentType

Default Type
Expand Down
18 changes: 16 additions & 2 deletions components/bubble/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ demo:
<code src="./demo/basic.tsx">基本</code>
<code src="./demo/avatar-and-placement.tsx">支持位置和头像</code>
<code src="./demo/header-and-footer.tsx">头和尾</code>
<code src="./demo/extra.tsx" version="1.5.0">额外内容</code>
<code src="./demo/loading.tsx">加载中</code>
<code src="./demo/typing.tsx">打字效果</code>
<code src="./demo/custom-content.tsx">自定义渲染内容</code>
Expand All @@ -47,8 +48,9 @@ demo:
| avatar | 展示头像 | React.ReactNode | - | |
| classNames | 语义化结构 class | [Record<SemanticDOM, string>](#semantic-dom) | - | |
| content | 聊天内容 | ContentType | - | |
| footer | 底部内容 | React.ReactNode \| (content: ContentType ,info: { key?: string \| number }) => React.ReactNode | - | |
| header | 头部内容 | React.ReactNode \| (content: ContentType, info: { key?: string \| number }) => React.ReactNode | - | |
| footer | 底部内容 | [SlotRenderType](#slotrendertype) | - | |
| header | 头部内容 | [SlotRenderType](#slotrendertype) | - | |
| extra | 额外内容 | [SlotRenderType](#slotrendertype) | - | 1.5.0 |
| loading | 聊天内容加载状态 | boolean | - | |
| placement | 信息位置 | `start` \| `end` | `start` | |
| shape | 气泡形状 | `round` \| `corner` | - | |
Expand All @@ -59,6 +61,18 @@ demo:
| messageRender | 自定义渲染内容 | (content?: ContentType) => ReactNode | - | |
| onTypingComplete | 打字效果完成时的回调,如果没有设置 typing 将在渲染时立刻触发 | () => void | - | |

#### SlotRenderType

```typescript
type SlotInfoType = {
key?: string | number;
};

type SlotRenderType<ContentType> =
| React.ReactNode
| ((content: ContentType, info: SlotInfoType) => React.ReactNode);
```

#### ContentType

默认类型
Expand Down
Loading