diff --git a/components/__tests__/__snapshots__/index.test.ts.snap b/components/__tests__/__snapshots__/index.test.ts.snap index 50f2581df9e6..8de60f5d3159 100644 --- a/components/__tests__/__snapshots__/index.test.ts.snap +++ b/components/__tests__/__snapshots__/index.test.ts.snap @@ -16,6 +16,7 @@ exports[`antd exports modules correctly 1`] = ` "Card", "Carousel", "Cascader", + "Chatbox", "Checkbox", "Col", "Collapse", diff --git a/components/chatbox/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/chatbox/__tests__/__snapshots__/demo-extend.test.ts.snap new file mode 100644 index 000000000000..cb828546aa38 --- /dev/null +++ b/components/chatbox/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -0,0 +1,313 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders components/chatbox/demo/avatar-and-placement.tsx extend context correctly 1`] = ` +
+
+
+ + + + + +
+
+ Good morning, how are you ? +
+
+
+ +
+ What a beautiful day ! +
+
+
+
+ + + + + +
+
+ Hi, good morning, I'm fine ! +
+
+
+ +
+ Thank you ! +
+
+
+`; + +exports[`renders components/chatbox/demo/avatar-and-placement.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/chatbox/demo/basic.tsx extend context correctly 1`] = ` +
+
+ hello world ! +
+
+`; + +exports[`renders components/chatbox/demo/basic.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/chatbox/demo/contentRender.tsx extend context correctly 1`] = ` +
+
+ + + + + +
+
+ +

+ +

+
+`; + +exports[`renders components/chatbox/demo/contentRender.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/chatbox/demo/loading.tsx extend context correctly 1`] = ` +
+
+
+ + + + + +
+
+ + + + + +
+
+
+ Loading state: + +
+
+`; + +exports[`renders components/chatbox/demo/loading.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/chatbox/demo/typing.tsx extend context correctly 1`] = ` +
+
+ + + + + +
+
+ F +
+
+`; + +exports[`renders components/chatbox/demo/typing.tsx extend context correctly 2`] = `[]`; diff --git a/components/chatbox/__tests__/__snapshots__/demo.test.ts.snap b/components/chatbox/__tests__/__snapshots__/demo.test.ts.snap new file mode 100644 index 000000000000..09e60f547b80 --- /dev/null +++ b/components/chatbox/__tests__/__snapshots__/demo.test.ts.snap @@ -0,0 +1,297 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders components/chatbox/demo/avatar-and-placement.tsx correctly 1`] = ` +
+
+
+ + + + + +
+
+ Good morning, how are you ? +
+
+
+ +
+ What a beautiful day ! +
+
+
+
+ + + + + +
+
+ Hi, good morning, I'm fine ! +
+
+
+ +
+ Thank you ! +
+
+
+`; + +exports[`renders components/chatbox/demo/basic.tsx correctly 1`] = ` +
+
+ hello world ! +
+
+`; + +exports[`renders components/chatbox/demo/contentRender.tsx correctly 1`] = ` +
+
+ + + + + +
+
+
+`; + +exports[`renders components/chatbox/demo/loading.tsx correctly 1`] = ` +
+
+
+ + + + + +
+
+ + + + + +
+
+
+ Loading state: + +
+
+`; + +exports[`renders components/chatbox/demo/typing.tsx correctly 1`] = ` +
+
+ + + + + +
+
+
+`; diff --git a/components/chatbox/__tests__/__snapshots__/index.test.tsx.snap b/components/chatbox/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000000..c3dbc3f1388d --- /dev/null +++ b/components/chatbox/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`chatbox Chatbox component work 1`] = ` +
+
+ test +
+
+`; + +exports[`chatbox rtl render component should be rendered correctly in RTL direction 1`] = ` +
+
+ test +
+
+`; diff --git a/components/chatbox/__tests__/demo-extend.test.ts b/components/chatbox/__tests__/demo-extend.test.ts new file mode 100644 index 000000000000..4940e499e235 --- /dev/null +++ b/components/chatbox/__tests__/demo-extend.test.ts @@ -0,0 +1,3 @@ +import { extendTest } from '../../../tests/shared/demoTest'; + +extendTest('chatbox'); diff --git a/components/chatbox/__tests__/demo.test.ts b/components/chatbox/__tests__/demo.test.ts new file mode 100644 index 000000000000..549211096c11 --- /dev/null +++ b/components/chatbox/__tests__/demo.test.ts @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('chatbox'); diff --git a/components/chatbox/__tests__/image.test.ts b/components/chatbox/__tests__/image.test.ts new file mode 100644 index 000000000000..7e79d7a14b77 --- /dev/null +++ b/components/chatbox/__tests__/image.test.ts @@ -0,0 +1,5 @@ +import { imageDemoTest } from '../../../tests/shared/imageTest'; + +describe('chatbox image', () => { + imageDemoTest('chatbox'); +}); diff --git a/components/chatbox/__tests__/index.test.tsx b/components/chatbox/__tests__/index.test.tsx new file mode 100644 index 000000000000..9289b0f43447 --- /dev/null +++ b/components/chatbox/__tests__/index.test.tsx @@ -0,0 +1,102 @@ +import React from 'react'; + +import Chatbox from '..'; +import mountTest from '../../../tests/shared/mountTest'; +import rtlTest from '../../../tests/shared/rtlTest'; +import { render, waitFakeTimer } from '../../../tests/utils'; + +describe('chatbox', () => { + mountTest(() => ); + rtlTest(() => ); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('Chatbox component work', () => { + const { container } = render(); + const element = container.querySelector('.ant-chatbox'); + expect(element).toBeTruthy(); + expect(element).toMatchSnapshot(); + }); + + it('Chatbox support content', () => { + const { container } = render(); + const element = container.querySelector('.ant-chatbox .ant-chatbox-content'); + expect(element?.textContent).toBe('hello world'); + }); + + it('Chatbox support contentRender', () => { + const { container } = render( + {content}} + />, + ); + const element = container.querySelector('.ant-chatbox .test-contentRender'); + expect(element).toBeTruthy(); + expect(element?.textContent).toBe('test-contentRender'); + }); + + it('Chatbox support typing', () => { + const { container } = render(); + expect(container.querySelector('.ant-chatbox')).toHaveClass( + 'ant-chatbox-typing', + ); + }); + + it('Chatbox support avatar', () => { + const { container } = render( + avatar} content="" />, + ); + expect(container.querySelector('.ant-chatbox .test-avatar')).toBeTruthy(); + }); + + it('Chatbox support loading', () => { + const { container } = render(); + const selectors = '.ant-chatbox .ant-chatbox-content .ant-chatbox-dot'; + expect(container.querySelector(selectors)).toBeTruthy(); + }); + + it('Chatbox support placement', () => { + const { container, rerender } = render(); + const element = container.querySelector('.ant-chatbox'); + expect(element).toHaveClass('ant-chatbox-start'); + rerender(); + expect(element).toHaveClass('ant-chatbox-end'); + }); + + it('Chatbox support typing effect', async () => { + const { container } = render(); + const element = container.querySelector('.ant-chatbox .ant-chatbox-content'); + expect(element?.textContent).toBe('你'); + await waitFakeTimer(); + expect(element?.textContent).toBe('你好你好你好'); + }); + + it('Chatbox Should support className & classNames & style & styles', () => { + const { container } = render( + avatar} + className="test-className" + classNames={{ avatar: 'test-avatar', content: 'test-content' }} + style={{ backgroundColor: 'green' }} + styles={{ avatar: { color: 'red' }, content: { color: 'blue' } }} + />, + ); + const element = container.querySelector('.ant-chatbox'); + const avatarElement = element?.querySelector('.ant-chatbox-avatar'); + const contentElement = element?.querySelector('.ant-chatbox-content'); + expect(element).toHaveClass('test-className'); + expect(avatarElement).toHaveClass('test-avatar'); + expect(contentElement).toHaveClass('test-content'); + expect(element).toHaveStyle({ backgroundColor: 'green' }); + expect(avatarElement).toHaveStyle({ color: 'red' }); + expect(contentElement).toHaveStyle({ color: 'blue' }); + }); +}); diff --git a/components/chatbox/demo/_semantic.tsx b/components/chatbox/demo/_semantic.tsx new file mode 100644 index 000000000000..6c93b9fce49d --- /dev/null +++ b/components/chatbox/demo/_semantic.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { UserOutlined } from '@ant-design/icons'; +import { Avatar, Chatbox } from 'antd'; + +import SemanticPreview from '../../../.dumi/components/SemanticPreview'; +import useLocale from '../../../.dumi/hooks/useLocale'; + +const locales = { + cn: { + avatar: '头像的外层容器', + content: '聊天内容的容器', + }, + en: { + avatar: 'Wrapper element of the avatar', + content: 'Wrapper element of the content', + }, +}; + +const App: React.FC = () => { + const [locale] = useLocale(locales); + return ( + + } />} + /> + + ); +}; + +export default App; diff --git a/components/chatbox/demo/avatar-and-placement.md b/components/chatbox/demo/avatar-and-placement.md new file mode 100644 index 000000000000..6928e1e1c2b8 --- /dev/null +++ b/components/chatbox/demo/avatar-and-placement.md @@ -0,0 +1,7 @@ +## zh-CN + +通过 `avatar` 设置自定义头像,通过 `placement` 设置位置,提供了 `start`、`end` 两个选项。 + +## en-US + +Set custom avatar by `avatar` prop, set the placement of the message by `placement` prop, which has two preset values: `start` and `end`. diff --git a/components/chatbox/demo/avatar-and-placement.tsx b/components/chatbox/demo/avatar-and-placement.tsx new file mode 100644 index 000000000000..02f7ab2adffb --- /dev/null +++ b/components/chatbox/demo/avatar-and-placement.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { UserOutlined } from '@ant-design/icons'; +import { Avatar, Chatbox, Flex } from 'antd'; + +const fooAvatar: React.CSSProperties = { + color: '#f56a00', + backgroundColor: '#fde3cf', +}; + +const barAvatar: React.CSSProperties = { + color: '#fff', + backgroundColor: '#87d068', +}; + +const hideAvatar: React.CSSProperties = { + visibility: 'hidden', +}; + +const App: React.FC = () => ( + + } style={fooAvatar} />} + /> + } + /> + } style={barAvatar} />} + /> + } + /> + +); + +export default App; diff --git a/components/chatbox/demo/basic.md b/components/chatbox/demo/basic.md new file mode 100644 index 000000000000..673339b1ba7d --- /dev/null +++ b/components/chatbox/demo/basic.md @@ -0,0 +1,7 @@ +## zh-CN + +基础用法。 + +## en-US + +Basic usage. diff --git a/components/chatbox/demo/basic.tsx b/components/chatbox/demo/basic.tsx new file mode 100644 index 000000000000..329c4fd82e58 --- /dev/null +++ b/components/chatbox/demo/basic.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Chatbox } from 'antd'; + +const App = () => ; + +export default App; diff --git a/components/chatbox/demo/contentRender.md b/components/chatbox/demo/contentRender.md new file mode 100644 index 000000000000..a985e8857d5a --- /dev/null +++ b/components/chatbox/demo/contentRender.md @@ -0,0 +1,7 @@ +## zh-CN + +配合 `markdown-it` 实现自定义渲染内容。 + +## en-US + +Cooperate with `markdown-it` to achieve customized rendering content. diff --git a/components/chatbox/demo/contentRender.tsx b/components/chatbox/demo/contentRender.tsx new file mode 100644 index 000000000000..a0a9e39bc8d7 --- /dev/null +++ b/components/chatbox/demo/contentRender.tsx @@ -0,0 +1,47 @@ +/* eslint-disable react/no-danger */ +import React from 'react'; +import { UserOutlined } from '@ant-design/icons'; +import { Avatar, Chatbox } from 'antd'; +import type { ChatboxProps } from 'antd'; +import markdownit from 'markdown-it'; + +const sentences = [ + '# Title \n An enterprise-class UI design language and React UI library. \n ...丨', + '# 标题 \n 企业级产品设计体系,创造高效愉悦的工作体验。\n ...丨', +]; + +const md = markdownit({ html: true, breaks: true }); + +const useLoopSentence = () => { + const [index, setIndex] = React.useState(0); + const timerRef = React.useRef>(); + React.useEffect(() => { + timerRef.current = setTimeout( + () => setIndex((prevState) => (prevState ? 0 : 1)), + sentences[index].length * 100 + 1000, + ); + return () => clearTimeout(timerRef.current); + }, [index]); + return sentences[index]; +}; + +const contentRender: ChatboxProps['contentRender'] = (content) => { + if (!content) { + return null; + } + return ; +}; + +const App: React.FC = () => { + const content = useLoopSentence(); + return ( + } />} + /> + ); +}; + +export default App; diff --git a/components/chatbox/demo/loading.md b/components/chatbox/demo/loading.md new file mode 100644 index 000000000000..8f0c95770dff --- /dev/null +++ b/components/chatbox/demo/loading.md @@ -0,0 +1,7 @@ +## zh-CN + +通过 `loading` 属性控制加载状态。 + +## en-US + +Control the loading state by `loading` prop. diff --git a/components/chatbox/demo/loading.tsx b/components/chatbox/demo/loading.tsx new file mode 100644 index 000000000000..5812ff7b167b --- /dev/null +++ b/components/chatbox/demo/loading.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { UserOutlined } from '@ant-design/icons'; +import { Avatar, Chatbox, Flex, Switch } from 'antd'; + +const App: React.FC = () => { + const [loading, setLoading] = React.useState(true); + return ( + + } />} + /> + + Loading state: + + + + ); +}; + +export default App; diff --git a/components/chatbox/demo/typing.md b/components/chatbox/demo/typing.md new file mode 100644 index 000000000000..0715f21b0875 --- /dev/null +++ b/components/chatbox/demo/typing.md @@ -0,0 +1,7 @@ +## zh-CN + +通过设置 `typing` 属性,开启打字效果。 + +## en-US + +Enable typing output by setting the `typing` prop. diff --git a/components/chatbox/demo/typing.tsx b/components/chatbox/demo/typing.tsx new file mode 100644 index 000000000000..5e432508da83 --- /dev/null +++ b/components/chatbox/demo/typing.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { UserOutlined } from '@ant-design/icons'; +import { Avatar, Chatbox } from 'antd'; + +const sentences = ['Feel free to use Ant Design !', '欢迎使用 Ant Design!']; + +const useLoopSentence = () => { + const [index, setIndex] = React.useState(0); + const timerRef = React.useRef>(); + React.useEffect(() => { + timerRef.current = setTimeout( + () => setIndex((prevState) => (prevState ? 0 : 1)), + sentences[index].length * 100 + 1000, + ); + return () => clearTimeout(timerRef.current); + }, [index]); + return sentences[index]; +}; + +const App: React.FC = () => { + const content = useLoopSentence(); + return ( + } />} + /> + ); +}; + +export default App; diff --git a/components/chatbox/hooks/useTypedEffect.ts b/components/chatbox/hooks/useTypedEffect.ts new file mode 100644 index 000000000000..1565ba5b955e --- /dev/null +++ b/components/chatbox/hooks/useTypedEffect.ts @@ -0,0 +1,46 @@ +import React from 'react'; + +import type { TypingOption } from '../interface'; + +const useTypedEffect = (content?: string, mergedTyping?: Required | false) => { + const [typedContent, setTypedContent] = React.useState(''); + const [isTyping, setIsTyping] = React.useState(mergedTyping !== false); + + const timerRef = React.useRef>(); + + const clearTimer = () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + + React.useEffect(() => { + if (!content || !mergedTyping) { + return; + } + setIsTyping(true); + let stepCount = 0; + const { step, interval } = mergedTyping; + + const typedTimer = () => { + stepCount += step; + setTypedContent(content.slice(0, stepCount) ?? ''); + if (stepCount < content.length) { + timerRef.current = setTimeout(typedTimer, interval); + } else { + setIsTyping(false); + } + }; + + typedTimer(); + + return () => { + clearTimer(); + setIsTyping(false); + }; + }, [content, mergedTyping]); + + return { typedContent, isTyping }; +}; + +export default useTypedEffect; diff --git a/components/chatbox/hooks/useTypingValue.ts b/components/chatbox/hooks/useTypingValue.ts new file mode 100644 index 000000000000..bd1975327f54 --- /dev/null +++ b/components/chatbox/hooks/useTypingValue.ts @@ -0,0 +1,30 @@ +import React from 'react'; + +import type { ChatboxProps, TypingOption } from '../interface'; + +function isObject(value: any): value is Record { + return value && typeof value === 'object'; +} + +const defaultTypingOption: Required = { + step: 1, + interval: 100, +}; + +const useTypingValue = (typing: ChatboxProps['typing']) => { + const mergedTyping = React.useMemo | false>( + () => { + if (isObject(typing)) { + return { ...defaultTypingOption, ...typing }; + } + if (typing === true) { + return defaultTypingOption; + } + return false; + }, + isObject(typing) ? [typing.interval, typing.step] : [typing], + ); + return mergedTyping; +}; + +export default useTypingValue; diff --git a/components/chatbox/index.en-US.md b/components/chatbox/index.en-US.md new file mode 100644 index 000000000000..555a15afe175 --- /dev/null +++ b/components/chatbox/index.en-US.md @@ -0,0 +1,51 @@ +--- +category: Components +group: Data Display +title: Chatbox +description: A bubble component for chat. +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*NMvqRZpuJfQAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*D70qQJJmzhgAAAAAAAAAAAAADrJ8AQ/original +demo: + cols: 2 +tag: 5.19.0 +--- + +## When To Use + +Often used when chatting. + +## Examples + + +Basic +Placement and avatar +Loading +Typing effect +Content render + +## API + +Common props ref:[Common props](/docs/react/common-props) + +> This component is available since `antd@5.19.0`. + +### Chatbox + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| avatar | Avatar component | `React.ReactNode` | - | | +| classNames | Semantic DOM class | [Record](#semantic-dom) | - | | +| styles | Semantic DOM style | [Record](#semantic-dom) | - | | +| placement | Direction of Message | `start \| end` | `start` | | +| loading | Loading state of Message | `boolean` | - | | +| typing | Show message with typing motion | `boolean \| { step?: number, interval?: number }` | `false` | | +| content | Content of Chatbox | `string` | - | | +| contentRender | Display cuztomized content | `(content?: string) => ReactNode` | - | | + +## Semantic DOM + + + +## Design Token + + diff --git a/components/chatbox/index.tsx b/components/chatbox/index.tsx new file mode 100644 index 000000000000..a48455b10446 --- /dev/null +++ b/components/chatbox/index.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import classnames from 'classnames'; + +import { ConfigContext } from '../config-provider'; +import type { ConfigConsumerProps } from '../config-provider'; +import useTypedEffect from './hooks/useTypedEffect'; +import useTypingValue from './hooks/useTypingValue'; +import type { ChatboxProps } from './interface'; +import Loading from './loading'; +import useStyle from './style'; + +const Chatbox: React.FC = (props) => { + const { + prefixCls: customizePrefixCls, + className, + rootClassName, + style, + classNames, + styles, + avatar, + placement = 'start', + loading = false, + typing, + content, + contentRender, + ...otherHtmlProps + } = props; + const { direction, chatbox, getPrefixCls } = React.useContext(ConfigContext); + const prefixCls = getPrefixCls('chatbox', customizePrefixCls); + const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); + + const mergedTyping = useTypingValue(typing); + + const { typedContent, isTyping } = useTypedEffect(content, mergedTyping); + + const mergedCls = classnames( + className, + rootClassName, + chatbox?.className, + prefixCls, + hashId, + cssVarCls, + `${prefixCls}-${placement}`, + { + [`${prefixCls}-rtl`]: direction === 'rtl', + [`${prefixCls}-typing`]: isTyping && !loading && !contentRender, + }, + ); + + const mergedAvatarCls = classnames( + `${prefixCls}-avatar`, + classNames?.avatar, + chatbox?.classNames?.avatar, + ); + + const mergedContentCls = classnames( + `${prefixCls}-content`, + classNames?.content, + chatbox?.classNames?.content, + ); + + const mergedText = mergedTyping !== false ? typedContent : content; + + const mergedContent = contentRender ? contentRender(mergedText) : mergedText; + + return wrapCSSVar( +
+ {avatar && ( +
+ {avatar} +
+ )} +
+ {loading ? : mergedContent} +
+
, + ); +}; + +if (process.env.NODE_ENV !== 'production') { + Chatbox.displayName = 'Chatbox'; +} + +export type { ChatboxProps }; + +export default Chatbox; diff --git a/components/chatbox/index.zh-CN.md b/components/chatbox/index.zh-CN.md new file mode 100644 index 000000000000..d16f2cc4b5b5 --- /dev/null +++ b/components/chatbox/index.zh-CN.md @@ -0,0 +1,52 @@ +--- +category: Components +group: 数据展示 +title: Chatbox +subtitle: 聊天框 +description: 用于聊天的气泡组件。 +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*NMvqRZpuJfQAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*D70qQJJmzhgAAAAAAAAAAAAADrJ8AQ/original +demo: + cols: 2 +tag: 5.19.0 +--- + +## 何时使用 + +常用于聊天的时候。 + +## 代码演示 + + +基本 +支持位置和头像 +加载中 +打字效果 +自定义渲染 + +## API + +通用属性参考:[通用属性](/docs/react/common-props) + +> 自 `antd@5.19.0` 版本开始提供该组件。 + +### Chatbox + +| 属性 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| avatar | 展示头像 | `React.ReactNode` | - | | +| classNames | 语义化结构 class | [Record](#semantic-dom) | - | | +| styles | 语义化结构 style | [Record](#semantic-dom) | - | | +| placement | 信息位置 | `start \| end` | `start` | | +| loading | 聊天内容加载状态 | `boolean` | - | | +| typing | 设置聊天内容打字动画 | `boolean \| { step?: number, interval?: number }` | `false` | | +| content | 聊天内容 | `string` | - | | +| contentRender | 自定义渲染内容 | `(content?: string) => ReactNode` | - | | + +## Semantic DOM + + + +## 主题变量(Design Token) + + diff --git a/components/chatbox/interface.ts b/components/chatbox/interface.ts new file mode 100644 index 000000000000..6d51e133189e --- /dev/null +++ b/components/chatbox/interface.ts @@ -0,0 +1,31 @@ +export interface TypingOption { + /** + * @since 5.19.0 + * @default 1 + */ + step?: number; + /** + * @since 5.19.0 + * @default 100 + */ + interval?: number; +} + +export interface ChatboxProps extends React.HTMLAttributes { + prefixCls?: string; + rootClassName?: string; + classNames?: { + avatar?: string; + content?: string; + }; + styles?: { + avatar?: React.CSSProperties; + content?: React.CSSProperties; + }; + avatar?: React.ReactNode; + placement?: 'start' | 'end'; + loading?: boolean; + typing?: boolean | TypingOption; + content: string; + contentRender?: (content?: string) => React.ReactNode; +} diff --git a/components/chatbox/loading.tsx b/components/chatbox/loading.tsx new file mode 100644 index 000000000000..5e4bd3afcbe7 --- /dev/null +++ b/components/chatbox/loading.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +interface LoadingProps { + prefixCls?: string; +} + +const Loading: React.FC = (props) => { + const { prefixCls } = props; + return ( + + + + + + ); +}; + +export default Loading; diff --git a/components/chatbox/style/index.ts b/components/chatbox/style/index.ts new file mode 100644 index 000000000000..cbf4a86baafb --- /dev/null +++ b/components/chatbox/style/index.ts @@ -0,0 +1,131 @@ +import { Keyframes, unit } from '@ant-design/cssinjs'; + +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal'; +import { genStyleHooks, mergeToken } from '../../theme/internal'; + +const loadingMove = new Keyframes('loadingMove', { + '0%': { + transform: 'translateY(0)', + }, + '10%': { + transform: 'translateY(4px)', + }, + '20%': { + transform: 'translateY(0)', + }, + '30%': { + transform: 'translateY(-4px)', + }, + '40%': { + transform: 'translateY(0)', + }, +}); + +const cursorBlink = new Keyframes('cursorBlink', { + '0%': { + opacity: 1, + }, + '50%': { + opacity: 0, + }, + '100%': { + opacity: 1, + }, +}); + +export interface ComponentToken { + // +} + +export interface ChatboxToken extends FullToken<'Chatbox'> { + chatboxContentMaxWidth: number | string; +} + +const genChatboxStyle: GenerateStyle = (token) => { + const { componentCls, fontSize, lineHeight, paddingSM, padding, paddingXS, colorText, calc } = + token; + return { + [componentCls]: { + display: 'flex', + columnGap: paddingXS, + maxWidth: '100%', + [`&${componentCls}-end`]: { + justifyContent: 'end', + flexDirection: 'row-reverse', + }, + [`&${componentCls}-rtl`]: { + direction: 'rtl', + }, + [`&${componentCls}-typing ${componentCls}-content:last-child::after`]: { + content: '"|"', + fontWeight: 900, + userSelect: 'none', + opacity: 1, + marginInlineStart: '0.1em', + animationName: cursorBlink, + animationDuration: '0.8s', + animationIterationCount: 'infinite', + animationTimingFunction: 'linear', + }, + [`& ${componentCls}-avatar`]: { + display: 'inline-flex', + justifyContent: 'center', + }, + [`& ${componentCls}-content`]: { + position: 'relative', + padding: `${unit(paddingSM)} ${unit(padding)}`, + color: colorText, + fontSize: token.fontSize, + lineHeight: token.lineHeight, + minHeight: calc(paddingSM).mul(2).add(calc(lineHeight).mul(fontSize)).equal(), + maxWidth: token.chatboxContentMaxWidth, + backgroundColor: token.colorInfoBg, + borderRadius: token.borderRadiusLG, + boxShadow: token.boxShadowTertiary, + [`& ${componentCls}-dot`]: { + position: 'relative', + height: '100%', + display: 'flex', + alignItems: 'center', + columnGap: token.marginXS, + padding: `0 ${unit(token.paddingXXS)}`, + '&-item': { + backgroundColor: token.colorPrimary, + borderRadius: '100%', + width: 4, + height: 4, + animationName: loadingMove, + animationDuration: '2s', + animationIterationCount: 'infinite', + animationTimingFunction: 'linear', + '&:nth-child(1)': { + animationDelay: '0s', + }, + '&:nth-child(2)': { + animationDelay: '0.2s', + }, + '&:nth-child(3)': { + animationDelay: '0.4s', + }, + }, + }, + }, + }, + }; +}; + +export const prepareComponentToken: GetDefaultToken<'Chatbox'> = () => ({ + // +}); + +export default genStyleHooks<'Chatbox'>( + 'Chatbox', + (token) => { + const { paddingXS, calc } = token; + const chatBoxToken = mergeToken(token, { + chatboxContentMaxWidth: `calc(100% - ${calc(paddingXS).add(32).equal()})`, + }); + return genChatboxStyle(chatBoxToken); + }, + prepareComponentToken, +); diff --git a/components/config-provider/__tests__/style.test.tsx b/components/config-provider/__tests__/style.test.tsx index 4050da315f07..c37da8ef88b1 100644 --- a/components/config-provider/__tests__/style.test.tsx +++ b/components/config-provider/__tests__/style.test.tsx @@ -11,6 +11,7 @@ import Calendar from '../../calendar'; import Card from '../../card'; import Carousel from '../../carousel'; import Cascader from '../../cascader'; +import Chatbox from '../../chatbox'; import Checkbox from '../../checkbox'; import Collapse from '../../collapse'; import ColorPicker from '../../color-picker'; @@ -1575,4 +1576,28 @@ describe('ConfigProvider support style and className props', () => { const element = container.querySelector('.test-cp-icon'); expect(element).toBeTruthy(); }); + + it('CP Should support Chatbox', () => { + const { container } = render( + + avatar
} /> + , + ); + const element = container.querySelector('.ant-chatbox'); + const avatarElement = element?.querySelector('.ant-chatbox-avatar'); + const contentElement = element?.querySelector('.ant-chatbox-content'); + expect(element).toHaveClass('test-cp-className'); + expect(avatarElement).toHaveClass('test-cp-avatar'); + expect(contentElement).toHaveClass('test-cp-content'); + expect(element).toHaveStyle({ backgroundColor: 'green' }); + expect(avatarElement).toHaveStyle({ color: 'red' }); + expect(contentElement).toHaveStyle({ color: 'blue' }); + }); }); diff --git a/components/config-provider/context.ts b/components/config-provider/context.ts index 2e20f2249b91..4d1fc72a5f67 100644 --- a/components/config-provider/context.ts +++ b/components/config-provider/context.ts @@ -6,6 +6,7 @@ import type { AlertProps } from '../alert'; import type { BadgeProps } from '../badge'; import type { ButtonProps } from '../button'; import type { CardProps } from '../card'; +import type { ChatboxProps } from '../chatbox/interface'; import type { CollapseProps } from '../collapse'; import type { DrawerProps } from '../drawer'; import type { FlexProps } from '../flex/interface'; @@ -150,6 +151,8 @@ export type TagConfig = ComponentStyleConfig & Pick; +export type ChatboxConfig = ComponentStyleConfig & Pick; + export type DrawerConfig = ComponentStyleConfig & Pick; @@ -222,6 +225,7 @@ export interface ConfigConsumerProps { calendar?: ComponentStyleConfig; carousel?: ComponentStyleConfig; cascader?: ComponentStyleConfig; + chatbox?: ChatboxConfig; collapse?: CollapseConfig; floatButtonGroup?: FloatButtonGroupConfig; typography?: ComponentStyleConfig; diff --git a/components/config-provider/index.en-US.md b/components/config-provider/index.en-US.md index 942cd4defec7..5bc4af31baca 100644 --- a/components/config-provider/index.en-US.md +++ b/components/config-provider/index.en-US.md @@ -106,6 +106,7 @@ const { | breadcrumb | Set Breadcrumb common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 | | button | Set Button common props | { className?: string, style?: React.CSSProperties, classNames?: { icon: string }, styles?: { icon: React.CSSProperties }, autoInsertSpace?: boolean } | - | 5.6.0, autoInsertSpace: 5.17.0 | | card | Set Card common props | { className?: string, style?: React.CSSProperties, classNames?: [CardProps\["classNames"\]](/components/card#api), styles?: [CardProps\["styles"\]](/components/card#api) } | - | 5.7.0, `classNames` and `styles`: 5.14.0 | +| chatbox | Set Chatbox common props | { className?: string, style?: React.CSSProperties, classNames?: { avatar?: string; content?: string; }, styles?: { avatar?: CSSProperties, content?: CSSProperties } } | - | 5.19.0 | | calendar | Set Calendar common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 | | carousel | Set Carousel common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 | | cascader | Set Cascader common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 | diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx index 99e536e717a2..8fb58285243e 100644 --- a/components/config-provider/index.tsx +++ b/components/config-provider/index.tsx @@ -19,6 +19,7 @@ import type { BadgeConfig, ButtonConfig, CardConfig, + ChatboxConfig, CollapseConfig, ComponentStyleConfig, ConfigConsumerProps, @@ -162,6 +163,7 @@ export interface ConfigProviderProps { calendar?: ComponentStyleConfig; carousel?: ComponentStyleConfig; cascader?: ComponentStyleConfig; + chatbox?: ChatboxConfig; collapse?: CollapseConfig; divider?: ComponentStyleConfig; drawer?: DrawerConfig; @@ -320,6 +322,7 @@ const ProviderChildren: React.FC = (props) => { calendar, carousel, cascader, + chatbox, collapse, typography, checkbox, @@ -418,6 +421,7 @@ const ProviderChildren: React.FC = (props) => { cascader, collapse, typography, + chatbox, checkbox, descriptions, divider, diff --git a/components/config-provider/index.zh-CN.md b/components/config-provider/index.zh-CN.md index 4ee55456bde2..69bf60f3e1c4 100644 --- a/components/config-provider/index.zh-CN.md +++ b/components/config-provider/index.zh-CN.md @@ -109,6 +109,7 @@ const { | button | 设置 Button 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: { icon: string }, styles?: { icon: React.CSSProperties }, autoInsertSpace?: boolean } | - | 5.6.0, autoInsertSpace: 5.17.0 | | calendar | 设置 Calendar 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 | | card | 设置 Card 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: [CardProps\["classNames"\]](/components/card-cn#api), styles?: [CardProps\["styles"\]](/components/card-cn#api) } | - | 5.7.0, `classNames` 和 `styles`: 5.14.0 | +| chatbox | 设置 Chatbox 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: { avatar?: string; content?: string; }, styles?: { avatar?: CSSProperties, content?: CSSProperties } } | - | 5.19.0 | | carousel | 设置 Carousel 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 | | cascader | 设置 Cascader 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 | | checkbox | 设置 Checkbox 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 | diff --git a/components/index.ts b/components/index.ts index 905586012069..573060dda531 100644 --- a/components/index.ts +++ b/components/index.ts @@ -29,6 +29,8 @@ export type { CarouselProps } from './carousel'; export { default as Cascader } from './cascader'; export type { CascaderProps, CascaderAutoProps } from './cascader'; export type { CascaderPanelProps, CascaderPanelAutoProps } from './cascader/Panel'; +export { default as Chatbox } from './chatbox'; +export type { ChatboxProps } from './chatbox'; export { default as Checkbox } from './checkbox'; export type { CheckboxOptionType, CheckboxProps, CheckboxRef } from './checkbox'; export { default as Col } from './col'; diff --git a/components/theme/interface/components.ts b/components/theme/interface/components.ts index 2ecbe62e32c8..a4d7f94a411d 100644 --- a/components/theme/interface/components.ts +++ b/components/theme/interface/components.ts @@ -12,6 +12,7 @@ import type { ComponentToken as CalendarComponentToken } from '../../calendar/st import type { ComponentToken as CardComponentToken } from '../../card/style'; import type { ComponentToken as CarouselComponentToken } from '../../carousel/style'; import type { ComponentToken as CascaderComponentToken } from '../../cascader/style'; +import type { ComponentToken as ChatboxComponentToken } from '../../chatbox/style'; import type { ComponentToken as CheckboxComponentToken } from '../../checkbox/style'; import type { ComponentToken as CollapseComponentToken } from '../../collapse/style'; import type { ComponentToken as ColorPickerComponentToken } from '../../color-picker/style'; @@ -76,6 +77,7 @@ export interface ComponentTokenMap { Card?: CardComponentToken; Carousel?: CarouselComponentToken; Cascader?: CascaderComponentToken; + Chatbox?: ChatboxComponentToken; Checkbox?: CheckboxComponentToken; ColorPicker?: ColorPickerComponentToken; Collapse?: CollapseComponentToken; diff --git a/package.json b/package.json index ab9053c32691..f273310d976a 100644 --- a/package.json +++ b/package.json @@ -206,6 +206,7 @@ "@types/jquery": "^3.5.29", "@types/jsdom": "^21.1.6", "@types/lodash": "^4.17.0", + "@types/markdown-it": "^14.0.1", "@types/minimist": "^1.2.5", "@types/node": "^20.12.7", "@types/nprogress": "^0.2.3", @@ -285,6 +286,7 @@ "lodash": "^4.17.21", "lunar-typescript": "^1.7.5", "lz-string": "^1.5.0", + "markdown-it": "^14.1.0", "minimist": "^1.2.8", "mockdate": "^3.0.5", "node-fetch": "^3.3.2", diff --git a/scripts/__snapshots__/check-site.ts.snap b/scripts/__snapshots__/check-site.ts.snap index 40ca1dd0d32e..3d52a63028c0 100644 --- a/scripts/__snapshots__/check-site.ts.snap +++ b/scripts/__snapshots__/check-site.ts.snap @@ -56,6 +56,10 @@ exports[`site test Component components/checkbox en Page 1`] = `3`; exports[`site test Component components/checkbox zh Page 1`] = `3`; +exports[`site test Component components/chatbox en Page 1`] = `1`; + +exports[`site test Component components/chatbox zh Page 1`] = `1`; + exports[`site test Component components/collapse en Page 1`] = `2`; exports[`site test Component components/collapse zh Page 1`] = `2`; diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index 0a4758fc7e28..e2d99a784e71 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -16,6 +16,7 @@ exports[`antd dist files exports modules correctly 1`] = ` "Card", "Carousel", "Cascader", + "Chatbox", "Checkbox", "Col", "Collapse",