From 4832896a8e2e9cba458f4cab49e98fe08523bd78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 17 Oct 2024 14:24:43 +0800 Subject: [PATCH 01/28] chore: init --- .../__snapshots__/demo-extend.test.ts.snap | 859 ++++++++++++++++++ .../__tests__/__snapshots__/demo.test.ts.snap | 851 +++++++++++++++++ .../__snapshots__/index.test.tsx.snap | 11 + .../attachment/__tests__/demo-extend.test.ts | 3 + components/attachment/__tests__/demo.test.ts | 3 + components/attachment/__tests__/image.test.ts | 5 + .../attachment/__tests__/index.test.tsx | 90 ++ components/attachment/demo/_semantic.tsx | 57 ++ components/attachment/demo/basic.md | 7 + components/attachment/demo/basic.tsx | 64 ++ components/attachment/index.en-US.md | 55 ++ components/attachment/index.tsx | 198 ++++ components/attachment/index.zh-CN.md | 58 ++ components/attachment/interface.ts | 31 + components/attachment/style/index.ts | 75 ++ 15 files changed, 2367 insertions(+) create mode 100644 components/attachment/__tests__/__snapshots__/demo-extend.test.ts.snap create mode 100644 components/attachment/__tests__/__snapshots__/demo.test.ts.snap create mode 100644 components/attachment/__tests__/__snapshots__/index.test.tsx.snap create mode 100644 components/attachment/__tests__/demo-extend.test.ts create mode 100644 components/attachment/__tests__/demo.test.ts create mode 100644 components/attachment/__tests__/image.test.ts create mode 100644 components/attachment/__tests__/index.test.tsx create mode 100644 components/attachment/demo/_semantic.tsx create mode 100644 components/attachment/demo/basic.md create mode 100644 components/attachment/demo/basic.tsx create mode 100644 components/attachment/index.en-US.md create mode 100644 components/attachment/index.tsx create mode 100644 components/attachment/index.zh-CN.md create mode 100644 components/attachment/interface.ts create mode 100644 components/attachment/style/index.ts diff --git a/components/attachment/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/attachment/__tests__/__snapshots__/demo-extend.test.ts.snap new file mode 100644 index 00000000..8da94454 --- /dev/null +++ b/components/attachment/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -0,0 +1,859 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders components/prompts/demo/basic.tsx extend context correctly 1`] = ` +
+
+
+ ✨ Inspirational Sparks and Marvelous Tips +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+`; + +exports[`renders components/prompts/demo/basic.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/prompts/demo/disabled.tsx extend context correctly 1`] = ` +
+
+ ☕️ It's time to relax! +
+ +
+`; + +exports[`renders components/prompts/demo/disabled.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/prompts/demo/flex-vertical.tsx extend context correctly 1`] = ` +
+
+ 🤔 You might also want to ask: +
+ +
+`; + +exports[`renders components/prompts/demo/flex-vertical.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/prompts/demo/flex-wrap.tsx extend context correctly 1`] = ` +
+
+ ✨ Inspirational Sparks and Marvelous Tips +
+ +
+`; + +exports[`renders components/prompts/demo/flex-wrap.tsx extend context correctly 2`] = `[]`; diff --git a/components/attachment/__tests__/__snapshots__/demo.test.ts.snap b/components/attachment/__tests__/__snapshots__/demo.test.ts.snap new file mode 100644 index 00000000..855bca3c --- /dev/null +++ b/components/attachment/__tests__/__snapshots__/demo.test.ts.snap @@ -0,0 +1,851 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders components/prompts/demo/basic.tsx correctly 1`] = ` +
+
+
+ ✨ Inspirational Sparks and Marvelous Tips +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+`; + +exports[`renders components/prompts/demo/disabled.tsx correctly 1`] = ` +
+
+ ☕️ It's time to relax! +
+ +
+`; + +exports[`renders components/prompts/demo/flex-vertical.tsx correctly 1`] = ` +
+
+ 🤔 You might also want to ask: +
+ +
+`; + +exports[`renders components/prompts/demo/flex-wrap.tsx correctly 1`] = ` +
+
+ ✨ Inspirational Sparks and Marvelous Tips +
+ +
+`; diff --git a/components/attachment/__tests__/__snapshots__/index.test.tsx.snap b/components/attachment/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000..5c369e8d --- /dev/null +++ b/components/attachment/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`bubble rtl render component should be rendered correctly in RTL direction 1`] = ` +
+
+`; diff --git a/components/attachment/__tests__/demo-extend.test.ts b/components/attachment/__tests__/demo-extend.test.ts new file mode 100644 index 00000000..be0fe341 --- /dev/null +++ b/components/attachment/__tests__/demo-extend.test.ts @@ -0,0 +1,3 @@ +import { extendTest } from '../../../tests/shared/demoTest'; + +extendTest('attachment'); diff --git a/components/attachment/__tests__/demo.test.ts b/components/attachment/__tests__/demo.test.ts new file mode 100644 index 00000000..69142771 --- /dev/null +++ b/components/attachment/__tests__/demo.test.ts @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('attachment'); diff --git a/components/attachment/__tests__/image.test.ts b/components/attachment/__tests__/image.test.ts new file mode 100644 index 00000000..ba8894fd --- /dev/null +++ b/components/attachment/__tests__/image.test.ts @@ -0,0 +1,5 @@ +import { imageDemoTest } from '../../../tests/shared/imageTest'; + +describe('attachment image', () => { + imageDemoTest('attachment'); +}); diff --git a/components/attachment/__tests__/index.test.tsx b/components/attachment/__tests__/index.test.tsx new file mode 100644 index 00000000..d05bae1d --- /dev/null +++ b/components/attachment/__tests__/index.test.tsx @@ -0,0 +1,90 @@ +import React from 'react'; + +import Attachment, { type PromptsProps } from '..'; +import mountTest from '../../../tests/shared/mountTest'; +import rtlTest from '../../../tests/shared/rtlTest'; +import { fireEvent, render } from '../../../tests/utils'; + +import type { PromptProps } from '../interface'; + +// Mock data +const mockData: PromptProps[] = [ + { + key: '1', + label: 'Label 1', + description: 'Description 1', + icon: Icon 1, + disabled: false, + }, + { + key: '2', + label: 'Label 2', + description: 'Description 2', + icon: Icon 2, + disabled: true, + }, +]; + +const mockProps: PromptsProps = { + title: 'Test Title', + items: mockData, + onItemClick: jest.fn(), + prefixCls: 'custom', +}; + +describe('bubble', () => { + mountTest(() => ); + rtlTest(() => ); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should render the title', () => { + const { getByText } = render(); + const titleElement = getByText(/test title/i); + expect(titleElement).toBeInTheDocument(); + }); + + it('should render the correct number of buttons', () => { + const { getAllByRole } = render(); + const buttons = getAllByRole('button'); + expect(buttons).toHaveLength(mockData.length); + }); + + it('should render the labels and descriptions', () => { + const { getByText } = render(); + mockData.forEach((item) => { + const label = getByText(item.label as string); + const description = getByText(item.description as string); + expect(label).toBeInTheDocument(); + expect(description).toBeInTheDocument(); + }); + }); + + it('should call onItemClick when a button is clicked', () => { + const { getByText } = render(); + const button = getByText(/label 1/i); + fireEvent.click(button); + expect(mockProps.onItemClick).toHaveBeenCalledWith({ data: mockData[0] }); + }); + + it('should disable buttons correctly', () => { + const { getByText } = render(); + const disabledButton = getByText(/label 2/i).closest('button'); + expect(disabledButton).toBeDisabled(); + }); + + it('should render icons', () => { + const { getByText } = render(); + mockData.forEach((item) => { + if (item.icon) { + const icon = getByText(`Icon ${item.key}`); + expect(icon).toBeInTheDocument(); + } + }); + }); +}); diff --git a/components/attachment/demo/_semantic.tsx b/components/attachment/demo/_semantic.tsx new file mode 100644 index 00000000..781f1a8d --- /dev/null +++ b/components/attachment/demo/_semantic.tsx @@ -0,0 +1,57 @@ +import { BulbOutlined, InfoCircleOutlined, RocketOutlined } from '@ant-design/icons'; +import { Prompts, type PromptsProps } from '@ant-design/x'; +import React from 'react'; +import SemanticPreview from '../../../.dumi/components/SemanticPreview'; +import useLocale from '../../../.dumi/hooks/useLocale'; + +const locales = { + cn: { title: '标题容器', list: '列表容器', item: '列表项', itemContent: '列表项内容' }, + en: { + title: 'Title container', + list: 'List container', + item: 'List item', + itemContent: 'List item content', + }, +}; + +const items: PromptsProps['items'] = [ + { + key: '1', + icon: , + label: 'Ignite Your Creativity', + description: 'Got any sparks for a new project?', + disabled: false, + }, + { + key: '2', + icon: , + label: 'Uncover Background Info', + description: 'Help me understand the background of this topic.', + disabled: false, + }, + { + key: '3', + icon: , + label: 'Efficiency Boost Battle', + description: 'How can I work faster and better?', + disabled: false, + }, +]; + +const App: React.FC = () => { + const [locale] = useLocale(locales); + return ( + + + + ); +}; + +export default App; diff --git a/components/attachment/demo/basic.md b/components/attachment/demo/basic.md new file mode 100644 index 00000000..673339b1 --- /dev/null +++ b/components/attachment/demo/basic.md @@ -0,0 +1,7 @@ +## zh-CN + +基础用法。 + +## en-US + +Basic usage. diff --git a/components/attachment/demo/basic.tsx b/components/attachment/demo/basic.tsx new file mode 100644 index 00000000..79b7c4c2 --- /dev/null +++ b/components/attachment/demo/basic.tsx @@ -0,0 +1,64 @@ +import { + BulbOutlined, + InfoCircleOutlined, + RocketOutlined, + SmileOutlined, + WarningOutlined, +} from '@ant-design/icons'; +import { Prompts } from '@ant-design/x'; +import type { PromptsProps } from '@ant-design/x'; +import { App } from 'antd'; +import React from 'react'; + +const items: PromptsProps['items'] = [ + { + key: '1', + icon: , + label: 'Ignite Your Creativity', + description: 'Got any sparks for a new project?', + }, + { + key: '2', + icon: , + label: 'Uncover Background Info', + description: 'Help me understand the background of this topic.', + }, + { + key: '3', + icon: , + label: 'Efficiency Boost Battle', + description: 'How can I work faster and better?', + }, + { + key: '4', + icon: , + label: 'Tell me a Joke', + description: 'Why do not ants get sick? Because they have tiny ant-bodies!', + }, + { + key: '5', + icon: , + label: 'Common Issue Solutions', + description: 'How to solve common issues? Share some tips!', + }, +]; + +const Demo = () => { + const { message } = App.useApp(); + + return ( + { + message.success(`You clicked a prompt: ${info.data.label}`); + }} + /> + ); +}; + +export default () => ( + + + +); diff --git a/components/attachment/index.en-US.md b/components/attachment/index.en-US.md new file mode 100644 index 00000000..feb194e1 --- /dev/null +++ b/components/attachment/index.en-US.md @@ -0,0 +1,55 @@ +--- +category: Components +group: + title: UI + order: 0 +title: Attachment +description: Display the collection of attachment information. +cover: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*1ysXSqEnAckAAAAAAAAAAAAADgCCAQ/original +coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*EkYUTotf-eYAAAAAAAAAAAAADgCCAQ/original +demo: + cols: 1 +--- + +## When To Use + +The Prompts component is used to display a predefined set of questions or suggestion that are relevant to the current context. + +## Examples + + +Basic + +## API + +### PromptsProps + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| classNames | Custom style class names for different parts of each prompt item. | Record<'list' \| 'item' \| 'content' \| 'title', string> | - | - | +| items | List containing multiple prompt items. | PromptProps[] | - | - | +| prefixCls | Prefix for style class names. | string | - | - | +| rootClassName | Style class name for the root node. | string | - | - | +| styles | Custom styles for different parts of each prompt item. | Record<'list' \| 'item' \| 'content' \| 'title', React.CSSProperties> | - | - | +| title | Title displayed at the top of the prompt list. | React.ReactNode | - | - | +| vertical | When set to `true`, the Prompts will be arranged vertically. | boolean | `false` | - | +| wrap | When set to `true`, the Prompts will automatically wrap. | boolean | `false` | - | +| onItemClick | Callback function when a prompt item is clicked. | (info: { data: PromptProps }) => void | - | - | + +### PromptProps + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| description | Prompt description providing additional information. | React.ReactNode | - | - | +| disabled | When set to `true`, click events are disabled. | boolean | `false` | - | +| icon | Prompt icon displayed on the left side of the prompt item. | React.ReactNode | - | - | +| key | Unique identifier used to distinguish each prompt item. | string | - | - | +| label | Prompt label displaying the main content of the prompt. | React.ReactNode | - | - | + +## Semantic DOM + + + +## Design Token + + diff --git a/components/attachment/index.tsx b/components/attachment/index.tsx new file mode 100644 index 00000000..5e7415c7 --- /dev/null +++ b/components/attachment/index.tsx @@ -0,0 +1,198 @@ +import { Button, Typography } from 'antd'; +import classnames from 'classnames'; +import React from 'react'; + +import useXComponentConfig from '../_util/hooks/use-x-component-config'; +import { useXProviderContext } from '../x-provider'; + +import useStyle from './style'; + +import type { PromptProps } from './interface'; + +export type SemanticType = 'list' | 'item' | 'itemContent' | 'title'; + +export interface PromptsProps + extends Omit, 'onClick' | 'title'> { + /** + * @desc 包含多个提示项的列表。 + * @descEN List containing multiple prompt items. + */ + items?: PromptProps[]; + + /** + * @desc 显示在提示列表顶部的标题。 + * @descEN Title displayed at the top of the prompt list. + */ + title?: React.ReactNode; + + /** + * @desc Item 提示项被点击时的回调函数。 + * @descEN Callback function when a prompt item is clicked. + */ + onItemClick?: (info: { data: PromptProps }) => void; + + /** + * @desc 提示列表是否垂直排列。 + * @descEN Whether the prompt list is arranged vertically. + */ + vertical?: boolean; + + /** + * @desc 提示列表是否换行。 + * @descEN Whether the prompt list is wrapped. + */ + wrap?: boolean; + + /** + * @desc 自定义样式,用于各个提示项的不同部分。 + * @descEN Custom styles for different parts of each prompt item. + */ + styles?: Partial>; + + /** + * @desc 自定义样式类名,用于各个提示项的不同部分。 + * @descEN Custom style class names for different parts of each prompt item. + */ + classNames?: Partial>; + + /** + * @desc 样式类名的前缀。 + * @descEN Prefix for style class names. + */ + prefixCls?: string; + + /** + * @desc 根节点的样式类名。 + * @descEN Style class name for the root node. + */ + rootClassName?: string; +} + +const Attachment: React.FC = (props) => { + const { + prefixCls: customizePrefixCls, + title, + className, + items, + onItemClick, + vertical, + wrap, + rootClassName, + styles = {}, + classNames = {}, + style, + ...htmlProps + } = props; + + // ============================ PrefixCls ============================ + const { getPrefixCls, direction } = useXProviderContext(); + + const prefixCls = getPrefixCls('prompts', customizePrefixCls); + + // ===================== Component Config ========================= + const contextConfig = useXComponentConfig('prompts'); + + // ============================ Style ============================ + const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); + + const mergedCls = classnames( + prefixCls, + contextConfig.className, + className, + rootClassName, + hashId, + cssVarCls, + { + [`${prefixCls}-rtl`]: direction === 'rtl', + }, + ); + + const mergedListCls = classnames( + `${prefixCls}-list`, + contextConfig.classNames.list, + classNames.list, + { [`${prefixCls}-list-wrap`]: wrap }, + { [`${prefixCls}-list-vertical`]: vertical }, + ); + + // ============================ Render ============================ + return wrapCSSVar( +
+ {/* Title */} + {title && ( + + {title} + + )} + {/* Prompt List */} +
    + {items?.map((info, index) => ( +
  • + {/* Prompt Item */} + +
  • + ))} +
+
, + ); +}; + +if (process.env.NODE_ENV !== 'production') { + Attachment.displayName = 'Attachment'; +} + +export type { PromptProps }; + +export default Attachment; diff --git a/components/attachment/index.zh-CN.md b/components/attachment/index.zh-CN.md new file mode 100644 index 00000000..7d1e5f32 --- /dev/null +++ b/components/attachment/index.zh-CN.md @@ -0,0 +1,58 @@ +--- +category: Components +group: + title: 用户界面 + order: 0 +title: Attachment +subtitle: 附件 +description: 用于展示一组附件信息集合。 +cover: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*1ysXSqEnAckAAAAAAAAAAAAADgCCAQ/original +coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*EkYUTotf-eYAAAAAAAAAAAAADgCCAQ/original +demo: + cols: 1 +--- + +## 何时使用 + +Attachment 组件用于需要展示一组附件信息集合的场景。 + +## 代码演示 + + +基本 + +## API + +通用属性参考:[通用属性](/docs/react/common-props) + +### PromptsProps + +| 属性 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| classNames | 自定义样式类名,用于各个提示项的不同部分。 | Record<'list' \| 'item' \| 'content' \| 'title', string> | - | - | +| items | 包含多个提示项的列表。 | PromptProps[] | - | - | +| prefixCls | 样式类名的前缀。 | string | - | - | +| rootClassName | 根节点的样式类名。 | string | - | - | +| styles | 自定义样式,用于各个提示项的不同部分。 | Record<'list' \| 'item' \| 'content' \| 'title', React.CSSProperties> | - | - | +| title | 显示在提示列表顶部的标题。 | React.ReactNode | - | - | +| vertical | 设置为 `true` 时, 提示列表将垂直排列。 | boolean | `false` | - | +| wrap | 设置为 `true` 时, 提示列表将自动换行。 | boolean | `false` | - | +| onItemClick | 提示项被点击时的回调函数。 | (info: { data: PromptProps }) => void | - | - | + +### PromptProps + +| 属性 | 说明 | 类型 | 默认值 | 版本 | +| ----------- | ------------------------------ | --------------- | ------- | ---- | +| description | 提示描述提供额外的信息。 | React.ReactNode | - | - | +| disabled | 设置为 `true` 时禁用点击事件。 | boolean | `false` | - | +| icon | 提示图标显示在提示项的左侧。 | React.ReactNode | - | - | +| key | 唯一标识用于区分每个提示项。 | string | - | - | +| label | 提示标签显示提示的主要内容。 | React.ReactNode | - | - | + +## Semantic DOM + + + +## 主题变量(Design Token) + + diff --git a/components/attachment/interface.ts b/components/attachment/interface.ts new file mode 100644 index 00000000..840560fc --- /dev/null +++ b/components/attachment/interface.ts @@ -0,0 +1,31 @@ +export interface PromptProps { + /** + * @desc 唯一标识用于区分每个提示项。 + * @descEN Unique identifier used to distinguish each prompt item. + */ + key: string; + + /** + * @desc 提示图标显示在提示项的左侧。 + * @descEN Prompt icon displayed on the left side of the prompt item. + */ + icon?: React.ReactNode; + + /** + * @desc 提示标签显示提示的主要内容。 + * @descEN Prompt label displaying the main content of the prompt. + */ + label?: React.ReactNode; + + /** + * @desc 提示描述提供额外的信息。 + * @descEN Prompt description providing additional information. + */ + description?: React.ReactNode; + + /** + * @desc 设置为 true 时禁用点击事件。 + * @descEN When set to true, click events are disabled. + */ + disabled?: boolean; +} diff --git a/components/attachment/style/index.ts b/components/attachment/style/index.ts new file mode 100644 index 00000000..be83241b --- /dev/null +++ b/components/attachment/style/index.ts @@ -0,0 +1,75 @@ +import { unit } from '@ant-design/cssinjs'; +import { mergeToken } from '@ant-design/cssinjs-utils'; +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/cssinjs-utils'; +import { genStyleHooks } from '../../theme/genStyleUtils'; + +// biome-ignore lint/suspicious/noEmptyInterface: ComponentToken need to be empty by default +export interface ComponentToken {} + +export interface PromptsToken extends FullToken<'Prompts'> {} + +const genPromptsStyle: GenerateStyle = (token) => { + const { componentCls } = token; + + return { + [componentCls]: { + maxWidth: '100%', + + [`&${componentCls}-rtl`]: { + direction: 'rtl', + }, + [`& ${componentCls}-title`]: { + marginBlockStart: 0, + }, + + [`& ${componentCls}-list`]: { + display: 'flex', + gap: token.paddingSM, + overflowX: 'scroll', + '&::-webkit-scrollbar': { + display: 'none', + }, + listStyle: 'none', + paddingInlineStart: 0, + marginBlock: 0, + + '&-wrap': { + flexWrap: 'wrap', + }, + '&-vertical': { + flexDirection: 'column', + }, + }, + + // ========================= item ========================= + [`& ${componentCls}-item`]: { + display: 'flex', + gap: token.paddingSM, + height: 'auto', + padding: token.paddingSM, + alignItems: 'flex-start', + justifyContent: 'flex-start', + borderRadius: token.borderRadiusLG, + border: `${unit(token.lineWidth)} ${token.lineType} ${token.colorBorderSecondary}`, + + [`& ${componentCls}-content`]: { + display: 'inline-flex', + gap: token.paddingXS, + flexDirection: 'column', + alignItems: 'flex-start', + }, + }, + }, + }; +}; + +export const prepareComponentToken: GetDefaultToken<'Prompts'> = () => ({}); + +export default genStyleHooks( + 'Prompts', + (token) => { + const compToken = mergeToken(token, {}); + return genPromptsStyle(compToken); + }, + prepareComponentToken, +); From abb18c877628cded9348d3a46df3d41485cdeaf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 17 Oct 2024 18:01:59 +0800 Subject: [PATCH 02/28] docs: drap upload --- components/attachment/demo/basic.md | 7 - components/attachment/demo/basic.tsx | 64 ------ components/attachment/index.tsx | 198 ------------------ components/attachment/style/index.ts | 75 ------- components/attachments/DropUploader.tsx | 84 ++++++++ .../attachments/PlaceholderUploader.tsx | 34 +++ components/attachments/SilentUploader.tsx | 21 ++ .../__snapshots__/demo-extend.test.ts.snap | 0 .../__tests__/__snapshots__/demo.test.ts.snap | 0 .../__snapshots__/index.test.tsx.snap | 0 .../__tests__/demo-extend.test.ts | 0 .../__tests__/demo.test.ts | 0 .../__tests__/image.test.ts | 0 .../__tests__/index.test.tsx | 18 +- .../demo/_semantic.tsx | 0 components/attachments/demo/basic.md | 7 + components/attachments/demo/basic.tsx | 41 ++++ .../index.en-US.md | 0 components/attachments/index.tsx | 101 +++++++++ .../index.zh-CN.md | 0 .../{attachment => attachments}/interface.ts | 0 components/attachments/style/index.ts | 53 +++++ components/index.ts | 2 + components/theme/components.ts | 2 + components/x-provider/context.ts | 2 + package.json | 21 +- 26 files changed, 374 insertions(+), 356 deletions(-) delete mode 100644 components/attachment/demo/basic.md delete mode 100644 components/attachment/demo/basic.tsx delete mode 100644 components/attachment/index.tsx delete mode 100644 components/attachment/style/index.ts create mode 100644 components/attachments/DropUploader.tsx create mode 100644 components/attachments/PlaceholderUploader.tsx create mode 100644 components/attachments/SilentUploader.tsx rename components/{attachment => attachments}/__tests__/__snapshots__/demo-extend.test.ts.snap (100%) rename components/{attachment => attachments}/__tests__/__snapshots__/demo.test.ts.snap (100%) rename components/{attachment => attachments}/__tests__/__snapshots__/index.test.tsx.snap (100%) rename components/{attachment => attachments}/__tests__/demo-extend.test.ts (100%) rename components/{attachment => attachments}/__tests__/demo.test.ts (100%) rename components/{attachment => attachments}/__tests__/image.test.ts (100%) rename components/{attachment => attachments}/__tests__/index.test.tsx (78%) rename components/{attachment => attachments}/demo/_semantic.tsx (100%) create mode 100644 components/attachments/demo/basic.md create mode 100644 components/attachments/demo/basic.tsx rename components/{attachment => attachments}/index.en-US.md (100%) create mode 100644 components/attachments/index.tsx rename components/{attachment => attachments}/index.zh-CN.md (100%) rename components/{attachment => attachments}/interface.ts (100%) create mode 100644 components/attachments/style/index.ts diff --git a/components/attachment/demo/basic.md b/components/attachment/demo/basic.md deleted file mode 100644 index 673339b1..00000000 --- a/components/attachment/demo/basic.md +++ /dev/null @@ -1,7 +0,0 @@ -## zh-CN - -基础用法。 - -## en-US - -Basic usage. diff --git a/components/attachment/demo/basic.tsx b/components/attachment/demo/basic.tsx deleted file mode 100644 index 79b7c4c2..00000000 --- a/components/attachment/demo/basic.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - BulbOutlined, - InfoCircleOutlined, - RocketOutlined, - SmileOutlined, - WarningOutlined, -} from '@ant-design/icons'; -import { Prompts } from '@ant-design/x'; -import type { PromptsProps } from '@ant-design/x'; -import { App } from 'antd'; -import React from 'react'; - -const items: PromptsProps['items'] = [ - { - key: '1', - icon: , - label: 'Ignite Your Creativity', - description: 'Got any sparks for a new project?', - }, - { - key: '2', - icon: , - label: 'Uncover Background Info', - description: 'Help me understand the background of this topic.', - }, - { - key: '3', - icon: , - label: 'Efficiency Boost Battle', - description: 'How can I work faster and better?', - }, - { - key: '4', - icon: , - label: 'Tell me a Joke', - description: 'Why do not ants get sick? Because they have tiny ant-bodies!', - }, - { - key: '5', - icon: , - label: 'Common Issue Solutions', - description: 'How to solve common issues? Share some tips!', - }, -]; - -const Demo = () => { - const { message } = App.useApp(); - - return ( - { - message.success(`You clicked a prompt: ${info.data.label}`); - }} - /> - ); -}; - -export default () => ( - - - -); diff --git a/components/attachment/index.tsx b/components/attachment/index.tsx deleted file mode 100644 index 5e7415c7..00000000 --- a/components/attachment/index.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { Button, Typography } from 'antd'; -import classnames from 'classnames'; -import React from 'react'; - -import useXComponentConfig from '../_util/hooks/use-x-component-config'; -import { useXProviderContext } from '../x-provider'; - -import useStyle from './style'; - -import type { PromptProps } from './interface'; - -export type SemanticType = 'list' | 'item' | 'itemContent' | 'title'; - -export interface PromptsProps - extends Omit, 'onClick' | 'title'> { - /** - * @desc 包含多个提示项的列表。 - * @descEN List containing multiple prompt items. - */ - items?: PromptProps[]; - - /** - * @desc 显示在提示列表顶部的标题。 - * @descEN Title displayed at the top of the prompt list. - */ - title?: React.ReactNode; - - /** - * @desc Item 提示项被点击时的回调函数。 - * @descEN Callback function when a prompt item is clicked. - */ - onItemClick?: (info: { data: PromptProps }) => void; - - /** - * @desc 提示列表是否垂直排列。 - * @descEN Whether the prompt list is arranged vertically. - */ - vertical?: boolean; - - /** - * @desc 提示列表是否换行。 - * @descEN Whether the prompt list is wrapped. - */ - wrap?: boolean; - - /** - * @desc 自定义样式,用于各个提示项的不同部分。 - * @descEN Custom styles for different parts of each prompt item. - */ - styles?: Partial>; - - /** - * @desc 自定义样式类名,用于各个提示项的不同部分。 - * @descEN Custom style class names for different parts of each prompt item. - */ - classNames?: Partial>; - - /** - * @desc 样式类名的前缀。 - * @descEN Prefix for style class names. - */ - prefixCls?: string; - - /** - * @desc 根节点的样式类名。 - * @descEN Style class name for the root node. - */ - rootClassName?: string; -} - -const Attachment: React.FC = (props) => { - const { - prefixCls: customizePrefixCls, - title, - className, - items, - onItemClick, - vertical, - wrap, - rootClassName, - styles = {}, - classNames = {}, - style, - ...htmlProps - } = props; - - // ============================ PrefixCls ============================ - const { getPrefixCls, direction } = useXProviderContext(); - - const prefixCls = getPrefixCls('prompts', customizePrefixCls); - - // ===================== Component Config ========================= - const contextConfig = useXComponentConfig('prompts'); - - // ============================ Style ============================ - const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); - - const mergedCls = classnames( - prefixCls, - contextConfig.className, - className, - rootClassName, - hashId, - cssVarCls, - { - [`${prefixCls}-rtl`]: direction === 'rtl', - }, - ); - - const mergedListCls = classnames( - `${prefixCls}-list`, - contextConfig.classNames.list, - classNames.list, - { [`${prefixCls}-list-wrap`]: wrap }, - { [`${prefixCls}-list-vertical`]: vertical }, - ); - - // ============================ Render ============================ - return wrapCSSVar( -
- {/* Title */} - {title && ( - - {title} - - )} - {/* Prompt List */} -
    - {items?.map((info, index) => ( -
  • - {/* Prompt Item */} - -
  • - ))} -
-
, - ); -}; - -if (process.env.NODE_ENV !== 'production') { - Attachment.displayName = 'Attachment'; -} - -export type { PromptProps }; - -export default Attachment; diff --git a/components/attachment/style/index.ts b/components/attachment/style/index.ts deleted file mode 100644 index be83241b..00000000 --- a/components/attachment/style/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { unit } from '@ant-design/cssinjs'; -import { mergeToken } from '@ant-design/cssinjs-utils'; -import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/cssinjs-utils'; -import { genStyleHooks } from '../../theme/genStyleUtils'; - -// biome-ignore lint/suspicious/noEmptyInterface: ComponentToken need to be empty by default -export interface ComponentToken {} - -export interface PromptsToken extends FullToken<'Prompts'> {} - -const genPromptsStyle: GenerateStyle = (token) => { - const { componentCls } = token; - - return { - [componentCls]: { - maxWidth: '100%', - - [`&${componentCls}-rtl`]: { - direction: 'rtl', - }, - [`& ${componentCls}-title`]: { - marginBlockStart: 0, - }, - - [`& ${componentCls}-list`]: { - display: 'flex', - gap: token.paddingSM, - overflowX: 'scroll', - '&::-webkit-scrollbar': { - display: 'none', - }, - listStyle: 'none', - paddingInlineStart: 0, - marginBlock: 0, - - '&-wrap': { - flexWrap: 'wrap', - }, - '&-vertical': { - flexDirection: 'column', - }, - }, - - // ========================= item ========================= - [`& ${componentCls}-item`]: { - display: 'flex', - gap: token.paddingSM, - height: 'auto', - padding: token.paddingSM, - alignItems: 'flex-start', - justifyContent: 'flex-start', - borderRadius: token.borderRadiusLG, - border: `${unit(token.lineWidth)} ${token.lineType} ${token.colorBorderSecondary}`, - - [`& ${componentCls}-content`]: { - display: 'inline-flex', - gap: token.paddingXS, - flexDirection: 'column', - alignItems: 'flex-start', - }, - }, - }, - }; -}; - -export const prepareComponentToken: GetDefaultToken<'Prompts'> = () => ({}); - -export default genStyleHooks( - 'Prompts', - (token) => { - const compToken = mergeToken(token, {}); - return genPromptsStyle(compToken); - }, - prepareComponentToken, -); diff --git a/components/attachments/DropUploader.tsx b/components/attachments/DropUploader.tsx new file mode 100644 index 00000000..0aa88ee1 --- /dev/null +++ b/components/attachments/DropUploader.tsx @@ -0,0 +1,84 @@ +import classnames from 'classnames'; +import React from 'react'; +import { createPortal } from 'react-dom'; + +export interface DropUploaderProps { + prefixCls: string; + className: string; + getDropContainer?: null | (() => HTMLElement | null | undefined); + children?: React.ReactNode; +} + +export default function DropUploader(props: DropUploaderProps) { + const { getDropContainer, className, prefixCls, children } = props; + + const [container, setContainer] = React.useState(); + const [showArea, setShowArea] = React.useState(null); + + // ============================= MISC ============================= + + // ========================== Container =========================== + React.useEffect(() => { + const nextContainer = getDropContainer?.(); + if (container !== nextContainer) { + setContainer(nextContainer); + } + }, [getDropContainer]); + + // ============================= Drop ============================= + React.useEffect(() => { + // Add global drop event + if (container) { + const onDragEnter = (e: DragEvent) => { + setShowArea(true); + console.log('enter'); + }; + + // Should prevent default to make drop event work + const onDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + + const onDragLeave = (e: DragEvent) => { + if (!e.relatedTarget) { + setShowArea(false); + console.log('leave'); + } + }; + const onDrop = (e: DragEvent) => { + setShowArea(false); + e.preventDefault(); + + console.log('drop', e.defaultPrevented); + }; + + document.addEventListener('dragenter', onDragEnter); + document.addEventListener('dragover', onDragOver); + document.addEventListener('dragleave', onDragLeave); + document.addEventListener('drop', onDrop); + return () => { + document.removeEventListener('dragenter', onDragEnter); + document.removeEventListener('dragover', onDragOver); + document.removeEventListener('dragleave', onDragLeave); + document.removeEventListener('drop', onDrop); + }; + } + }, [!!container]); + + // ============================ Render ============================ + if (!getDropContainer || !container || showArea === null) { + return null; + } + + return createPortal( +
+ {children} +
, + container, + ); +} diff --git a/components/attachments/PlaceholderUploader.tsx b/components/attachments/PlaceholderUploader.tsx new file mode 100644 index 00000000..253c3ddd --- /dev/null +++ b/components/attachments/PlaceholderUploader.tsx @@ -0,0 +1,34 @@ +import { Flex, Typography, Upload, type UploadProps } from 'antd'; +import classNames from 'classnames'; +import React from 'react'; + +export interface PlaceholderProps { + prefixCls: string; + icon?: React.ReactNode; + title?: React.ReactNode; + description?: React.ReactNode; + className: string; + upload?: UploadProps; +} + +export default function Placeholder(props: PlaceholderProps) { + const { prefixCls, icon, title, description, className, upload } = props; + + const placeholderCls = `${prefixCls}-placeholder`; + + return ( +
+ + + {icon} + + {title} + + + {description} + + + +
+ ); +} diff --git a/components/attachments/SilentUploader.tsx b/components/attachments/SilentUploader.tsx new file mode 100644 index 00000000..db75bdab --- /dev/null +++ b/components/attachments/SilentUploader.tsx @@ -0,0 +1,21 @@ +import { Upload, type UploadProps } from 'antd'; +import React from 'react'; + +export interface SilentUploaderProps { + children: React.ReactElement; + upload: UploadProps; +} + +/** + * SilentUploader is only wrap children with antd Upload component. + */ +export default function SilentUploader(props: SilentUploaderProps) { + const { children, upload } = props; + + // ============================ Render ============================ + return ( + + {children} + + ); +} diff --git a/components/attachment/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/attachments/__tests__/__snapshots__/demo-extend.test.ts.snap similarity index 100% rename from components/attachment/__tests__/__snapshots__/demo-extend.test.ts.snap rename to components/attachments/__tests__/__snapshots__/demo-extend.test.ts.snap diff --git a/components/attachment/__tests__/__snapshots__/demo.test.ts.snap b/components/attachments/__tests__/__snapshots__/demo.test.ts.snap similarity index 100% rename from components/attachment/__tests__/__snapshots__/demo.test.ts.snap rename to components/attachments/__tests__/__snapshots__/demo.test.ts.snap diff --git a/components/attachment/__tests__/__snapshots__/index.test.tsx.snap b/components/attachments/__tests__/__snapshots__/index.test.tsx.snap similarity index 100% rename from components/attachment/__tests__/__snapshots__/index.test.tsx.snap rename to components/attachments/__tests__/__snapshots__/index.test.tsx.snap diff --git a/components/attachment/__tests__/demo-extend.test.ts b/components/attachments/__tests__/demo-extend.test.ts similarity index 100% rename from components/attachment/__tests__/demo-extend.test.ts rename to components/attachments/__tests__/demo-extend.test.ts diff --git a/components/attachment/__tests__/demo.test.ts b/components/attachments/__tests__/demo.test.ts similarity index 100% rename from components/attachment/__tests__/demo.test.ts rename to components/attachments/__tests__/demo.test.ts diff --git a/components/attachment/__tests__/image.test.ts b/components/attachments/__tests__/image.test.ts similarity index 100% rename from components/attachment/__tests__/image.test.ts rename to components/attachments/__tests__/image.test.ts diff --git a/components/attachment/__tests__/index.test.tsx b/components/attachments/__tests__/index.test.tsx similarity index 78% rename from components/attachment/__tests__/index.test.tsx rename to components/attachments/__tests__/index.test.tsx index d05bae1d..cc151248 100644 --- a/components/attachment/__tests__/index.test.tsx +++ b/components/attachments/__tests__/index.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Attachment, { type PromptsProps } from '..'; +import Attachments, { type PromptsProps } from '..'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; import { fireEvent, render } from '../../../tests/utils'; @@ -33,8 +33,8 @@ const mockProps: PromptsProps = { }; describe('bubble', () => { - mountTest(() => ); - rtlTest(() => ); + mountTest(() => ); + rtlTest(() => ); beforeAll(() => { jest.useFakeTimers(); }); @@ -44,19 +44,19 @@ describe('bubble', () => { }); it('should render the title', () => { - const { getByText } = render(); + const { getByText } = render(); const titleElement = getByText(/test title/i); expect(titleElement).toBeInTheDocument(); }); it('should render the correct number of buttons', () => { - const { getAllByRole } = render(); + const { getAllByRole } = render(); const buttons = getAllByRole('button'); expect(buttons).toHaveLength(mockData.length); }); it('should render the labels and descriptions', () => { - const { getByText } = render(); + const { getByText } = render(); mockData.forEach((item) => { const label = getByText(item.label as string); const description = getByText(item.description as string); @@ -66,20 +66,20 @@ describe('bubble', () => { }); it('should call onItemClick when a button is clicked', () => { - const { getByText } = render(); + const { getByText } = render(); const button = getByText(/label 1/i); fireEvent.click(button); expect(mockProps.onItemClick).toHaveBeenCalledWith({ data: mockData[0] }); }); it('should disable buttons correctly', () => { - const { getByText } = render(); + const { getByText } = render(); const disabledButton = getByText(/label 2/i).closest('button'); expect(disabledButton).toBeDisabled(); }); it('should render icons', () => { - const { getByText } = render(); + const { getByText } = render(); mockData.forEach((item) => { if (item.icon) { const icon = getByText(`Icon ${item.key}`); diff --git a/components/attachment/demo/_semantic.tsx b/components/attachments/demo/_semantic.tsx similarity index 100% rename from components/attachment/demo/_semantic.tsx rename to components/attachments/demo/_semantic.tsx diff --git a/components/attachments/demo/basic.md b/components/attachments/demo/basic.md new file mode 100644 index 00000000..337bb517 --- /dev/null +++ b/components/attachments/demo/basic.md @@ -0,0 +1,7 @@ +## zh-CN + +基础用法,可以通过 `getDropContainer` 支持拖拽上传。 + +## en-US + +Basic usage. You can use `getDropContainer` to support drag and drop upload. diff --git a/components/attachments/demo/basic.tsx b/components/attachments/demo/basic.tsx new file mode 100644 index 00000000..d6e24b00 --- /dev/null +++ b/components/attachments/demo/basic.tsx @@ -0,0 +1,41 @@ +import { CloudUploadOutlined } from '@ant-design/icons'; +import { Attachments } from '@ant-design/x'; +import { App, Button, Space, Switch } from 'antd'; +import React from 'react'; + +const Demo = () => { + const { message } = App.useApp(); + + const [fullScreenDrop, setFullScreenDrop] = React.useState(false); + + return ( + + + false} + onChange={({ file }) => { + message.info(`Mock upload: ${file.name}`); + }} + getDropContainer={fullScreenDrop ? () => document.body : null} + placeholder={{ + icon: , + title: 'Drag & Drop files here', + description: 'Support file type: image, video, audio, document, etc.', + }} + > + + + + ); +}; + +export default () => ( + + + +); diff --git a/components/attachment/index.en-US.md b/components/attachments/index.en-US.md similarity index 100% rename from components/attachment/index.en-US.md rename to components/attachments/index.en-US.md diff --git a/components/attachments/index.tsx b/components/attachments/index.tsx new file mode 100644 index 00000000..7273d45d --- /dev/null +++ b/components/attachments/index.tsx @@ -0,0 +1,101 @@ +import { Button, Typography } from 'antd'; +import type { GetProp, UploadProps } from 'antd'; +import classnames from 'classnames'; +import React from 'react'; + +import useXComponentConfig from '../_util/hooks/use-x-component-config'; +import { useXProviderContext } from '../x-provider'; + +import { useMergedState } from 'rc-util'; +import DropUploader from './DropUploader'; +import PlaceholderUploader, { PlaceholderProps } from './PlaceholderUploader'; +import SilentUploader from './SilentUploader'; +import useStyle from './style'; + +export type SemanticType = 'list' | 'item' | 'itemContent' | 'title'; + +export type Attachment = GetProp[number]; + +export interface AttachmentsProps extends Omit { + prefixCls?: string; + style?: React.CSSProperties; + className?: string; + + getDropContainer?: null | (() => HTMLElement | null | undefined); + + items?: Attachment[]; + children?: React.ReactElement; + + // ============= placeholder ============= + placeholder?: Pick; +} + +const Attachments: React.FC = (props) => { + const { + prefixCls: customizePrefixCls, + className, + style, + items, + children, + getDropContainer, + placeholder, + onChange, + ...uploadProps + } = props; + + // ============================ PrefixCls ============================ + const { getPrefixCls, direction } = useXProviderContext(); + + const prefixCls = getPrefixCls('attachment', customizePrefixCls); + + // ===================== Component Config ========================= + const contextConfig = useXComponentConfig('attachments'); + + // ============================ Style ============================ + const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); + + const cssinjsCls = classnames(hashId, cssVarCls); + + // ============================ Upload ============================ + const [fileList, setFileList] = useMergedState([], { + value: items, + }); + + const mergedUploadProps: UploadProps = { + ...uploadProps, + fileList, + onChange: (info) => { + setFileList(info.fileList); + onChange?.(info); + }, + }; + + // ============================ Render ============================ + if (children) { + return ( + <> + {children} + + + + + ); + } + + return wrapCSSVar(
2333
); +}; + +if (process.env.NODE_ENV !== 'production') { + Attachments.displayName = 'Attachments'; +} + +export default Attachments; diff --git a/components/attachment/index.zh-CN.md b/components/attachments/index.zh-CN.md similarity index 100% rename from components/attachment/index.zh-CN.md rename to components/attachments/index.zh-CN.md diff --git a/components/attachment/interface.ts b/components/attachments/interface.ts similarity index 100% rename from components/attachment/interface.ts rename to components/attachments/interface.ts diff --git a/components/attachments/style/index.ts b/components/attachments/style/index.ts new file mode 100644 index 00000000..f109646f --- /dev/null +++ b/components/attachments/style/index.ts @@ -0,0 +1,53 @@ +import { unit } from '@ant-design/cssinjs'; +import { mergeToken } from '@ant-design/cssinjs-utils'; +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/cssinjs-utils'; +import { genStyleHooks } from '../../theme/genStyleUtils'; + +// biome-ignore lint/suspicious/noEmptyInterface: ComponentToken need to be empty by default +export interface ComponentToken {} + +export interface AttachmentsToken extends FullToken<'Attachments'> {} + +const genAttachmentsStyle: GenerateStyle = (token) => { + const { componentCls } = token; + + const dropAreaCls = `${componentCls}-drop-area`; + const placeholderCls = `${componentCls}-placeholder`; + + return { + // ============================== Full Screen ============================== + [dropAreaCls]: { + position: 'absolute', + inset: 0, + zIndex: token.zIndexPopupBase, + }, + + // ============================== Placeholder ============================== + [placeholderCls]: { + width: '100%', + height: '100%', + background: token.colorBgBlur, + backdropFilter: 'blur(10px)', + + [`${placeholderCls}-icon`]: { + fontSize: token.fontSizeHeading1, + lineHeight: 1, + }, + [`${placeholderCls}-title${placeholderCls}-title`]: { + margin: 0, + }, + [`${placeholderCls}-description`]: {}, + }, + }; +}; + +export const prepareComponentToken: GetDefaultToken<'Attachments'> = () => ({}); + +export default genStyleHooks( + 'Attachments', + (token) => { + const compToken = mergeToken(token, {}); + return genAttachmentsStyle(compToken); + }, + prepareComponentToken, +); diff --git a/components/index.ts b/components/index.ts index 45ddeda6..ef98c63a 100644 --- a/components/index.ts +++ b/components/index.ts @@ -1,3 +1,5 @@ +export { default as Attachments } from './attachments'; +export type { AttachmentsProps } from './attachments'; export { default as Sender } from './sender'; export { default as Bubble } from './bubble'; export type { BubbleProps } from './bubble'; diff --git a/components/theme/components.ts b/components/theme/components.ts index 877b3cf7..ffba23a4 100644 --- a/components/theme/components.ts +++ b/components/theme/components.ts @@ -1,3 +1,4 @@ +import type { ComponentToken as AttachmentsToken } from '../attachments/style'; import type { ComponentToken as BubbleComponentToken } from '../bubble/style'; import type { ComponentToken as ConversationsComponentToken } from '../conversations/style'; import type { ComponentToken as PromptsComponentToken } from '../prompts/style'; @@ -6,6 +7,7 @@ import type { ComponentToken as SuggestionComponentToken } from '../suggestion/s import type { ComponentToken as ThoughtChainComponentToken } from '../thought-chain/style'; export interface ComponentTokenMap { + Attachments?: AttachmentsToken; Bubble?: BubbleComponentToken; Conversations?: ConversationsComponentToken; Prompts?: PromptsComponentToken; diff --git a/components/x-provider/context.ts b/components/x-provider/context.ts index 679a036e..d5d37d66 100644 --- a/components/x-provider/context.ts +++ b/components/x-provider/context.ts @@ -1,6 +1,7 @@ import React from 'react'; import type { AnyObject } from '../_util/type'; +import { AttachmentsProps } from '../attachments'; import type { BubbleProps } from '../bubble'; import type { ConversationsProps } from '../conversations'; import type { PromptsProps } from '../prompts'; @@ -29,6 +30,7 @@ export interface XComponentsConfig { sender?: ComponentStyleConfig; suggestion?: ComponentStyleConfig; thoughtChain?: ComponentStyleConfig; + attachments?: ComponentStyleConfig; } export interface XProviderProps extends XComponentsConfig { diff --git a/package.json b/package.json index 054c5cb3..4db240ba 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,14 @@ "name": "@ant-design/x", "version": "1.0.0-alpha.4", "description": "Crafting AI-driven interfaces with React, seamlessly integrating smart chat components and API services at your fingertips.", - "keywords": ["AI", "Copilot", "ant", "components", "framework", "react"], + "keywords": [ + "AI", + "Copilot", + "ant", + "components", + "framework", + "react" + ], "homepage": "https://x.ant.design", "bugs": { "url": "https://github.com/ant-design/x/issues" @@ -20,7 +27,13 @@ "main": "lib/index.js", "module": "es/index.js", "typings": "es/index.d.ts", - "files": ["dist", "es", "lib", "locale", "BUG_VERSIONS.json"], + "files": [ + "dist", + "es", + "lib", + "locale", + "BUG_VERSIONS.json" + ], "scripts": { "api-collection": "antd-tools run api-collection", "authors": "tsx scripts/generate-authors.ts", @@ -68,7 +81,9 @@ "tsc": "tsc --noEmit", "version": "tsx scripts/generate-version.ts" }, - "browserslist": ["defaults"], + "browserslist": [ + "defaults" + ], "dependencies": { "@ant-design/colors": "^7.1.0", "@ant-design/cssinjs": "^1.21.1", From 4416ac7d52e7da70f97d734258e0e333e2317df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 17 Oct 2024 19:14:08 +0800 Subject: [PATCH 03/28] chore: support placeholder --- .../attachments/PlaceholderUploader.tsx | 44 +++++++++---- components/attachments/demo/placeholder.md | 7 +++ components/attachments/demo/placeholder.tsx | 61 +++++++++++++++++++ components/attachments/index.en-US.md | 1 + components/attachments/index.tsx | 17 ++++-- components/attachments/index.zh-CN.md | 1 + 6 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 components/attachments/demo/placeholder.md create mode 100644 components/attachments/demo/placeholder.tsx diff --git a/components/attachments/PlaceholderUploader.tsx b/components/attachments/PlaceholderUploader.tsx index 253c3ddd..db7eaf4e 100644 --- a/components/attachments/PlaceholderUploader.tsx +++ b/components/attachments/PlaceholderUploader.tsx @@ -2,32 +2,50 @@ import { Flex, Typography, Upload, type UploadProps } from 'antd'; import classNames from 'classnames'; import React from 'react'; -export interface PlaceholderProps { - prefixCls: string; +export interface PlaceholderConfig { icon?: React.ReactNode; title?: React.ReactNode; description?: React.ReactNode; +} + +export interface PlaceholderProps { + prefixCls: string; + placeholder?: PlaceholderConfig | React.ReactElement; className: string; upload?: UploadProps; } export default function Placeholder(props: PlaceholderProps) { - const { prefixCls, icon, title, description, className, upload } = props; + const { prefixCls, placeholder = {}, className, upload } = props; const placeholderCls = `${prefixCls}-placeholder`; + const placeholderConfig = (placeholder || {}) as PlaceholderConfig; + + const node = React.isValidElement(placeholder) ? ( + placeholder + ) : ( + + + {placeholderConfig.icon} + + + {placeholderConfig.title} + + + {placeholderConfig.description} + + + ); + return (
- - - {icon} - - {title} - - - {description} - - + + {node}
); diff --git a/components/attachments/demo/placeholder.md b/components/attachments/demo/placeholder.md new file mode 100644 index 00000000..d40cc651 --- /dev/null +++ b/components/attachments/demo/placeholder.md @@ -0,0 +1,7 @@ +## zh-CN + +修改占位信息。 + +## en-US + +Modify placeholder information. diff --git a/components/attachments/demo/placeholder.tsx b/components/attachments/demo/placeholder.tsx new file mode 100644 index 00000000..c0689355 --- /dev/null +++ b/components/attachments/demo/placeholder.tsx @@ -0,0 +1,61 @@ +import { CloudUploadOutlined } from '@ant-design/icons'; +import { Attachments, type AttachmentsProps } from '@ant-design/x'; +import { App, Button, Flex, Result, Space, Switch, theme } from 'antd'; +import React from 'react'; + +const Demo = () => { + const { message } = App.useApp(); + + const { token } = theme.useToken(); + + const [items, setItems] = React.useState([]); + + const sharedBorderStyle: React.CSSProperties = { + border: `${token.lineWidthBold}px solid ${token.colorBorderSecondary}`, + borderRadius: token.borderRadius, + overflow: 'hidden', + }; + + const sharedAttachmentProps: AttachmentsProps = { + beforeUpload: () => false, + items, + onChange: ({ file, fileList }) => { + message.info(`Mock upload: ${file.name}`); + setItems(fileList); + }, + }; + + return ( + +
+ , + title: 'Drag & Drop files here', + description: 'Support file type: image, video, audio, document, etc.', + }} + /> +
+ +
+ } + extra={} + /> + } + /> +
+
+ ); +}; + +export default () => ( + + + +); diff --git a/components/attachments/index.en-US.md b/components/attachments/index.en-US.md index feb194e1..9ab2b201 100644 --- a/components/attachments/index.en-US.md +++ b/components/attachments/index.en-US.md @@ -19,6 +19,7 @@ The Prompts component is used to display a predefined set of questions or sugges Basic +Placeholder ## API diff --git a/components/attachments/index.tsx b/components/attachments/index.tsx index 7273d45d..01f5b1c7 100644 --- a/components/attachments/index.tsx +++ b/components/attachments/index.tsx @@ -27,7 +27,7 @@ export interface AttachmentsProps extends Omit { children?: React.ReactElement; // ============= placeholder ============= - placeholder?: Pick; + placeholder?: PlaceholderProps['placeholder']; } const Attachments: React.FC = (props) => { @@ -72,7 +72,7 @@ const Attachments: React.FC = (props) => { // ============================ Render ============================ if (children) { - return ( + return wrapCSSVar( <> {children} = (props) => { className={cssinjsCls} > - + , ); } - return wrapCSSVar(
2333
); + return wrapCSSVar( + , + ); }; if (process.env.NODE_ENV !== 'production') { diff --git a/components/attachments/index.zh-CN.md b/components/attachments/index.zh-CN.md index 7d1e5f32..e508d859 100644 --- a/components/attachments/index.zh-CN.md +++ b/components/attachments/index.zh-CN.md @@ -20,6 +20,7 @@ Attachment 组件用于需要展示一组附件信息集合的场景。 基本 +占位信息 ## API From e031bd0c6ce90c7d73bda7a792a16fa309f4036a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 17 Oct 2024 19:31:54 +0800 Subject: [PATCH 04/28] feat: placeholder drag --- components/attachments/DropUploader.tsx | 6 +++- .../attachments/PlaceholderUploader.tsx | 29 ++++++++++++++++++- components/attachments/demo/placeholder.tsx | 19 ++++++++---- components/attachments/style/index.ts | 16 +++++++++- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/components/attachments/DropUploader.tsx b/components/attachments/DropUploader.tsx index 0aa88ee1..6d965908 100644 --- a/components/attachments/DropUploader.tsx +++ b/components/attachments/DropUploader.tsx @@ -70,9 +70,13 @@ export default function DropUploader(props: DropUploaderProps) { return null; } + const areaCls = `${prefixCls}-drop-area`; + return createPortal(
{ + setDragIn(true); + }; + + const onDragLeave = (e: React.DragEvent) => { + // Leave the div should end + if ((e.currentTarget as HTMLElement).contains(e.relatedTarget as HTMLElement)) { + return; + } + setDragIn(false); + }; + + const onDrop = () => { + setDragIn(false); + }; + + // ============================ Render ============================ const node = React.isValidElement(placeholder) ? ( placeholder ) : ( @@ -39,7 +59,14 @@ export default function Placeholder(props: PlaceholderProps) { ); return ( -
+
{ @@ -8,12 +8,12 @@ const Demo = () => { const { token } = theme.useToken(); - const [items, setItems] = React.useState([]); + const [items, setItems] = React.useState>([]); const sharedBorderStyle: React.CSSProperties = { - border: `${token.lineWidthBold}px solid ${token.colorBorderSecondary}`, borderRadius: token.borderRadius, overflow: 'hidden', + background: token.colorBgContainer, }; const sharedAttachmentProps: AttachmentsProps = { @@ -26,13 +26,20 @@ const Demo = () => { }; return ( - +
, - title: 'Drag & Drop files here', + title: 'Click or Drop files here', description: 'Support file type: image, video, audio, document, etc.', }} /> @@ -50,6 +57,8 @@ const Demo = () => { } />
+ + {!!items.length && }
); }; diff --git a/components/attachments/style/index.ts b/components/attachments/style/index.ts index f109646f..f20726c2 100644 --- a/components/attachments/style/index.ts +++ b/components/attachments/style/index.ts @@ -20,14 +20,28 @@ const genAttachmentsStyle: GenerateStyle = (token) => { position: 'absolute', inset: 0, zIndex: token.zIndexPopupBase, + boxSizing: 'border-box', + + '&-on-body': { + position: 'fixed', + inset: 0, + }, }, // ============================== Placeholder ============================== [placeholderCls]: { - width: '100%', height: '100%', background: token.colorBgBlur, backdropFilter: 'blur(10px)', + borderRadius: token.borderRadius, + borderWidth: token.lineWidthBold, + borderStyle: 'dashed', + borderColor: 'transparent', + boxSizing: 'border-box', + + [`&${placeholderCls}-drag-in`]: { + borderColor: token.colorPrimaryHover, + }, [`${placeholderCls}-icon`]: { fontSize: token.fontSizeHeading1, From 507b11c9cabad5e1c1f4055f54d9d8e321213d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 18 Oct 2024 11:18:32 +0800 Subject: [PATCH 05/28] docs: update doc --- components/attachments/demo/placeholder.tsx | 39 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/components/attachments/demo/placeholder.tsx b/components/attachments/demo/placeholder.tsx index 907669ad..417bc53c 100644 --- a/components/attachments/demo/placeholder.tsx +++ b/components/attachments/demo/placeholder.tsx @@ -3,12 +3,35 @@ import { Attachments, type AttachmentsProps } from '@ant-design/x'; import { App, Button, Flex, GetProp, Result, Space, Switch, theme } from 'antd'; import React from 'react'; +const presetFiles: AttachmentsProps['items'] = [ + { + uid: '1', + name: 'xxx.png', + status: 'uploading', + url: 'http://www.baidu.com/xxx.png', + percent: 33, + }, + { + uid: '2', + name: 'yyy.png', + status: 'done', + url: 'http://www.baidu.com/yyy.png', + }, + { + uid: '3', + name: 'zzz.png', + status: 'error', + response: 'Server Error 500', // custom error message to show + url: 'http://www.baidu.com/zzz.png', + }, +]; + const Demo = () => { const { message } = App.useApp(); const { token } = theme.useToken(); - const [items, setItems] = React.useState>([]); + const [items, setItems] = React.useState>(presetFiles); const sharedBorderStyle: React.CSSProperties = { borderRadius: token.borderRadius, @@ -58,7 +81,19 @@ const Demo = () => { />
- {!!items.length && } + + + + ); }; From 961340738531b70e42bca107238018e998c8cfa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 18 Oct 2024 16:26:41 +0800 Subject: [PATCH 06/28] feat: file list --- .../{DropUploader.tsx => DropArea.tsx} | 2 +- .../attachments/FileList/FileListCard.tsx | 146 +++++++++++++++++ components/attachments/FileList/index.tsx | 23 +++ .../attachments/PlaceholderUploader.tsx | 5 +- components/attachments/demo/placeholder.tsx | 10 +- components/attachments/index.tsx | 67 +++++--- components/attachments/style/index.ts | 152 +++++++++++++++--- 7 files changed, 353 insertions(+), 52 deletions(-) rename components/attachments/{DropUploader.tsx => DropArea.tsx} (97%) create mode 100644 components/attachments/FileList/FileListCard.tsx create mode 100644 components/attachments/FileList/index.tsx diff --git a/components/attachments/DropUploader.tsx b/components/attachments/DropArea.tsx similarity index 97% rename from components/attachments/DropUploader.tsx rename to components/attachments/DropArea.tsx index 6d965908..f7f07c47 100644 --- a/components/attachments/DropUploader.tsx +++ b/components/attachments/DropArea.tsx @@ -9,7 +9,7 @@ export interface DropUploaderProps { children?: React.ReactNode; } -export default function DropUploader(props: DropUploaderProps) { +export default function DropArea(props: DropUploaderProps) { const { getDropContainer, className, prefixCls, children } = props; const [container, setContainer] = React.useState(); diff --git a/components/attachments/FileList/FileListCard.tsx b/components/attachments/FileList/FileListCard.tsx new file mode 100644 index 00000000..3216beae --- /dev/null +++ b/components/attachments/FileList/FileListCard.tsx @@ -0,0 +1,146 @@ +import { + CloseCircleFilled, + FileExcelFilled, + FileImageFilled, + FileMarkdownFilled, + FilePdfFilled, + FilePptFilled, + FileTextFilled, + FileWordFilled, + FileZipFilled, +} from '@ant-design/icons'; +import classNames from 'classnames'; +import React from 'react'; +import type { Attachment } from '..'; + +export interface FileListCardProps { + prefixCls: string; + item: Attachment; + onRemove: (item: Attachment) => void; +} + +const EMPTY = '\u00A0'; + +const DEFAULT_ICON_COLOR = '#8c8c8c'; + +const PRESET_FILE_ICONS: { + ext: string[]; + color: string; + icon: React.ReactElement; +}[] = [ + { + icon: , + color: '#22b35e', + ext: ['xlsx', 'xls'], + }, + { + icon: , + color: DEFAULT_ICON_COLOR, + ext: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'], + }, + { + icon: , + color: DEFAULT_ICON_COLOR, + ext: ['md', 'mdx'], + }, + { + icon: , + color: '#ff4d4f', + ext: ['pdf'], + }, + { + icon: , + color: '#ff6e31', + ext: ['ppt', 'pptx'], + }, + { + icon: , + color: '#1677ff', + ext: ['doc', 'docx'], + }, + { + icon: , + color: '#fab714', + ext: ['zip', 'rar', '7z', 'tar', 'gz'], + }, +]; + +function getSize(size: number) { + let retSize = size; + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']; + let unitIndex = 0; + + while (retSize >= 1024 && unitIndex < units.length - 1) { + retSize /= 1024; + unitIndex++; + } + + return `${retSize.toFixed(0)} ${units[unitIndex]}`; +} + +export default function FileListCard(props: FileListCardProps) { + const { prefixCls, item, onRemove } = props; + const { name, size, percent, status } = item; + const cardCls = `${prefixCls}-card`; + + // ============================== Name ============================== + const [namePrefix, nameSuffix] = React.useMemo(() => { + const nameStr = name || ''; + const match = nameStr.match(/^(.*)\.[^.]+$/); + return match ? [match[1], nameStr.slice(match[1].length)] : [nameStr, '']; + }, [name]); + + // ============================== Desc ============================== + const desc = React.useMemo(() => { + if (status === 'uploading') { + return `${percent || 0}%`; + } + + if (status === 'error') { + return item.response || EMPTY; + } + + return size ? getSize(size) : EMPTY; + }, [status, percent]); + + // ============================== Icon ============================== + const [icon, iconColor] = React.useMemo(() => { + for (const { ext, icon, color } of PRESET_FILE_ICONS) { + if (ext.some((e) => nameSuffix.toLowerCase() === `.${e}`)) { + return [icon, color]; + } + } + + return [, DEFAULT_ICON_COLOR]; + }, [nameSuffix]); + + // ============================= Render ============================= + return ( +
+
+ {icon} +
+
+
+
{namePrefix ?? EMPTY}
+
{nameSuffix}
+
+
+
{desc}
+ {/*
*/} +
+
+ + {/* Remove Icon */} + +
+ ); +} diff --git a/components/attachments/FileList/index.tsx b/components/attachments/FileList/index.tsx new file mode 100644 index 00000000..3883fc14 --- /dev/null +++ b/components/attachments/FileList/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import type { Attachment } from '..'; +import FileListCard from './FileListCard'; + +export interface FileListProps { + prefixCls: string; + items: Attachment[]; + onRemove: (item: Attachment) => void; +} + +export default function FileList(props: FileListProps) { + const { prefixCls, items, onRemove } = props; + + const listCls = `${prefixCls}-list`; + + return ( +
+ {items.map((item) => ( + + ))} +
+ ); +} diff --git a/components/attachments/PlaceholderUploader.tsx b/components/attachments/PlaceholderUploader.tsx index f5ec60d3..ef656d23 100644 --- a/components/attachments/PlaceholderUploader.tsx +++ b/components/attachments/PlaceholderUploader.tsx @@ -11,12 +11,11 @@ export interface PlaceholderConfig { export interface PlaceholderProps { prefixCls: string; placeholder?: PlaceholderConfig | React.ReactElement; - className: string; upload?: UploadProps; } export default function Placeholder(props: PlaceholderProps) { - const { prefixCls, placeholder = {}, className, upload } = props; + const { prefixCls, placeholder = {}, upload } = props; const placeholderCls = `${prefixCls}-placeholder`; @@ -60,7 +59,7 @@ export default function Placeholder(props: PlaceholderProps) { return (
{ const sharedAttachmentProps: AttachmentsProps = { beforeUpload: () => false, items, - onChange: ({ file, fileList }) => { - message.info(`Mock upload: ${file.name}`); + onChange: ({ fileList }) => { setItems(fileList); }, }; diff --git a/components/attachments/index.tsx b/components/attachments/index.tsx index 01f5b1c7..aa005570 100644 --- a/components/attachments/index.tsx +++ b/components/attachments/index.tsx @@ -6,8 +6,9 @@ import React from 'react'; import useXComponentConfig from '../_util/hooks/use-x-component-config'; import { useXProviderContext } from '../x-provider'; -import { useMergedState } from 'rc-util'; -import DropUploader from './DropUploader'; +import { useEvent, useMergedState } from 'rc-util'; +import DropArea from './DropArea'; +import FileList from './FileList'; import PlaceholderUploader, { PlaceholderProps } from './PlaceholderUploader'; import SilentUploader from './SilentUploader'; import useStyle from './style'; @@ -61,44 +62,64 @@ const Attachments: React.FC = (props) => { value: items, }); + const triggerChange: GetProp = useEvent((info) => { + setFileList(info.fileList); + onChange?.(info); + }); + const mergedUploadProps: UploadProps = { ...uploadProps, fileList, - onChange: (info) => { - setFileList(info.fileList); - onChange?.(info); - }, + onChange: triggerChange, + }; + + const onItemRemove = (item: Attachment) => { + const newFileList = fileList.filter((fileItem) => fileItem.uid !== item.uid); + triggerChange({ + file: item, + fileList: newFileList, + }); }; // ============================ Render ============================ + let renderChildren: React.ReactElement; + if (children) { - return wrapCSSVar( + renderChildren = ( <> {children} - + + + + + ); + } else { + return ( +
+ {fileList.length ? ( + + ) : ( - - , + )} +
); } - return wrapCSSVar( - , - ); + return wrapCSSVar(renderChildren); }; if (process.env.NODE_ENV !== 'production') { diff --git a/components/attachments/style/index.ts b/components/attachments/style/index.ts index f20726c2..709afdc2 100644 --- a/components/attachments/style/index.ts +++ b/components/attachments/style/index.ts @@ -9,10 +9,11 @@ export interface ComponentToken {} export interface AttachmentsToken extends FullToken<'Attachments'> {} const genAttachmentsStyle: GenerateStyle = (token) => { - const { componentCls } = token; + const { componentCls, calc } = token; const dropAreaCls = `${componentCls}-drop-area`; const placeholderCls = `${componentCls}-placeholder`; + const fileListCls = `${componentCls}-list`; return { // ============================== Full Screen ============================== @@ -28,29 +29,140 @@ const genAttachmentsStyle: GenerateStyle = (token) => { }, }, - // ============================== Placeholder ============================== - [placeholderCls]: { - height: '100%', - background: token.colorBgBlur, - backdropFilter: 'blur(10px)', - borderRadius: token.borderRadius, - borderWidth: token.lineWidthBold, - borderStyle: 'dashed', - borderColor: 'transparent', - boxSizing: 'border-box', + '&': { + // ============================= Placeholder ============================= + [placeholderCls]: { + height: '100%', + background: token.colorBgBlur, + backdropFilter: 'blur(10px)', + borderRadius: token.borderRadius, + borderWidth: token.lineWidthBold, + borderStyle: 'dashed', + borderColor: 'transparent', + boxSizing: 'border-box', - [`&${placeholderCls}-drag-in`]: { - borderColor: token.colorPrimaryHover, - }, + [`&${placeholderCls}-drag-in`]: { + borderColor: token.colorPrimaryHover, + }, - [`${placeholderCls}-icon`]: { - fontSize: token.fontSizeHeading1, - lineHeight: 1, + [`${placeholderCls}-icon`]: { + fontSize: token.fontSizeHeading1, + lineHeight: 1, + }, + [`${placeholderCls}-title${placeholderCls}-title`]: { + margin: 0, + }, + [`${placeholderCls}-description`]: {}, }, - [`${placeholderCls}-title${placeholderCls}-title`]: { - margin: 0, + }, + + [componentCls]: { + // =============================== File List =============================== + [fileListCls]: { + display: 'flex', + flexWrap: 'wrap', + gap: token.paddingSM, + fontSize: token.fontSize, + lineHeight: token.lineHeight, + color: token.colorText, + paddingBlock: token.paddingSM, + paddingInline: token.padding, + + '&-card': { + padding: calc(token.paddingSM).sub(token.lineWidth).equal(), + paddingInlineStart: calc(token.padding).add(token.lineWidth).equal(), + borderRadius: token.borderRadius, + background: token.colorFillContent, + borderWidth: token.lineWidth, + borderStyle: 'solid', + borderColor: 'transparent', + display: 'flex', + flexWrap: 'nowrap', + gap: token.paddingXS, + alignItems: 'flex-start', + width: 236, + position: 'relative', + + // Icon + '&-icon': { + fontSize: calc(token.fontSizeLG).mul(2).equal(), + lineHeight: 1, + paddingTop: calc(token.paddingXXS).mul(1.5).equal(), + flex: 'none', + }, + + // Content + '&-content': { + flex: 'auto', + minWidth: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + }, + + '&-name, &-desc': { + display: 'flex', + flexWrap: 'nowrap', + }, + + '&-ellipsis-prefix': { + flex: '0 1 auto', + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + + '&-ellipsis-suffix': { + flex: 'none', + }, + + '&-desc': { + color: token.colorTextTertiary, + }, + + // Remove + '&-remove': { + position: 'absolute', + top: 0, + insetInlineEnd: 0, + border: 0, + padding: token.paddingXXS, + background: 'transparent', + lineHeight: 1, + transform: 'translate(50%, -50%)', + fontSize: token.fontSize, + cursor: 'pointer', + opacity: token.opacityLoading, + display: 'none', + + '&:dir(rtl)': { + transform: 'translate(-50%, -50%)', + }, + + '&:hover': { + opacity: 1, + }, + + '&:active': { + opacity: token.opacityLoading, + }, + }, + + [`&:hover ${fileListCls}-card-remove`]: { + display: 'block', + }, + + // Status + '&-status-error': { + borderColor: token.colorError, + + [`${fileListCls}-card-desc`]: { + color: token.colorError, + }, + }, + }, }, - [`${placeholderCls}-description`]: {}, }, }; }; From 34d7f8203d73647f7738ff623bbf5d3e7960cd41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 18 Oct 2024 17:27:49 +0800 Subject: [PATCH 07/28] chore: filelist attachment --- .../attachments/FileList/FileListCard.tsx | 14 ++++-- components/attachments/FileList/index.tsx | 37 +++++++++++++-- .../attachments/PlaceholderUploader.tsx | 2 +- components/attachments/demo/placeholder.tsx | 1 + components/attachments/index.tsx | 35 ++++++++++----- components/attachments/style/index.ts | 45 ++++++++++++++++++- 6 files changed, 114 insertions(+), 20 deletions(-) diff --git a/components/attachments/FileList/FileListCard.tsx b/components/attachments/FileList/FileListCard.tsx index 3216beae..53e46d8d 100644 --- a/components/attachments/FileList/FileListCard.tsx +++ b/components/attachments/FileList/FileListCard.tsx @@ -17,6 +17,8 @@ export interface FileListCardProps { prefixCls: string; item: Attachment; onRemove: (item: Attachment) => void; + className?: string; + style?: React.CSSProperties; } const EMPTY = '\u00A0'; @@ -78,8 +80,8 @@ function getSize(size: number) { return `${retSize.toFixed(0)} ${units[unitIndex]}`; } -export default function FileListCard(props: FileListCardProps) { - const { prefixCls, item, onRemove } = props; +function FileListCard(props: FileListCardProps, ref: React.Ref) { + const { prefixCls, item, onRemove, className, style } = props; const { name, size, percent, status } = item; const cardCls = `${prefixCls}-card`; @@ -116,7 +118,11 @@ export default function FileListCard(props: FileListCardProps) { // ============================= Render ============================= return ( -
+
{icon}
@@ -144,3 +150,5 @@ export default function FileListCard(props: FileListCardProps) {
); } + +export default React.forwardRef(FileListCard); diff --git a/components/attachments/FileList/index.tsx b/components/attachments/FileList/index.tsx index 3883fc14..815cbfee 100644 --- a/components/attachments/FileList/index.tsx +++ b/components/attachments/FileList/index.tsx @@ -1,3 +1,4 @@ +import { CSSMotionList } from 'rc-motion'; import React from 'react'; import type { Attachment } from '..'; import FileListCard from './FileListCard'; @@ -13,11 +14,41 @@ export default function FileList(props: FileListProps) { const listCls = `${prefixCls}-list`; + const [firstMount, setFirstMount] = React.useState(false); + + React.useEffect(() => { + setFirstMount(true); + return () => { + setFirstMount(false); + }; + }, []); + return (
- {items.map((item) => ( - - ))} + ({ + key: item.uid, + item, + }))} + motionName={`${listCls}-card-motion`} + component={false} + motionAppear={firstMount} + motionLeave + motionEnter + > + {({ key, item, className: motionCls, style: motionStyle }) => { + return ( + + ); + }} +
); } diff --git a/components/attachments/PlaceholderUploader.tsx b/components/attachments/PlaceholderUploader.tsx index ef656d23..d8d96e93 100644 --- a/components/attachments/PlaceholderUploader.tsx +++ b/components/attachments/PlaceholderUploader.tsx @@ -44,7 +44,7 @@ export default function Placeholder(props: PlaceholderProps) { const node = React.isValidElement(placeholder) ? ( placeholder ) : ( - + {placeholderConfig.icon} diff --git a/components/attachments/demo/placeholder.tsx b/components/attachments/demo/placeholder.tsx index f9ab8543..4e7503a1 100644 --- a/components/attachments/demo/placeholder.tsx +++ b/components/attachments/demo/placeholder.tsx @@ -76,6 +76,7 @@ const Demo = () => { title="Custom Placeholder Node" icon={} extra={} + style={{ padding: 0 }} /> } /> diff --git a/components/attachments/index.tsx b/components/attachments/index.tsx index aa005570..79d270c4 100644 --- a/components/attachments/index.tsx +++ b/components/attachments/index.tsx @@ -52,6 +52,9 @@ const Attachments: React.FC = (props) => { // ===================== Component Config ========================= const contextConfig = useXComponentConfig('attachments'); + // ============================= Ref ============================= + const containerRef = React.useRef(null); + // ============================ Style ============================ const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); @@ -84,16 +87,20 @@ const Attachments: React.FC = (props) => { // ============================ Render ============================ let renderChildren: React.ReactElement; + const placeholderNode = ( + + ); + if (children) { renderChildren = ( <> {children} - + {placeholderNode} ); @@ -105,15 +112,21 @@ const Attachments: React.FC = (props) => { })} style={style} dir={direction || 'ltr'} + ref={containerRef} > {fileList.length ? ( - + <> + + containerRef.current} + prefixCls={prefixCls} + className={cssinjsCls} + > + {placeholderNode} + + ) : ( - + placeholderNode )}
); diff --git a/components/attachments/style/index.ts b/components/attachments/style/index.ts index 709afdc2..dae306e4 100644 --- a/components/attachments/style/index.ts +++ b/components/attachments/style/index.ts @@ -9,7 +9,7 @@ export interface ComponentToken {} export interface AttachmentsToken extends FullToken<'Attachments'> {} const genAttachmentsStyle: GenerateStyle = (token) => { - const { componentCls, calc } = token; + const { componentCls, calc, antCls } = token; const dropAreaCls = `${componentCls}-drop-area`; const placeholderCls = `${componentCls}-placeholder`; @@ -27,6 +27,10 @@ const genAttachmentsStyle: GenerateStyle = (token) => { position: 'fixed', inset: 0, }, + + [placeholderCls]: { + padding: 0, + }, }, '&': { @@ -40,23 +44,35 @@ const genAttachmentsStyle: GenerateStyle = (token) => { borderStyle: 'dashed', borderColor: 'transparent', boxSizing: 'border-box', + padding: token.padding, + + [`${antCls}-upload-wrapper ${antCls}-upload${antCls}-upload-btn`]: { + padding: 0, + }, [`&${placeholderCls}-drag-in`]: { borderColor: token.colorPrimaryHover, }, + [`${placeholderCls}-inner`]: { + gap: calc(token.paddingXXS).div(2).equal(), + }, [`${placeholderCls}-icon`]: { - fontSize: token.fontSizeHeading1, + fontSize: token.fontSizeHeading2, lineHeight: 1, }, [`${placeholderCls}-title${placeholderCls}-title`]: { margin: 0, + fontSize: token.fontSize, + lineHeight: token.lineHeight, }, [`${placeholderCls}-description`]: {}, }, }, [componentCls]: { + position: 'relative', + // =============================== File List =============================== [fileListCls]: { display: 'flex', @@ -161,6 +177,31 @@ const genAttachmentsStyle: GenerateStyle = (token) => { color: token.colorError, }, }, + + // Motion + '&-motion': { + overflow: 'hidden', + transition: ['opacity', 'width', 'margin', 'padding'] + .map((prop) => `${prop} ${token.motionDurationSlow}`) + .join(','), + + [`${fileListCls}-card-remove`]: { + display: 'none !important', + }, + + '&-appear-start': { + width: 0, + transition: 'none', + }, + + '&-leave-active': { + opacity: 0, + width: 0, + paddingInline: 0, + borderInlineWidth: 0, + marginInlineEnd: calc(token.paddingSM).mul(-1).equal(), + }, + }, }, }, }, From d863cd9a87b3049e85ca517ad3101ee538b78b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 21 Oct 2024 14:53:49 +0800 Subject: [PATCH 08/28] chore: img style --- components/attachments/DropArea.tsx | 4 - .../attachments/FileList/FileListCard.tsx | 99 ++++++++++-- components/attachments/FileList/Progress.tsx | 55 +++++++ components/attachments/demo/placeholder.tsx | 27 +++- components/attachments/style/index.ts | 151 +++++++++++++----- components/attachments/util.ts | 63 ++++++++ 6 files changed, 339 insertions(+), 60 deletions(-) create mode 100644 components/attachments/FileList/Progress.tsx create mode 100644 components/attachments/util.ts diff --git a/components/attachments/DropArea.tsx b/components/attachments/DropArea.tsx index f7f07c47..0649fbe6 100644 --- a/components/attachments/DropArea.tsx +++ b/components/attachments/DropArea.tsx @@ -31,7 +31,6 @@ export default function DropArea(props: DropUploaderProps) { if (container) { const onDragEnter = (e: DragEvent) => { setShowArea(true); - console.log('enter'); }; // Should prevent default to make drop event work @@ -42,14 +41,11 @@ export default function DropArea(props: DropUploaderProps) { const onDragLeave = (e: DragEvent) => { if (!e.relatedTarget) { setShowArea(false); - console.log('leave'); } }; const onDrop = (e: DragEvent) => { setShowArea(false); e.preventDefault(); - - console.log('drop', e.defaultPrevented); }; document.addEventListener('dragenter', onDragEnter); diff --git a/components/attachments/FileList/FileListCard.tsx b/components/attachments/FileList/FileListCard.tsx index 53e46d8d..691387cc 100644 --- a/components/attachments/FileList/FileListCard.tsx +++ b/components/attachments/FileList/FileListCard.tsx @@ -12,6 +12,8 @@ import { import classNames from 'classnames'; import React from 'react'; import type { Attachment } from '..'; +import { isImageFileType, previewImage } from '../util'; +import Progress from './Progress'; export interface FileListCardProps { prefixCls: string; @@ -25,6 +27,8 @@ const EMPTY = '\u00A0'; const DEFAULT_ICON_COLOR = '#8c8c8c'; +const IMG_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']; + const PRESET_FILE_ICONS: { ext: string[]; color: string; @@ -38,7 +42,7 @@ const PRESET_FILE_ICONS: { { icon: , color: DEFAULT_ICON_COLOR, - ext: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'], + ext: IMG_EXTS, }, { icon: , @@ -67,6 +71,10 @@ const PRESET_FILE_ICONS: { }, ]; +function matchExt(suffix: string, ext: string[]) { + return ext.some((e) => suffix.toLowerCase() === `.${e}`); +} + function getSize(size: number) { let retSize = size; const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']; @@ -92,6 +100,8 @@ function FileListCard(props: FileListCardProps, ref: React.Ref) return match ? [match[1], nameStr.slice(match[1].length)] : [nameStr, '']; }, [name]); + const isImg = React.useMemo(() => matchExt(nameSuffix, IMG_EXTS), [nameSuffix]); + // ============================== Desc ============================== const desc = React.useMemo(() => { if (status === 'uploading') { @@ -108,7 +118,7 @@ function FileListCard(props: FileListCardProps, ref: React.Ref) // ============================== Icon ============================== const [icon, iconColor] = React.useMemo(() => { for (const { ext, icon, color } of PRESET_FILE_ICONS) { - if (ext.some((e) => nameSuffix.toLowerCase() === `.${e}`)) { + if (matchExt(nameSuffix, ext)) { return [icon, color]; } } @@ -116,26 +126,83 @@ function FileListCard(props: FileListCardProps, ref: React.Ref) return [, DEFAULT_ICON_COLOR]; }, [nameSuffix]); + // ========================== ImagePreview ========================== + const [previewImg, setPreviewImg] = React.useState(); + + React.useEffect(() => { + if (item.originFileObj) { + let synced = true; + previewImage(item.originFileObj).then((url) => { + if (synced) { + setPreviewImg(url); + } + }); + + return () => { + synced = false; + }; + } + setPreviewImg(undefined); + }, [item.originFileObj]); + // ============================= Render ============================= + let content: React.ReactNode = null; + + if (isImg) { + // Preview Image style + content = ( + <> + preview + + {status !== 'done' && ( +
+ {status === 'uploading' && percent !== undefined && ( + + )} + {status === 'error' && ( +
+
{desc}
+
+ )} +
+ )} + + ); + } else { + // Preview Card style + content = ( + <> +
+ {icon} +
+
+
+
{namePrefix ?? EMPTY}
+
{nameSuffix}
+
+
+
{desc}
+
+
+ + ); + } + return (
-
- {icon} -
-
-
-
{namePrefix ?? EMPTY}
-
{nameSuffix}
-
-
-
{desc}
- {/*
*/} -
-
+ {content} {/* Remove Icon */} + {!disabled && ( + + )}
); } diff --git a/components/attachments/FileList/index.tsx b/components/attachments/FileList/index.tsx index 815cbfee..2f8d1bba 100644 --- a/components/attachments/FileList/index.tsx +++ b/components/attachments/FileList/index.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import { CSSMotionList } from 'rc-motion'; import React from 'react'; import type { Attachment } from '..'; @@ -7,10 +8,12 @@ export interface FileListProps { prefixCls: string; items: Attachment[]; onRemove: (item: Attachment) => void; + overflow?: 'scrollX' | 'scrollY' | 'wrap'; + disabled?: boolean; } export default function FileList(props: FileListProps) { - const { prefixCls, items, onRemove } = props; + const { prefixCls, items, onRemove, overflow, disabled } = props; const listCls = `${prefixCls}-list`; @@ -24,7 +27,11 @@ export default function FileList(props: FileListProps) { }, []); return ( -
+
({ key: item.uid, @@ -45,6 +52,7 @@ export default function FileList(props: FileListProps) { onRemove={onRemove} className={motionCls} style={motionStyle} + disabled={disabled} /> ); }} diff --git a/components/attachments/demo/overflow.md b/components/attachments/demo/overflow.md new file mode 100644 index 00000000..d40cc651 --- /dev/null +++ b/components/attachments/demo/overflow.md @@ -0,0 +1,7 @@ +## zh-CN + +修改占位信息。 + +## en-US + +Modify placeholder information. diff --git a/components/attachments/demo/overflow.tsx b/components/attachments/demo/overflow.tsx new file mode 100644 index 00000000..5dd3647c --- /dev/null +++ b/components/attachments/demo/overflow.tsx @@ -0,0 +1,43 @@ +import { CloudUploadOutlined } from '@ant-design/icons'; +import { Attachments, type AttachmentsProps } from '@ant-design/x'; +import { Flex, Segmented } from 'antd'; +import React from 'react'; + +const presetFiles: AttachmentsProps['items'] = Array.from({ length: 30 }).map((_, index) => ({ + uid: String(index), + name: `file-${index}.jpg`, + status: 'done', + thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', + url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', +})); + +const Demo = () => { + const [overflow, setOverflow] = React.useState('scrollX'); + + return ( + + + false} + placeholder={{ + icon: , + title: 'Click or Drop files here', + description: 'Support file type: image, video, audio, document, etc.', + }} + disabled + /> + + ); +}; + +export default Demo; diff --git a/components/attachments/index.en-US.md b/components/attachments/index.en-US.md index 9ab2b201..993caad7 100644 --- a/components/attachments/index.en-US.md +++ b/components/attachments/index.en-US.md @@ -20,6 +20,7 @@ The Prompts component is used to display a predefined set of questions or sugges Basic Placeholder +Overflow ## API diff --git a/components/attachments/index.tsx b/components/attachments/index.tsx index 79d270c4..7b893887 100644 --- a/components/attachments/index.tsx +++ b/components/attachments/index.tsx @@ -1,4 +1,3 @@ -import { Button, Typography } from 'antd'; import type { GetProp, UploadProps } from 'antd'; import classnames from 'classnames'; import React from 'react'; @@ -8,7 +7,7 @@ import { useXProviderContext } from '../x-provider'; import { useEvent, useMergedState } from 'rc-util'; import DropArea from './DropArea'; -import FileList from './FileList'; +import FileList, { type FileListProps } from './FileList'; import PlaceholderUploader, { PlaceholderProps } from './PlaceholderUploader'; import SilentUploader from './SilentUploader'; import useStyle from './style'; @@ -22,13 +21,17 @@ export interface AttachmentsProps extends Omit { style?: React.CSSProperties; className?: string; - getDropContainer?: null | (() => HTMLElement | null | undefined); - - items?: Attachment[]; children?: React.ReactElement; + disabled?: boolean; + // ============= placeholder ============= placeholder?: PlaceholderProps['placeholder']; + getDropContainer?: null | (() => HTMLElement | null | undefined); + + // ============== File List ============== + items?: Attachment[]; + overflow?: FileListProps['overflow']; } const Attachments: React.FC = (props) => { @@ -41,6 +44,8 @@ const Attachments: React.FC = (props) => { getDropContainer, placeholder, onChange, + overflow, + disabled, ...uploadProps } = props; @@ -116,7 +121,13 @@ const Attachments: React.FC = (props) => { > {fileList.length ? ( <> - + containerRef.current} prefixCls={prefixCls} diff --git a/components/attachments/index.zh-CN.md b/components/attachments/index.zh-CN.md index e508d859..5d593cae 100644 --- a/components/attachments/index.zh-CN.md +++ b/components/attachments/index.zh-CN.md @@ -21,6 +21,7 @@ Attachment 组件用于需要展示一组附件信息集合的场景。 基本 占位信息 +超出样式 ## API diff --git a/components/attachments/interface.ts b/components/attachments/interface.ts deleted file mode 100644 index 840560fc..00000000 --- a/components/attachments/interface.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface PromptProps { - /** - * @desc 唯一标识用于区分每个提示项。 - * @descEN Unique identifier used to distinguish each prompt item. - */ - key: string; - - /** - * @desc 提示图标显示在提示项的左侧。 - * @descEN Prompt icon displayed on the left side of the prompt item. - */ - icon?: React.ReactNode; - - /** - * @desc 提示标签显示提示的主要内容。 - * @descEN Prompt label displaying the main content of the prompt. - */ - label?: React.ReactNode; - - /** - * @desc 提示描述提供额外的信息。 - * @descEN Prompt description providing additional information. - */ - description?: React.ReactNode; - - /** - * @desc 设置为 true 时禁用点击事件。 - * @descEN When set to true, click events are disabled. - */ - disabled?: boolean; -} diff --git a/components/attachments/style/index.ts b/components/attachments/style/index.ts index 8e81352b..99e1c7c5 100644 --- a/components/attachments/style/index.ts +++ b/components/attachments/style/index.ts @@ -93,6 +93,7 @@ const genFileListStyle: GenerateStyle = (token) => { return { [componentCls]: { position: 'relative', + width: '100%', ...anyBoxSizing, // =============================== File List =============================== @@ -105,6 +106,20 @@ const genFileListStyle: GenerateStyle = (token) => { color: token.colorText, paddingBlock: token.paddingSM, paddingInline: token.padding, + width: '100%', + + // Scroll + '&-overflow-scrollX': { + overflowX: 'auto', + overflowY: 'hidden', + flexWrap: 'nowrap', + }, + + '&-overflow-scrollY': { + overflowX: 'hidden', + overflowY: 'auto', + maxHeight: calc(cardHeight).mul(3).equal(), + }, [cardCls]: { borderRadius: token.borderRadius, @@ -113,6 +128,7 @@ const genFileListStyle: GenerateStyle = (token) => { borderWidth: token.lineWidth, borderStyle: 'solid', borderColor: 'transparent', + flex: 'none', // =============================== Desc ================================ [`${cardCls}-name,${cardCls}-desc`]: { From f36a319a6549da76cd9eb5183d0c32f6d1c4947a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 22 Oct 2024 19:21:19 +0800 Subject: [PATCH 11/28] chore: droparea support container --- components/attachments/DropArea.tsx | 4 +- .../attachments/FileList/FileListCard.tsx | 6 +- components/attachments/FileList/index.tsx | 89 ++++++++++- .../attachments/PlaceholderUploader.tsx | 5 + components/attachments/context.tsx | 7 + components/attachments/demo/overflow.tsx | 46 ++++-- components/attachments/demo/with-sender.md | 7 + components/attachments/demo/with-sender.tsx | 51 +++++++ components/attachments/index.en-US.md | 1 + components/attachments/index.tsx | 57 ++++--- components/attachments/index.zh-CN.md | 1 + components/attachments/style/index.ts | 141 +++++++++++++++++- components/sender/index.tsx | 22 ++- package.json | 1 + 14 files changed, 384 insertions(+), 54 deletions(-) create mode 100644 components/attachments/context.tsx create mode 100644 components/attachments/demo/with-sender.md create mode 100644 components/attachments/demo/with-sender.tsx diff --git a/components/attachments/DropArea.tsx b/components/attachments/DropArea.tsx index 0649fbe6..2b7f6f1b 100644 --- a/components/attachments/DropArea.tsx +++ b/components/attachments/DropArea.tsx @@ -1,6 +1,7 @@ import classnames from 'classnames'; import React from 'react'; import { createPortal } from 'react-dom'; +import { AttachmentContext } from './context'; export interface DropUploaderProps { prefixCls: string; @@ -11,6 +12,7 @@ export interface DropUploaderProps { export default function DropArea(props: DropUploaderProps) { const { getDropContainer, className, prefixCls, children } = props; + const { disabled } = React.useContext(AttachmentContext); const [container, setContainer] = React.useState(); const [showArea, setShowArea] = React.useState(null); @@ -62,7 +64,7 @@ export default function DropArea(props: DropUploaderProps) { }, [!!container]); // ============================ Render ============================ - if (!getDropContainer || !container || showArea === null) { + if (!getDropContainer || !container || showArea === null || disabled) { return null; } diff --git a/components/attachments/FileList/FileListCard.tsx b/components/attachments/FileList/FileListCard.tsx index 21b19c62..c606e7da 100644 --- a/components/attachments/FileList/FileListCard.tsx +++ b/components/attachments/FileList/FileListCard.tsx @@ -12,6 +12,7 @@ import { import classNames from 'classnames'; import React from 'react'; import type { Attachment } from '..'; +import { AttachmentContext } from '../context'; import { previewImage } from '../util'; import Progress from './Progress'; @@ -21,7 +22,6 @@ export interface FileListCardProps { onRemove: (item: Attachment) => void; className?: string; style?: React.CSSProperties; - disabled?: boolean; } const EMPTY = '\u00A0'; @@ -90,7 +90,9 @@ function getSize(size: number) { } function FileListCard(props: FileListCardProps, ref: React.Ref) { - const { prefixCls, item, onRemove, className, style, disabled } = props; + const { prefixCls, item, onRemove, className, style } = props; + const { disabled } = React.useContext(AttachmentContext); + const { name, size, percent, status = 'done' } = item; const cardCls = `${prefixCls}-card`; diff --git a/components/attachments/FileList/index.tsx b/components/attachments/FileList/index.tsx index 2f8d1bba..6e55e8bf 100644 --- a/components/attachments/FileList/index.tsx +++ b/components/attachments/FileList/index.tsx @@ -1,7 +1,11 @@ +import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons'; +import { Button, type UploadProps } from 'antd'; import classNames from 'classnames'; import { CSSMotionList } from 'rc-motion'; import React from 'react'; import type { Attachment } from '..'; +import SilentUploader from '../SilentUploader'; +import { AttachmentContext } from '../context'; import FileListCard from './FileListCard'; export interface FileListProps { @@ -9,16 +13,20 @@ export interface FileListProps { items: Attachment[]; onRemove: (item: Attachment) => void; overflow?: 'scrollX' | 'scrollY' | 'wrap'; - disabled?: boolean; + upload: UploadProps; } export default function FileList(props: FileListProps) { - const { prefixCls, items, onRemove, overflow, disabled } = props; + const { prefixCls, items, onRemove, overflow, upload } = props; const listCls = `${prefixCls}-list`; + const containerRef = React.useRef(null); + const [firstMount, setFirstMount] = React.useState(false); + const { disabled } = React.useContext(AttachmentContext); + React.useEffect(() => { setFirstMount(true); return () => { @@ -26,11 +34,61 @@ export default function FileList(props: FileListProps) { }; }, []); + // ================================= Scroll ================================= + const [pingStart, setPingStart] = React.useState(false); + const [pingEnd, setPingEnd] = React.useState(false); + + const checkPing = () => { + const containerEle = containerRef.current; + + if (containerEle) { + if (overflow === 'scrollX') { + setPingStart(containerEle.scrollLeft !== 0); + setPingEnd( + containerEle.scrollWidth - containerEle.clientWidth !== Math.abs(containerEle.scrollLeft), + ); + } else if (overflow === 'scrollY') { + setPingStart(containerEle.scrollTop !== 0); + setPingEnd( + containerEle.scrollHeight - containerEle.clientHeight !== containerEle.scrollTop, + ); + } + } + }; + + React.useEffect(() => { + checkPing(); + }, [overflow]); + + const onScrollOffset = (offset: -1 | 1) => { + const containerEle = containerRef.current; + + if (containerEle) { + containerEle.scrollTo({ + left: containerEle.scrollLeft + offset * containerEle.clientWidth, + behavior: 'smooth', + }); + } + }; + + const onScrollLeft = () => { + onScrollOffset(-1); + }; + + const onScrollRight = () => { + onScrollOffset(1); + }; + + // ================================= Render ================================= return (
({ @@ -52,11 +110,36 @@ export default function FileList(props: FileListProps) { onRemove={onRemove} className={motionCls} style={motionStyle} - disabled={disabled} /> ); }} + {!disabled && ( + + + + )} + + {overflow === 'scrollX' && ( + <> +
); } diff --git a/components/attachments/PlaceholderUploader.tsx b/components/attachments/PlaceholderUploader.tsx index d8d96e93..c82e3982 100644 --- a/components/attachments/PlaceholderUploader.tsx +++ b/components/attachments/PlaceholderUploader.tsx @@ -1,6 +1,7 @@ import { Flex, Typography, Upload, type UploadProps } from 'antd'; import classNames from 'classnames'; import React from 'react'; +import { AttachmentContext } from './context'; export interface PlaceholderConfig { icon?: React.ReactNode; @@ -21,6 +22,8 @@ export default function Placeholder(props: PlaceholderProps) { const placeholderConfig = (placeholder || {}) as PlaceholderConfig; + const { disabled } = React.useContext(AttachmentContext); + // ============================= Drag ============================= const [dragIn, setDragIn] = React.useState(false); @@ -61,10 +64,12 @@ export default function Placeholder(props: PlaceholderProps) {
({}); diff --git a/components/attachments/demo/overflow.tsx b/components/attachments/demo/overflow.tsx index 5dd3647c..14e4e847 100644 --- a/components/attachments/demo/overflow.tsx +++ b/components/attachments/demo/overflow.tsx @@ -1,6 +1,6 @@ import { CloudUploadOutlined } from '@ant-design/icons'; import { Attachments, type AttachmentsProps } from '@ant-design/x'; -import { Flex, Segmented } from 'antd'; +import { Flex, GetProp, Segmented, Switch } from 'antd'; import React from 'react'; const presetFiles: AttachmentsProps['items'] = Array.from({ length: 30 }).map((_, index) => ({ @@ -12,29 +12,47 @@ const presetFiles: AttachmentsProps['items'] = Array.from({ length: 30 }).map((_ })); const Demo = () => { - const [overflow, setOverflow] = React.useState('scrollX'); + const [overflow, setOverflow] = React.useState('wrap'); + const [items, setItems] = React.useState>(presetFiles); + const [disabled, setDisabled] = React.useState(false); return ( - - + + + + setItems((prev) => (prev.length ? [] : presetFiles))} + checkedChildren="Data" + unCheckedChildren="Data" + /> + + setItems(info.fileList)} beforeUpload={() => false} placeholder={{ icon: , title: 'Click or Drop files here', description: 'Support file type: image, video, audio, document, etc.', }} - disabled + disabled={disabled} /> ); diff --git a/components/attachments/demo/with-sender.md b/components/attachments/demo/with-sender.md new file mode 100644 index 00000000..4c1d0b0b --- /dev/null +++ b/components/attachments/demo/with-sender.md @@ -0,0 +1,7 @@ +## zh-CN + +配合 Sender 使用,在对话中插入附件。 + +## en-US + +Work with Sender to insert file into the conversation. diff --git a/components/attachments/demo/with-sender.tsx b/components/attachments/demo/with-sender.tsx new file mode 100644 index 00000000..48f5603e --- /dev/null +++ b/components/attachments/demo/with-sender.tsx @@ -0,0 +1,51 @@ +import { CloudUploadOutlined, LinkOutlined } from '@ant-design/icons'; +import { Attachments, Sender } from '@ant-design/x'; +import { App, Button, Flex, Space, Switch, theme } from 'antd'; +import React from 'react'; + +const Demo = () => { + const [open, setOpen] = React.useState(true); + const { token } = theme.useToken(); + + const senderRef = React.useRef(null); + + const senderHeader = ( + + false} + placeholder={{ + icon: , + title: 'Upload files', + description: 'Click or drag files to this area to upload', + }} + getDropContainer={() => senderRef.current} + /> + + ); + + return ( + + setOpen(!open)} type="text" icon={} />} + /> + + ); +}; + +export default () => ( + + + +); diff --git a/components/attachments/index.en-US.md b/components/attachments/index.en-US.md index 993caad7..3a577163 100644 --- a/components/attachments/index.en-US.md +++ b/components/attachments/index.en-US.md @@ -21,6 +21,7 @@ The Prompts component is used to display a predefined set of questions or sugges Basic Placeholder Overflow +Combination ## API diff --git a/components/attachments/index.tsx b/components/attachments/index.tsx index 7b893887..dcb37f9d 100644 --- a/components/attachments/index.tsx +++ b/components/attachments/index.tsx @@ -1,4 +1,4 @@ -import type { GetProp, UploadProps } from 'antd'; +import { type GetProp, type UploadProps } from 'antd'; import classnames from 'classnames'; import React from 'react'; @@ -10,6 +10,7 @@ import DropArea from './DropArea'; import FileList, { type FileListProps } from './FileList'; import PlaceholderUploader, { PlaceholderProps } from './PlaceholderUploader'; import SilentUploader from './SilentUploader'; +import { AttachmentContext } from './context'; import useStyle from './style'; export type SemanticType = 'list' | 'item' | 'itemContent' | 'title'; @@ -110,40 +111,52 @@ const Attachments: React.FC = (props) => { ); } else { - return ( + renderChildren = (
{fileList.length ? ( - <> - - containerRef.current} - prefixCls={prefixCls} - className={cssinjsCls} - > - {placeholderNode} - - + ) : ( placeholderNode )} + + containerRef.current)} + prefixCls={prefixCls} + className={cssinjsCls} + > + {placeholderNode} +
); } - return wrapCSSVar(renderChildren); + return wrapCSSVar( + + {renderChildren} + , + ); }; if (process.env.NODE_ENV !== 'production') { diff --git a/components/attachments/index.zh-CN.md b/components/attachments/index.zh-CN.md index 5d593cae..ad4b0b9e 100644 --- a/components/attachments/index.zh-CN.md +++ b/components/attachments/index.zh-CN.md @@ -22,6 +22,7 @@ Attachment 组件用于需要展示一组附件信息集合的场景。 基本 占位信息 超出样式 +组合示例 ## API diff --git a/components/attachments/style/index.ts b/components/attachments/style/index.ts index 99e1c7c5..f5e1ac65 100644 --- a/components/attachments/style/index.ts +++ b/components/attachments/style/index.ts @@ -1,10 +1,13 @@ import { CSSObject, unit } from '@ant-design/cssinjs'; import { mergeToken } from '@ant-design/cssinjs-utils'; +import { FastColor } from '@ant-design/fast-color'; import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/cssinjs-utils'; import { genStyleHooks } from '../../theme/genStyleUtils'; // biome-ignore lint/suspicious/noEmptyInterface: ComponentToken need to be empty by default -export interface ComponentToken {} +export interface ComponentToken { + colorBgPlaceholderHover: string; +} export interface AttachmentsToken extends FullToken<'Attachments'> {} @@ -42,13 +45,14 @@ const genAttachmentsStyle: GenerateStyle = (token) => { // ============================= Placeholder ============================= [placeholderCls]: { height: '100%', - background: token.colorBgBlur, - backdropFilter: 'blur(10px)', borderRadius: token.borderRadius, borderWidth: token.lineWidthBold, borderStyle: 'dashed', borderColor: 'transparent', padding: token.padding, + position: 'relative', + backdropFilter: 'blur(10px)', + background: token.colorBgPlaceholderHover, ...anyBoxSizing, [`${antCls}-upload-wrapper ${antCls}-upload${antCls}-upload-btn`]: { @@ -58,6 +62,10 @@ const genAttachmentsStyle: GenerateStyle = (token) => { [`&${placeholderCls}-drag-in`]: { borderColor: token.colorPrimaryHover, }, + [`&${placeholderCls}-disabled`]: { + opacity: 0.25, + pointerEvents: 'none', + }, [`${placeholderCls}-inner`]: { gap: calc(token.paddingXXS).div(2).equal(), @@ -107,20 +115,83 @@ const genFileListStyle: GenerateStyle = (token) => { paddingBlock: token.paddingSM, paddingInline: token.padding, width: '100%', + background: token.colorBgContainer, + + // Hide scrollbar + scrollbarWidth: 'none', + '-ms-overflow-style': 'none', + '&::-webkit-scrollbar': { + display: 'none', + }, // Scroll + '&-overflow-scrollX, &-overflow-scrollY': { + '&:before, &:after': { + content: '""', + position: 'absolute', + opacity: 0, + transition: `opacity ${token.motionDurationSlow}`, + zIndex: 1, + }, + }, + '&-overflow-ping-start:before': { + opacity: 1, + }, + '&-overflow-ping-end:after': { + opacity: 1, + }, + '&-overflow-scrollX': { overflowX: 'auto', overflowY: 'hidden', flexWrap: 'nowrap', + + '&:before, &:after': { + insetBlock: 0, + width: 8, + }, + '&:before': { + insetInlineStart: 0, + background: `linear-gradient(to right, rgba(0,0,0,0.06), rgba(0,0,0,0));`, + }, + '&:after': { + insetInlineEnd: 0, + background: `linear-gradient(to left, rgba(0,0,0,0.06), rgba(0,0,0,0));`, + }, + + '&:dir(rtl)': { + '&:before': { + background: `linear-gradient(to left, rgba(0,0,0,0.06), rgba(0,0,0,0));`, + }, + '&:after': { + background: `linear-gradient(to right, rgba(0,0,0,0.06), rgba(0,0,0,0));`, + }, + }, }, '&-overflow-scrollY': { overflowX: 'hidden', overflowY: 'auto', maxHeight: calc(cardHeight).mul(3).equal(), + + '&:before, &:after': { + insetInline: 0, + height: 8, + }, + + '&:before': { + insetBlockStart: 0, + background: `linear-gradient(to bottom, rgba(0,0,0,0.06), rgba(0,0,0,0));`, + }, + '&:after': { + insetBlockEnd: 0, + background: `linear-gradient(to top, rgba(0,0,0,0.06), rgba(0,0,0,0));`, + }, }, + // ====================================================================== + // == Card == + // ====================================================================== [cardCls]: { borderRadius: token.borderRadius, position: 'relative', @@ -292,12 +363,74 @@ const genFileListStyle: GenerateStyle = (token) => { }, }, }, + + // ====================================================================== + // == Upload == + // ====================================================================== + '&-upload-btn': { + width: cardHeight, + height: cardHeight, + fontSize: token.fontSizeHeading2, + color: '#999', + }, + + // ====================================================================== + // == PrevNext == + // ====================================================================== + '&-prev-btn, &-next-btn': { + position: 'absolute', + top: '50%', + transform: 'translateY(-50%)', + boxShadow: token.boxShadowTertiary, + opacity: 0, + pointerEvents: 'none', + }, + '&-prev-btn': { + left: { + _skip_check_: true, + value: token.padding, + }, + }, + '&-next-btn': { + right: { + _skip_check_: true, + value: token.padding, + }, + }, + + '&:dir(ltr)': { + [`&${fileListCls}-overflow-ping-start ${fileListCls}-prev-btn`]: { + opacity: 1, + pointerEvents: 'auto', + }, + [`&${fileListCls}-overflow-ping-end ${fileListCls}-next-btn`]: { + opacity: 1, + pointerEvents: 'auto', + }, + }, + '&:dir(rtl)': { + [`&${fileListCls}-overflow-ping-end ${fileListCls}-prev-btn`]: { + opacity: 1, + pointerEvents: 'auto', + }, + [`&${fileListCls}-overflow-ping-start ${fileListCls}-next-btn`]: { + opacity: 1, + pointerEvents: 'auto', + }, + }, }, }, }; }; -export const prepareComponentToken: GetDefaultToken<'Attachments'> = () => ({}); +export const prepareComponentToken: GetDefaultToken<'Attachments'> = (token) => { + const { colorBgContainer } = token; + const colorBgPlaceholderHover = new FastColor(colorBgContainer).setA(0.85); + + return { + colorBgPlaceholderHover: colorBgPlaceholderHover.toRgbString(), + }; +}; export default genStyleHooks( 'Attachments', diff --git a/components/sender/index.tsx b/components/sender/index.tsx index 2a825301..565bb4de 100644 --- a/components/sender/index.tsx +++ b/components/sender/index.tsx @@ -1,7 +1,7 @@ import { type ButtonProps, Flex, type GetProps, Input } from 'antd'; import classnames from 'classnames'; -import { useMergedState } from 'rc-util'; +import { useComposeRef, useMergedState } from 'rc-util'; import pickAttrs from 'rc-util/lib/pickAttrs'; import getValue from 'rc-util/lib/utils/get'; import React from 'react'; @@ -73,9 +73,7 @@ function getComponent( return getValue(components, path) || defaultComponent; } -const Sender: React.FC & { - Header: typeof SenderHeader; -} = (props) => { +function Sender(props: SenderProps, ref: React.Ref) { const { prefixCls: customizePrefixCls, styles = {}, @@ -109,6 +107,8 @@ const Sender: React.FC & { const containerRef = React.useRef(null); const inputRef = React.useRef(null); + const mergedContainerRef = useComposeRef(ref, containerRef); + // ======================= Component Config ======================= const contextConfig = useXComponentConfig('sender'); const inputCls = `${prefixCls}-input`; @@ -247,7 +247,7 @@ const Sender: React.FC & { // ============================ Render ============================ return wrapCSSVar(
& {
, ); +} + +const ForwardSender = React.forwardRef(Sender) as React.ForwardRefExoticComponent< + SenderProps & React.RefAttributes +> & { + Header: typeof SenderHeader; }; if (process.env.NODE_ENV !== 'production') { - Sender.displayName = 'Sender'; + ForwardSender.displayName = 'Sender'; } -Sender.Header = SenderHeader; +ForwardSender.Header = SenderHeader; -export default Sender; +export default ForwardSender; diff --git a/package.json b/package.json index 4db240ba..097fc42d 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@ant-design/colors": "^7.1.0", "@ant-design/cssinjs": "^1.21.1", "@ant-design/cssinjs-utils": "^1.1.0", + "@ant-design/fast-color": "^2.0.6", "@ant-design/icons": "^5.4.0", "@babel/runtime": "^7.25.6", "@ctrl/tinycolor": "^4.1.0", From 5743d3a40f2ecf9787c789d03a151b10a8cb06dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 22 Oct 2024 20:03:25 +0800 Subject: [PATCH 12/28] chore: good for drop --- components/attachments/DropArea.tsx | 5 ++- .../attachments/PlaceholderUploader.tsx | 9 ++++-- components/attachments/demo/with-sender.tsx | 16 +++++++--- components/attachments/index.tsx | 32 +++++++++++-------- components/attachments/style/index.ts | 6 ++++ 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/components/attachments/DropArea.tsx b/components/attachments/DropArea.tsx index 2b7f6f1b..6ff8d7ec 100644 --- a/components/attachments/DropArea.tsx +++ b/components/attachments/DropArea.tsx @@ -63,8 +63,11 @@ export default function DropArea(props: DropUploaderProps) { } }, [!!container]); + // =========================== Visible ============================ + const showDropdown = getDropContainer && container && showArea !== null && !disabled; + // ============================ Render ============================ - if (!getDropContainer || !container || showArea === null || disabled) { + if (!showDropdown) { return null; } diff --git a/components/attachments/PlaceholderUploader.tsx b/components/attachments/PlaceholderUploader.tsx index c82e3982..8bbdacfd 100644 --- a/components/attachments/PlaceholderUploader.tsx +++ b/components/attachments/PlaceholderUploader.tsx @@ -9,13 +9,15 @@ export interface PlaceholderConfig { description?: React.ReactNode; } +export type PlaceholderType = PlaceholderConfig | React.ReactElement; + export interface PlaceholderProps { prefixCls: string; - placeholder?: PlaceholderConfig | React.ReactElement; + placeholder?: PlaceholderType; upload?: UploadProps; } -export default function Placeholder(props: PlaceholderProps) { +function Placeholder(props: PlaceholderProps, ref: React.Ref) { const { prefixCls, placeholder = {}, upload } = props; const placeholderCls = `${prefixCls}-placeholder`; @@ -70,6 +72,7 @@ export default function Placeholder(props: PlaceholderProps) { onDragLeave={onDragLeave} onDrop={onDrop} aria-hidden={disabled} + ref={ref} > ); } + +export default React.forwardRef(Placeholder); diff --git a/components/attachments/demo/with-sender.tsx b/components/attachments/demo/with-sender.tsx index 48f5603e..94821e4f 100644 --- a/components/attachments/demo/with-sender.tsx +++ b/components/attachments/demo/with-sender.tsx @@ -23,11 +23,17 @@ const Demo = () => { false} - placeholder={{ - icon: , - title: 'Upload files', - description: 'Click or drag files to this area to upload', - }} + placeholder={(type) => + type === 'drop' + ? { + title: 'Drop file here', + } + : { + icon: , + title: 'Upload files', + description: 'Click or drag files to this area to upload', + } + } getDropContainer={() => senderRef.current} /> diff --git a/components/attachments/index.tsx b/components/attachments/index.tsx index dcb37f9d..9f03856f 100644 --- a/components/attachments/index.tsx +++ b/components/attachments/index.tsx @@ -8,7 +8,7 @@ import { useXProviderContext } from '../x-provider'; import { useEvent, useMergedState } from 'rc-util'; import DropArea from './DropArea'; import FileList, { type FileListProps } from './FileList'; -import PlaceholderUploader, { PlaceholderProps } from './PlaceholderUploader'; +import PlaceholderUploader, { PlaceholderProps, PlaceholderType } from './PlaceholderUploader'; import SilentUploader from './SilentUploader'; import { AttachmentContext } from './context'; import useStyle from './style'; @@ -27,7 +27,7 @@ export interface AttachmentsProps extends Omit { disabled?: boolean; // ============= placeholder ============= - placeholder?: PlaceholderProps['placeholder']; + placeholder?: PlaceholderType | ((type: 'inline' | 'drop') => PlaceholderType); getDropContainer?: null | (() => HTMLElement | null | undefined); // ============== File List ============== @@ -93,24 +93,30 @@ const Attachments: React.FC = (props) => { // ============================ Render ============================ let renderChildren: React.ReactElement; - const placeholderNode = ( - - ); + const getPlaceholderNode = (type: 'inline' | 'drop') => { + const placeholderContent = typeof placeholder === 'function' ? placeholder(type) : placeholder; + + return ( + + ); + }; if (children) { renderChildren = ( <> {children} - {placeholderNode} + {getPlaceholderNode('drop')} ); } else { + const hasFileList = fileList.length > 0; + renderChildren = (
= (props) => { dir={direction || 'ltr'} ref={containerRef} > - {fileList.length ? ( + {hasFileList ? ( = (props) => { upload={mergedUploadProps} /> ) : ( - placeholderNode + getPlaceholderNode('inline') )} = (props) => { prefixCls={prefixCls} className={cssinjsCls} > - {placeholderNode} + {getPlaceholderNode('drop')}
); diff --git a/components/attachments/style/index.ts b/components/attachments/style/index.ts index f5e1ac65..120294a6 100644 --- a/components/attachments/style/index.ts +++ b/components/attachments/style/index.ts @@ -36,6 +36,12 @@ const genAttachmentsStyle: GenerateStyle = (token) => { inset: 0, }, + '&-hide-placement': { + [`${placeholderCls}-inner`]: { + display: 'none', + }, + }, + [placeholderCls]: { padding: 0, }, From ef11a34f43aaa7fb05b49ae34277a8e5e0001f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 23 Oct 2024 11:04:46 +0800 Subject: [PATCH 13/28] docs: update demo --- components/attachments/demo/placeholder.tsx | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/components/attachments/demo/placeholder.tsx b/components/attachments/demo/placeholder.tsx index 2ce80b23..f6615488 100644 --- a/components/attachments/demo/placeholder.tsx +++ b/components/attachments/demo/placeholder.tsx @@ -1,6 +1,6 @@ import { CloudUploadOutlined } from '@ant-design/icons'; import { Attachments, type AttachmentsProps } from '@ant-design/x'; -import { App, Button, Flex, GetProp, Result, Space, Switch, theme } from 'antd'; +import { App, Button, Flex, GetProp, Result, theme } from 'antd'; import React from 'react'; const presetFiles: AttachmentsProps['items'] = [ @@ -51,6 +51,19 @@ const presetFiles: AttachmentsProps['items'] = [ }, ]; +type ExtractFunc = T extends (...args: any) => any ? T : never; + +const getPlaceholderFn = ( + inlinePlaceholder: ReturnType>, +) => { + return (type: 'inline' | 'drop') => + type === 'drop' + ? { + title: 'Drop file here', + } + : inlinePlaceholder; +}; + const Demo = () => { const { message } = App.useApp(); @@ -85,25 +98,25 @@ const Demo = () => {
, title: 'Click or Drop files here', description: 'Support file type: image, video, audio, document, etc.', - }} + })} />
} extra={} style={{ padding: 0 }} - /> - } + />, + )} />
From f88654c110aa93d0d6eed38598d01a5f94e42bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 24 Oct 2024 11:34:44 +0800 Subject: [PATCH 14/28] docs: demo --- components/attachments/demo/with-sender.tsx | 38 ++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/components/attachments/demo/with-sender.tsx b/components/attachments/demo/with-sender.tsx index 94821e4f..32e3bd18 100644 --- a/components/attachments/demo/with-sender.tsx +++ b/components/attachments/demo/with-sender.tsx @@ -1,11 +1,14 @@ import { CloudUploadOutlined, LinkOutlined } from '@ant-design/icons'; -import { Attachments, Sender } from '@ant-design/x'; -import { App, Button, Flex, Space, Switch, theme } from 'antd'; +import { Attachments, AttachmentsProps, Sender } from '@ant-design/x'; +import { App, Button, Flex, type GetProp, Typography } from 'antd'; import React from 'react'; const Demo = () => { const [open, setOpen] = React.useState(true); - const { token } = theme.useToken(); + const [items, setItems] = React.useState>([]); + const [text, setText] = React.useState(''); + + const { notification } = App.useApp(); const senderRef = React.useRef(null); @@ -23,6 +26,8 @@ const Demo = () => { false} + items={items} + onChange={({ fileList }) => setItems(fileList)} placeholder={(type) => type === 'drop' ? { @@ -40,11 +45,36 @@ const Demo = () => { ); return ( - + setOpen(!open)} type="text" icon={} />} + value={text} + onChange={setText} + onSubmit={() => { + notification.info({ + message: 'Mock Submit', + description: ( + +
    +
  • You said: {text}
  • +
  • + Attachments count: {items.length} +
      + {items.map((item) => ( +
    • {item.name}
    • + ))} +
    +
  • +
+
+ ), + }); + + setItems([]); + setText(''); + }} />
); From 91209e6da715e5056aadca9095cabbf2c175aef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 24 Oct 2024 11:53:53 +0800 Subject: [PATCH 15/28] test: basic test case --- components/attachments/DropArea.tsx | 2 - components/attachments/SilentUploader.tsx | 5 +- .../__snapshots__/demo-extend.test.ts.snap | 2780 +++++++++++++---- .../__tests__/__snapshots__/demo.test.ts.snap | 851 ----- .../__snapshots__/demo.test.tsx.snap | 2455 +++++++++++++++ .../__snapshots__/index.test.tsx.snap | 50 +- .../attachments/__tests__/demo-extend.test.ts | 2 +- components/attachments/__tests__/demo.test.ts | 3 - .../attachments/__tests__/demo.test.tsx | 11 + .../attachments/__tests__/image.test.ts | 4 +- .../attachments/__tests__/index.test.tsx | 76 +- components/attachments/index.tsx | 24 +- 12 files changed, 4731 insertions(+), 1532 deletions(-) delete mode 100644 components/attachments/__tests__/__snapshots__/demo.test.ts.snap create mode 100644 components/attachments/__tests__/__snapshots__/demo.test.tsx.snap delete mode 100644 components/attachments/__tests__/demo.test.ts create mode 100644 components/attachments/__tests__/demo.test.tsx diff --git a/components/attachments/DropArea.tsx b/components/attachments/DropArea.tsx index 6ff8d7ec..9a3f60df 100644 --- a/components/attachments/DropArea.tsx +++ b/components/attachments/DropArea.tsx @@ -17,8 +17,6 @@ export default function DropArea(props: DropUploaderProps) { const [container, setContainer] = React.useState(); const [showArea, setShowArea] = React.useState(null); - // ============================= MISC ============================= - // ========================== Container =========================== React.useEffect(() => { const nextContainer = getDropContainer?.(); diff --git a/components/attachments/SilentUploader.tsx b/components/attachments/SilentUploader.tsx index db75bdab..e12aabbe 100644 --- a/components/attachments/SilentUploader.tsx +++ b/components/attachments/SilentUploader.tsx @@ -4,17 +4,18 @@ import React from 'react'; export interface SilentUploaderProps { children: React.ReactElement; upload: UploadProps; + rootClassName?: string; } /** * SilentUploader is only wrap children with antd Upload component. */ export default function SilentUploader(props: SilentUploaderProps) { - const { children, upload } = props; + const { children, upload, rootClassName } = props; // ============================ Render ============================ return ( - + {children} ); diff --git a/components/attachments/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/attachments/__tests__/__snapshots__/demo-extend.test.ts.snap index 8da94454..a3093d63 100644 --- a/components/attachments/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/attachments/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -1,859 +1,2463 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders components/prompts/demo/basic.tsx extend context correctly 1`] = ` +exports[`renders components/attachments/demo/basic.tsx extend context correctly 1`] = `
-
- ✨ Inspirational Sparks and Marvelous Tips -
-
    -
  • - -
  • -
  • - +
+
+ +
-
- - - -
-
- - - Uncover Background Info - - -
- - -
  • - + +
  • +
    +
    +
    +
    +`; + +exports[`renders components/attachments/demo/basic.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/attachments/demo/overflow.tsx extend context correctly 1`] = ` +
    +
    +
    +
    + +
    +
    + +
    -
    -`; - -exports[`renders components/prompts/demo/basic.tsx extend context correctly 2`] = `[]`; - -exports[`renders components/prompts/demo/disabled.tsx extend context correctly 1`] = ` -
    -
    - ☕️ It's time to relax! -
    -
      -
    • -
    -
    - - - Task Completion Secrets - - - - What are some tricks for getting tasks done? - -
    - - -
  • -
  • +
    -
    +
    -
    - - - Time for a Coffee Break - - - - How to rest effectively after long hours of work? - -
    - - - -
    -`; - -exports[`renders components/prompts/demo/disabled.tsx extend context correctly 2`] = `[]`; - -exports[`renders components/prompts/demo/flex-vertical.tsx extend context correctly 1`] = ` -
    -
    - 🤔 You might also want to ask: -
    -
      -
    • -
    +
    -
    +
    -
    - - How to rest effectively after long hours of work? - -
    - - -
  • -
  • +
    -
    +
    -
    - - What are the secrets to maintaining a positive mindset? - -
    - - -
  • -
  • +
    -
    +
    -
    - - How to stay calm under immense pressure? - -
    - - - -
    -`; - -exports[`renders components/prompts/demo/flex-vertical.tsx extend context correctly 2`] = `[]`; - -exports[`renders components/prompts/demo/flex-wrap.tsx extend context correctly 1`] = ` -
    -
    - ✨ Inspirational Sparks and Marvelous Tips -
    -
      -
    • -
    +
    -
    +
    -
    - - Got any sparks for a new project? - -
    - - -
  • -
  • +
    -
    +
    -
    - - Help me understand the background of this topic. - -
    - - -
  • -
  • +
    -
    +
    -
    +
    +
    + preview +
    - - -
  • -
  • +
    -
    +
    -
    +
    +
    + preview +
    - - -
  • -
  • +
    -
    + +
    +
    + preview +
    -
    +
    +
    + preview +
    - - -
  • -
  • +
    -
    +
    -
    +
    +
    + preview +
    - - -
  • -
  • +
    -
    +
    -
    +
    +
    + preview +
    - - -
  • -
  • +
    -
    +
    -
    +
    +
    + preview + +
    +
    + preview +
    - - - -
    -`; - -exports[`renders components/prompts/demo/flex-wrap.tsx extend context correctly 2`] = `[]`; + +
    +
    + preview + +
    +
    + preview + +
    +
    + preview + +
    +
    + preview + +
    +
    + preview + +
    +
    + preview + +
    +
    + preview + +
    +
    + preview + +
    +
    + preview + +
    + +
    + + + + +
    +
    +
    +
    +
    +`; + +exports[`renders components/attachments/demo/overflow.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/attachments/demo/placeholder.tsx extend context correctly 1`] = ` +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    + uploading file +
    +
    + .xlsx +
    +
    +
    +
    + 93% +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    + uploaded file +
    +
    + .docx +
    +
    +
    +
    + 121 KB +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    + upload error with long text file name +
    +
    + .zip +
    +
    +
    +
    + Server Error 500 +
    +
    +
    + +
    +
    + preview +
    +
    +
    + + + + + + + + 33% + + +
    +
    +
    + +
    +
    + preview + +
    +
    + preview +
    +
    +
    + Server Error 500 +
    +
    +
    + +
    + +
    + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    + uploading file +
    +
    + .xlsx +
    +
    +
    +
    + 93% +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    + uploaded file +
    +
    + .docx +
    +
    +
    +
    + 121 KB +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    + upload error with long text file name +
    +
    + .zip +
    +
    +
    +
    + Server Error 500 +
    +
    +
    + +
    +
    + preview +
    +
    +
    + + + + + + + + 33% + + +
    +
    +
    + +
    +
    + preview + +
    +
    + preview +
    +
    +
    + Server Error 500 +
    +
    +
    + +
    + +
    + + + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +`; + +exports[`renders components/attachments/demo/placeholder.tsx extend context correctly 2`] = `[]`; + +exports[`renders components/attachments/demo/with-sender.tsx extend context correctly 1`] = ` +
    +
    +
    +
    +
    +
    + Attachments +
    +
    + +
    +
    +
    +
    +
    + +
    + + +
    +
    + + + + + +
    + Upload files +
    + + Click or drag files to this area to upload + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +