diff --git a/CHANGELOG.md b/CHANGELOG.md index ee23a66d2..ed2575531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,97 @@ toc: false spline: explain --- +## 🌈 1.0.0 `2026-10-15` + +### 🚨 Breaking Changes + +- `Cascader`: 移除 `lazy` 属性,懒加载仅通过 `load`(配合节点 `children=true`)生效 @anlyyao ([#964](https://github.com/Tencent/tdesign-mobile-react/pull/964)) +- `ColorPicker`: 删除 `HSB` 颜色格式,移除无效的 `onClose` 属性 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Guide`: 调整 `GuideStep` 的 `body`、`title`、`content` 和 `highlightContent` 类型 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Icon`: `TdIconfontProps` 重命名为 `TdIconFontProps` @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `NoticeBar`: `onChange` 参数变更,支持监听垂直轮播切换事件 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Picker`: 移除无效的 `visible` 属性,`swipeDuration` 标记为废弃,建议使用 `wheelConfig.inertiaDuration` @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Swiper`: 传入 `current` 时不再强制进入受控模式,默认值改由 `defaultCurrent` 管理 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `TabBar`: `onChange` 从直接接收选中值调整为接收包含 `value` 的对象参数 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) + +### 🚀 Features + +- `ActionSheet`: `ActionSheetItem` 的 `icon` 字段补充 `string` 类型 @anlyyao ([#960](https://github.com/Tencent/tdesign-mobile-react/pull/960)) +- `Button`: + - 优化点击态过度效果 @anlyyao ([common#2692](https://github.com/Tencent/tdesign-common/pull/2692)) + - 新增 `tag`、`href` 和 `form` 属性 @anlyyao ([#959](https://github.com/Tencent/tdesign-mobile-react/pull/959)) +- `Cascader`: + - 新增 `load` 属性,调整懒加载链路为基于 `load + children=true` @anlyyao ([#964](https://github.com/Tencent/tdesign-mobile-react/pull/964)) + - 新增 `filterable`、`filter` 和 `filter-placeholder` 属性,支持搜索功能 @boomLL ([#976](https://github.com/Tencent/tdesign-mobile-react/pull/976)) +- `CheckboxGroup`: 新增 `direction` 属性,表示复选框按钮排列方式 @anlyyao ([#974](https://github.com/Tencent/tdesign-mobile-react/pull/974)) +- `ColorPicker`: 新增 `HEX8` 颜色格式支持 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Comment`: 新增 `Comment` 评论组件 @slatejack ([#978](https://github.com/Tencent/tdesign-mobile-react/pull/978)) +- `Dropdown`: `DropdownOption` 的 `disabled` 字段改为可选项 @anlyyao ([#987](https://github.com/Tencent/tdesign-mobile-react/pull/987)) +- `DropdownMenu`: 新增 `onOpen`,`onOpened`,`onClose` 与 `onClosed` 属性,弹层动画时长现与 `duration` 配置保持一致 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Fab`: 新增 `xBounds` 和 `magnet` 属性 @boomLL ([#979](https://github.com/Tencent/tdesign-mobile-react/pull/979)) +- `Guide`: + - 新增 `hideBack` 属性,用于隐藏返回按钮。 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) + - `GuideStep` 新增 `children` @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) + - `GuideStep` 新增 `finishButtonProps` 属性,支持为不同步骤配置完成按钮 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Icon`: 新增 `tablet` 和 `tablet-filled` 图标 @RADWIMPS426 ([icons#293](https://github.com/Tencent/tdesign-icons/pull/293)) ([#951](https://github.com/Tencent/tdesign-mobile-react/pull/951)) +- `ImageViewer`: + - 新增 `cover` 属性,支持自定义图片上层覆盖内容 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) + - 新增 `image` 属性,支持自定义图片渲染 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) + - 新增 `loop` 属性,控制是否循环预览 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) + - `ImageInfo` 新增 `extra` 扩展数据 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Indexes`: 新增受控属性 `current` 和非受控属性 `defaultCurrent`,支持根据当前索引同步滚动位置和激活状态 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `NoticeBar`: 新增 `interval`,用于配置垂直轮播间隔 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Picker`: + - 新增 `wheelConfig`,支持配置惯性、回弹、过渡和点击阈值。 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) + - `PickerColumnItem` 新增 `disabled` @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `PullDownRefresh`: 新增 `successDuration`,用于配置刷新成功状态的展示时长,默认值为 500ms @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `Radio`: `RadioGroup.keys` 新增对 `disabled` 字段别名的支持 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) +- `RadioGroup`: 新增 `direction` 属性,表示单选框按钮排列方式 @anlyyao ([#974](https://github.com/Tencent/tdesign-mobile-react/pull/974)) +- `Search`: 新增 `maxcharacter` 和 `maxlength` 属性 @anlyyao ([#963](https://github.com/Tencent/tdesign-mobile-react/pull/963)) +- `Slider`: + - 实现 `onDragstart` 和 `onDragend` 拖动生命周期事件 @anlyyao ([#966](https://github.com/Tencent/tdesign-mobile-react/pull/966)) + - 新增 `vertical` 属性,支持垂直滑块 @liweijie0812 ([#991](https://github.com/Tencent/tdesign-mobile-react/pull/991)) +- `SwipeCell`: + - 新增 `onDragstart` 和 `onDragend` 事件 @boomLL ([#977](https://github.com/Tencent/tdesign-mobile-react/pull/977)) + - 支持通过实例方法 `showSure` 展示二次确认内容 @liweijie0812 ([#981](https://github.com/Tencent/tdesign-mobile-react/pull/981)) + +### 🐞 Bug Fixes + +- `ActionSheet`: 修复子项激活态样式缺失的问题 @anlyyao ([common#2691](https://github.com/Tencent/tdesign-common/pull/2691)) +- `Collapse`: 修复面板展开时底部线框样式错误 @anlyyao ([#962](https://github.com/Tencent/tdesign-mobile-react/pull/962)) +- `ConfigProvider`: + - `TableConfig`全局配置项 `pullUpLoadingText` 重命名为 `loadingText` @anlyyao ([#945](https://github.com/Tencent/tdesign-mobile-react/pull/945)) + - 修复语言包污染全局默认配置的问题 @anlyyao ([#962](https://github.com/Tencent/tdesign-mobile-react/pull/962)) +- `Form`: + - 修复 `FormItem` 中 `arrow` 图标颜色错误 @yzh1002 ([common#2738](https://github.com/Tencent/tdesign-common/pull/2738)) +- `Icon`: 修复 usb 原始图标的图层 ID 错误,在深色模式下渲染错误 @uyarn ([icons#298](https://github.com/Tencent/tdesign-icons/pull/298)) ([#953](https://github.com/Tencent/tdesign-mobile-react/pull/953)) +- `Input`: 修复 `input` 密码框样式错误 @anlyyao ([#988](https://github.com/Tencent/tdesign-mobile-react/pull/988)) +- `NoticeBar`: 修复横向滚动动画异常与滚动速度不符 @anlyyao ([#962](https://github.com/Tencent/tdesign-mobile-react/pull/962)) +- `Rate`: 修复深色模式下的提示文本颜色 @liweijie0812 ([common#2706](https://github.com/Tencent/tdesign-common/pull/2706)) +- `Search`: 修复搜索框清空图标垂直居中问题 @anlyyao ([common#2663](https://github.com/Tencent/tdesign-common/pull/2663)) +- `Slider`: 修复滑块与进度条错位与胶囊模式滑轨圆角样式错误 @liweijie0812 ([#991](https://github.com/Tencent/tdesign-mobile-react/pull/991)) +- `Tag`: 修复圆角样式值错误 @anlyyao ([common#2716](https://github.com/Tencent/tdesign-common/pull/2716)) +- `Textarea`: 修复 `placeholder` 占位内容字号错误 @anlyyao ([common#2717](https://github.com/Tencent/tdesign-common/pull/2717)) +- `Toast`: `clear/destroy` 统一走 `unmount -> remove`,修复锁滚动残留导致的页面“禁用” @anlyyao ([#964](https://github.com/Tencent/tdesign-mobile-react/pull/964)) +- `Upload`: + - 修复 `useMockProgress=false` 时真实上传进度不回调的问题 @anlyyao ([common#2690](https://github.com/Tencent/tdesign-common/pull/2690)) + - 修复开启 `preview` 后点击图片无法触发预览及 `onPreview` 事件 @anlyyao ([#961](https://github.com/Tencent/tdesign-mobile-react/pull/961)) + +### 📈 Performance + +- `Dialog`: plugin 模式移除写死的 zIndex 默认值(原 2500),层级统一由样式表提供(@z-index-popup = 1500) @anlyyao ([#980](https://github.com/Tencent/tdesign-mobile-react/pull/980)) +- `DropdownMenu`: 规范层级默认值,由 11600 调整为 1600,由样式表提供(@z-index-dropdown-menu = 1600) @anlyyao ([#980](https://github.com/Tencent/tdesign-mobile-react/pull/980)) +- `Message`: 规范层级默认值,由 15000 调整为 5000,由样式表提供(@z-index-message = 5000) @anlyyao ([#980](https://github.com/Tencent/tdesign-mobile-react/pull/980)) +- `Overlay`: zIndex 移除内联默认值 1000,层级改由样式表 @z-index-overlay(1000)提供 @anlyyao ([#980](https://github.com/Tencent/tdesign-mobile-react/pull/980)) +- `Popup`: 规范层级默认值,由 11500 调整为 1500,由样式表提供(@z-index-popup = 1500) @anlyyao ([#980](https://github.com/Tencent/tdesign-mobile-react/pull/980)) +- `Toast`: 规范层级默认值,由 12001 调整为 2000,由样式表提供(@z-index-toast = 2000) @anlyyao ([#980](https://github.com/Tencent/tdesign-mobile-react/pull/980)) +- `Style`: 修正灰阶色值。深色主题:`--td-gray-color-11` 由 `#383838` 修正为 `#393939`;浅色主题:`--td-gray-color-3` 由 `#e7e7e7` 修正为 `#e8e8e8`,`--td-gray-color-4` 由 `#dcdcdc` 修正为 `#ddd`,`--td-gray-color-5` 由 `#c5c5c5` 修正为 `#c6c6c6`,`--td-gray-color-11` 由 `#383838` 修正为 `#393939` @anlyyao ([common#2715](https://github.com/Tencent/tdesign-common/pull/2715)) + +### Other + +- `lib`: 修复 `lib` 产物中 `style/css.js` 引用不存在的 `index.css` 导致的样式断链问题 @anlyyao ([#967](https://github.com/Tencent/tdesign-mobile-react/pull/967)) +- `dist`: 修复 UMD 产物中 `react-dom/client` 被错误映射为 `global.client` 的问题 @anlyyao ([#967](https://github.com/Tencent/tdesign-mobile-react/pull/967)) + ## 🌈 0.23.3 `2026-08-03` ### 🐞 Bug Fixes diff --git a/package.json b/package.json index 1a3601552..ecd057ab0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tdesign-mobile-react", - "version": "1.0.0-beta", + "version": "1.0.0", "description": "TDesign Component for Mobile React", "title": "tdesign-mobile-react", "main": "cjs/index.js", @@ -116,7 +116,6 @@ "concurrently": "^6.4.0", "cross-env": "^10.0.0", "cssnano": "^5.0.12", - "csstype": "^3.1.3", "cz-conventional-changelog": "^3.3.0", "dom-parser": "^1.1.5", "eslint": "^8.4.1", diff --git a/site/docs/overview.en-US.md b/site/docs/overview.en-US.md index 8a9211ab6..c2d6d50c9 100644 --- a/site/docs/overview.en-US.md +++ b/site/docs/overview.en-US.md @@ -277,6 +277,13 @@ spline: explain

CountDown

+
+ + + +

Comment

+
+
@@ -484,4 +491,4 @@ spline: explain

Toast

- \ No newline at end of file + diff --git a/site/docs/overview.md b/site/docs/overview.md index fde7e4371..8892fa723 100644 --- a/site/docs/overview.md +++ b/site/docs/overview.md @@ -277,6 +277,13 @@ spline: explain

CountDown 倒计时

+
+ + + +

Comment 评论

+
+
@@ -484,4 +491,4 @@ spline: explain

Toast 轻提示

- \ No newline at end of file + diff --git a/site/mobile/mobile.config.js b/site/mobile/mobile.config.js index feea7c5af..fd0c5e03f 100644 --- a/site/mobile/mobile.config.js +++ b/site/mobile/mobile.config.js @@ -428,5 +428,11 @@ export default { name: 'typography', component: () => import('tdesign-mobile-react/typography/_example/index.tsx'), }, + { + title: 'Comment 评论', + titleEn: 'Comment', + name: 'comment', + component: () => import('tdesign-mobile-react/comment/_example/index.tsx'), + }, ], }; \ No newline at end of file diff --git a/site/vite.config.mjs b/site/vite.config.mjs index a8726806e..1bc30a745 100644 --- a/site/vite.config.mjs +++ b/site/vite.config.mjs @@ -8,6 +8,7 @@ import pwaConfig from './pwaConfig'; import changelog2Json from './web/plugins/changelog-to-json'; import tdocPlugin from './web/plugins/plugin-tdoc'; +import generateLlms from './web/plugins/generate-llms'; const publicPathMap = { preview: '/', @@ -65,6 +66,7 @@ export default ({ mode }) => react(), tdocPlugin(), changelog2Json(), + generateLlms(), VitePWA(pwaConfig), disableTreeShakingPlugin(['style/']), replace({ __DATE__: new Date().toISOString() }), diff --git a/site/web/plugins/generate-llms/index.js b/site/web/plugins/generate-llms/index.js new file mode 100644 index 000000000..28b67eeb0 --- /dev/null +++ b/site/web/plugins/generate-llms/index.js @@ -0,0 +1,110 @@ +import { promises, readFileSync, existsSync } from 'fs'; +import path from 'path'; + +import generateLlmsDocs, { createComponentDocParser } from '../../../../src/_common/docs/plugins/generate-llms'; + +/** 列出 demo 的候选路径:_example/.tsx(含 kebab/camelCase 双写)与 _example//index.tsx。 */ +function demoCandidates(componentDir, demoName) { + const camelName = demoName.includes('-') ? demoName.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase()) : demoName; + const names = camelName === demoName ? [demoName] : [demoName, camelName]; + return names.flatMap((name) => [ + path.join(componentDir, '_example', `${name}.tsx`), + path.join(componentDir, '_example', name, 'index.tsx'), + ]); +} + +/** + * 读取组件目录下的 demo 源码:_example/.tsx,回退 _example//index.tsx。 + * 输出 TSX 代码块。 + */ +function readTsxDemo(componentDir, demoName) { + for (const candidate of demoCandidates(componentDir, demoName)) { + try { + const content = readFileSync(candidate, 'utf-8'); + if (content.trim()) return `\`\`\`tsx\n${content.trim()}\n\`\`\``; + } catch { + // 继续尝试下一个候选路径 + } + } + return ''; +} + +/** + * 判断是否为 demo 占位符:匹配 _example/.tsx 或 _example//index.tsx。 + * 本仓库 demo 为扁平 .tsx 文件(非目录),默认目录判断不命中,需自定义以正确替换 {{ demo }}。 + */ +function isDemoSlot(componentDir, demoName) { + return demoCandidates(componentDir, demoName).some((candidate) => existsSync(candidate)); +} + +/** + * 将文档中 camelCase 的 {{ demoName }} 占位符归一化为 kebab-case(如 {{ iconDemo }} -> {{ icon-demo }}), + * 以命中通用占位符正则(仅匹配小写);仅当对应 demo 文件确实存在时才改写,避免误伤正文内容。 + */ +function normalizeDemoPlaceholders(doc, componentDir) { + return doc.replace(/\{\{\s*([a-zA-Z][a-zA-Z0-9]*)\s*\}\}/g, (match, name) => { + if (!/[A-Z]/.test(name) || !isDemoSlot(componentDir, name)) return match; + const kebabName = name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); + return `{{ ${kebabName} }}`; + }); +} + +/** + * vite 插件:站点构建时,基于组件清单映射生成组件的 LLM Markdown 文档。 + * 核心逻辑为纯 JS 方法 generateLlmsDocs(来自 common 的 docs/plugins/generate-llms), + * 此处仅负责 vite 构建钩子分发,并按 mobile-react 仓库约定注入组件文档与 demo 源码读取器。 + */ +export default function generateLlmsPlugin() { + let config; + return { + name: 'generate-llms', + configResolved(resolvedConfig) { + config = resolvedConfig; + }, + async closeBundle(error) { + if (error) return; + if (!config.env.PROD && config.env.MODE !== 'preview') return; + + // 基于 config.root 推导路径,避免依赖 __dirname 多层回溯 + const siteRoot = config.root; + const componentsRoot = path.resolve(siteRoot, '../src'); + // common 子仓文档根目录(组件文档为 docs/mobile/api/.md,含 frontmatter 与 {{ demo }} 占位符) + const docsRoot = path.resolve(siteRoot, '../src/_common/docs'); + // 产物输出目录:从 config.build.outDir 推导,避免硬编码 dist + const outputDir = path.resolve(siteRoot, config.build.outDir || 'dist'); + + // 组件文档读取器:common 子仓 docs/mobile/api/.md(frontmatter + 代码演示 + API), + // 并追加组件目录 .md 的 API 部分(站点构建时由 :: BASE_DOC :: 注入,此处手动拼接) + const readComponentDoc = async (componentDir, slug) => { + const docPath = path.join(docsRoot, 'mobile/api', `${slug}.md`); + try { + const raw = await promises.readFile(docPath, 'utf-8'); + const apiDoc = await promises.readFile(path.join(componentDir, `${slug}.md`), 'utf-8').catch(() => ''); + const apiBody = apiDoc.replace(/^[^\S\n]*::\s*BASE_DOC\s*::[^\S\n]*\n?/m, '').trim(); + const doc = apiBody ? `${raw}\n\n${apiBody}\n` : raw; + return normalizeDemoPlaceholders(doc, componentDir); + } catch { + return null; + } + }; + + // 通用文档解析管道:读取 frontmatter -> 替换 demo -> 清理正文 + const parseComponentDoc = createComponentDocParser({ + readComponentDoc, + readDemoCode: readTsxDemo, + isDemoSlot, + transformers: [], + }); + + await generateLlmsDocs({ + componentsRoot, + outputDir, + platform: 'mobile', + parseComponentDoc, + siteTitle: 'TDesign Mobile React', + siteDescription: 'TDesign 适配移动端的组件库,适合在 React 技术栈项目中使用。', + siteBaseUrl: config.base, + }); + }, + }; +} diff --git a/site/web/site.config.js b/site/web/site.config.js index 884b5a43e..0816b1390 100644 --- a/site/web/site.config.js +++ b/site/web/site.config.js @@ -375,6 +375,14 @@ export const docs = [ component: () => import('tdesign-mobile-react/count-down/count-down.md'), componentEn: () => import('tdesign-mobile-react/count-down/count-down.en-US.md'), }, + { + title: 'Comment 评论', + titleEn: 'Comment', + name: 'comment', + path: '/mobile-react/components/comment', + component: () => import('tdesign-mobile-react/comment/comment.md'), + componentEn: () => import('tdesign-mobile-react/comment/comment.en-US.md'), + }, { title: 'Empty 空状态', titleEn: 'Empty', diff --git a/src/_common b/src/_common index 3877b3252..d9734f64a 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 3877b3252de69cdef86172f14e35d63bad3a0960 +Subproject commit d9734f64af0e863f70e7f2c839a15278ca3d6867 diff --git a/src/action-sheet/__tests__/ActionSheetMethod.test.tsx b/src/action-sheet/__tests__/ActionSheetMethod.test.tsx index ac8804cea..bd5440ac3 100644 --- a/src/action-sheet/__tests__/ActionSheetMethod.test.tsx +++ b/src/action-sheet/__tests__/ActionSheetMethod.test.tsx @@ -1,10 +1,12 @@ -import { describe, it, expect, afterEach } from '@test/utils'; +import { describe, it, expect, afterEach, act } from '@test/utils'; import { show, close } from '../ActionSheetMethod'; describe('ActionSheetMethod', () => { - afterEach(() => { + afterEach(async () => { // 清理每次测试后可能残留的 ActionSheet - close(); + await act(async () => { + close(); + }); }); describe('module exports', () => { @@ -13,51 +15,63 @@ describe('ActionSheetMethod', () => { expect(typeof close).toBe('function'); }); - it('should be able to call show function', () => { - expect(() => { - show({ - items: ['Item 1', 'Item 2'], - }); - }).not.toThrow(); + it('should be able to call show function', async () => { + await act(async () => { + expect(() => { + show({ + items: ['Item 1', 'Item 2'], + }); + }).not.toThrow(); + }); }); - it('should be able to call close function', () => { - expect(() => { - close(); - }).not.toThrow(); + it('should be able to call close function', async () => { + await act(async () => { + expect(() => { + close(); + }).not.toThrow(); + }); }); - it('should handle show with empty config', () => { - expect(() => { - show({}); - }).not.toThrow(); + it('should handle show with empty config', async () => { + await act(async () => { + expect(() => { + show({}); + }).not.toThrow(); + }); }); - it('should handle show with various config options', () => { - expect(() => { - show({ - items: ['Test Item'], - theme: 'grid', - description: 'Test Description', - visible: true, - }); - }).not.toThrow(); + it('should handle show with various config options', async () => { + await act(async () => { + expect(() => { + show({ + items: ['Test Item'], + theme: 'grid', + description: 'Test Description', + visible: true, + }); + }).not.toThrow(); + }); }); - it('should handle multiple show calls', () => { - expect(() => { - show({ items: ['Item 1'] }); - show({ items: ['Item 2'] }); - }).not.toThrow(); + it('should handle multiple show calls', async () => { + await act(async () => { + expect(() => { + show({ items: ['Item 1'] }); + show({ items: ['Item 2'] }); + }).not.toThrow(); + }); }); - it('should handle show and close sequence', () => { - expect(() => { - show({ items: ['Item 1'] }); - close(); - show({ items: ['Item 2'] }); - close(); - }).not.toThrow(); + it('should handle show and close sequence', async () => { + await act(async () => { + expect(() => { + show({ items: ['Item 1'] }); + close(); + show({ items: ['Item 2'] }); + close(); + }).not.toThrow(); + }); }); }); }); diff --git a/src/cascader/Cascader.tsx b/src/cascader/Cascader.tsx index b1c0a50cf..ab1cdb026 100644 --- a/src/cascader/Cascader.tsx +++ b/src/cascader/Cascader.tsx @@ -1,22 +1,110 @@ import { useDeepCompareEffect } from 'ahooks'; import classNames from 'classnames'; import last from 'lodash-es/last'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CloseIcon, ChevronRightIcon } from 'tdesign-icons-react'; import useDefault from '../_util/useDefault'; import { Popup } from '../popup'; import { RadioGroup } from '../radio'; +import { Search } from '../search'; import Tabs from '../tabs'; import { StyledProps, TreeOptionData } from '../common'; import { usePrefixClass } from '../hooks/useClass'; import useConfig from '../hooks/useConfig'; import useDefaultProps from '../hooks/useDefaultProps'; +import useLatest from '../hooks/useLatest'; import parseTNode from '../_util/parseTNode'; import { cascaderDefaultProps } from './defaultProps'; -import { CascaderTriggerSource, TdCascaderProps } from './type'; +import { CascaderFilterFunction, CascaderTriggerSource, TdCascaderProps } from './type'; + +type OptionsType = TreeOptionData[]; + +type FlatPath = { + path: TreeOptionData[]; + indexes: number[]; + labels: string[]; + disabled: boolean; + isLeaf: boolean; +}; + +type ResultFragment = { id: number; text: string; highlight: boolean }; + +type FilterResult = { + indexes: number[]; + disabled: boolean; + fragments: ResultFragment[]; +}; export interface CascaderProps extends TdCascaderProps, StyledProps {} +function flattenPaths( + options: OptionsType, + keys: { label: string; value: string; children: string; disabled: string }, +): FlatPath[] { + const result: FlatPath[] = []; + + const walk = (list: TreeOptionData[], path: TreeOptionData[], indexes: number[]) => { + list.forEach((item, idx) => { + const nextPath = [...path, item]; + const nextIndexes = [...indexes, idx]; + const children = item[keys.children]; + // checkStrictly 下父节点也可被搜索选中,因此父节点同样生成一条路径并标记 isLeaf + const isLeaf = !(Array.isArray(children) && children.length > 0); + + result.push({ + path: nextPath, + indexes: nextIndexes, + labels: nextPath.map((node) => String(node[keys.label] ?? '')), + disabled: nextPath.some((node) => node[keys.disabled]), + isLeaf, + }); + + if (!isLeaf) { + walk(children as TreeOptionData[], nextPath, nextIndexes); + } + }); + }; + + walk(options || [], [], []); + return result; +} + +function buildFragments(labels: string[], keyword: string): ResultFragment[] { + const joined = labels.join(' / '); + const push = (acc: ResultFragment[], text: string, highlight: boolean) => { + if (!text) return; + acc.push({ id: acc.length, text, highlight }); + }; + + if (!keyword) return [{ id: 0, text: joined, highlight: false }]; + + const fragments: ResultFragment[] = []; + const haystack = joined.toLowerCase(); + const needle = keyword.toLowerCase(); + let cursor = 0; + while (cursor < joined.length) { + const hit = haystack.indexOf(needle, cursor); + if (hit === -1) { + push(fragments, joined.slice(cursor), false); + break; + } + push(fragments, joined.slice(cursor, hit), false); + push(fragments, joined.slice(hit, hit + needle.length), true); + cursor = hit + needle.length; + } + return fragments.length ? fragments : [{ id: 0, text: joined, highlight: false }]; +} + +function defaultFilter(keyword: string, _option: TreeOptionData, path: TreeOptionData[], labelKey: string): boolean { + const lower = keyword.toLowerCase(); + const joined = path + .map((node) => String(node?.[labelKey] ?? '')) + .join('') + .toLowerCase(); + const text = String(path[path.length - 1]?.text ?? '').toLowerCase(); + return joined.includes(lower) || (!!text && text.includes(lower)); +} + const Cascader: React.FC = (props) => { const cascaderClass = usePrefixClass('cascader'); const globalConfig = useConfig(); @@ -39,18 +127,28 @@ const Cascader: React.FC = (props) => { checkStrictly, closeBtn, load, + filter, + filterPlaceholder, + filterable, onChange, onClose, onPick, } = useDefaultProps(props, cascaderDefaultProps); const placeholderText = placeholder ?? globalConfig?.cascader?.placeholder; + const filterPlaceholderText = filterPlaceholder ?? globalConfig?.cascader?.filterPlaceholder; const [internalValue, setInternalValue] = useDefault(value, defaultValue, onChange); const [internalVisible, setInternalVisible] = useDefault(visible, false, () => ({})); const [internalSelectedValues, setInternalSelectedValues] = useState([]); const [stepIndex, setStepIndex] = useState(0); + // 搜索相关状态 + const [filterKeyword, setFilterKeyword] = useState(''); + const [filterResults, setFilterResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const filterDebouncedRef = useRef | null>(null); + const normalizedKeys = useMemo( () => ({ label: keys?.label ?? 'label', @@ -157,6 +255,179 @@ const Cascader: React.FC = (props) => { [onClose, normalizedKeys, optionsList, setInternalValue, setInternalVisible], ); + // 搜索相关函数 + const flatPaths = useMemo(() => flattenPaths(inputOptions, normalizedKeys), [inputOptions, normalizedKeys]); + + const resetFilter = useCallback(() => { + setFilterKeyword(''); + setFilterResults([]); + setIsSearching(false); + }, []); + + const applyFilter = useCallback( + (rawKeyword: string) => { + const keyword = String(rawKeyword ?? '').trim(); + if (!keyword) { + resetFilter(); + return; + } + + const userFilter = filter as CascaderFilterFunction | null; + const results: FilterResult[] = []; + + flatPaths.forEach((entry: FlatPath) => { + // 非 checkStrictly 仅保留叶子节点,checkStrictly 时父节点也可被搜索选中 + if (!checkStrictly && !entry.isLeaf) return; + + const leaf = entry.path[entry.path.length - 1]; + const matched = + typeof userFilter === 'function' + ? !!userFilter(keyword, leaf, entry.path) + : defaultFilter(keyword, leaf, entry.path, normalizedKeys.label); + if (matched) { + results.push({ + indexes: entry.indexes, + disabled: entry.disabled, + fragments: buildFragments(entry.labels, keyword), + }); + } + }); + + setFilterResults(results); + setIsSearching(true); + }, + [filter, normalizedKeys, flatPaths, resetFilter, checkStrictly], + ); + + const applyFilterLatest = useLatest(applyFilter); + + // options 变化时,若处于搜索态,基于最新数据重新过滤,避免结果过期 + useEffect(() => { + if (isSearching) { + applyFilter(filterKeyword); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [flatPaths]); + + const handleFilterChange = useCallback( + (value: string) => { + const trimmed = String(value ?? '').trim(); + setFilterKeyword(trimmed); + if (filterDebouncedRef.current) { + clearTimeout(filterDebouncedRef.current); + } + filterDebouncedRef.current = setTimeout(() => { + applyFilterLatest.current(trimmed); + }, 200); + }, + [applyFilterLatest], + ); + + const handleFilterClear = useCallback(() => { + if (filterDebouncedRef.current) { + clearTimeout(filterDebouncedRef.current); + } + resetFilter(); + }, [resetFilter]); + + const handleFilterResultTap = useCallback( + (result: FilterResult) => { + if (result.disabled || result.indexes.length === 0) return; + + const { indexes } = result; + const labelKey = normalizedKeys.label; + const valueKey = normalizedKeys.value; + const childrenKey = normalizedKeys.children; + + // 遍历路径,找到最终节点及其层级 + let current: TreeOptionData[] = inputOptions; + let finalNode: TreeOptionData | undefined; + let finalLevel = 0; + const selectedValues: CascaderProps['value'][] = []; + const selectedOptions: { [key: string]: any }[] = []; + + for (let i = 0; i < indexes.length; i++) { + const index = indexes[i]; + const next = current[index]; + // options 可能已变化,索引失效时直接终止,避免访问越界 + if (!next) return; + selectedValues.push(next[valueKey]); + selectedOptions.push({ + [labelKey]: next[labelKey], + [valueKey]: next[valueKey], + }); + finalNode = next; + finalLevel = i; + + const children = next[childrenKey]; + if (Array.isArray(children) && children.length > 0) { + current = children as TreeOptionData[]; + } + } + + onPick?.({ + value: finalNode[valueKey], + label: String(finalNode[labelKey] || ''), + index: indexes[indexes.length - 1], + level: finalLevel, + }); + + if (finalNode && finalNode[childrenKey] === true && load) { + load({ data: finalNode, value: finalNode[valueKey], label: finalNode[labelKey] }) + .then((loadedChildren) => { + setInternalSelectedValues(selectedValues); + setOptionsList((prev) => [...prev.slice(0, finalLevel + 1), normalizeOptionsByKeys(loadedChildren)]); + setStepIndex(finalLevel + 1); + resetFilter(); + }) + .catch((error) => { + console.error('Load children failed:', error); + }); + return; + } + + resetFilter(); + setInternalValue(last(selectedValues), selectedOptions as any); + setInternalVisible(false); + onClose?.('finish'); + }, + [ + normalizedKeys, + inputOptions, + setInternalValue, + setInternalVisible, + onClose, + resetFilter, + onPick, + load, + normalizeOptionsByKeys, + ], + ); + + // 清理防抖定时器 + useEffect( + () => () => { + if (filterDebouncedRef.current) { + clearTimeout(filterDebouncedRef.current); + } + }, + [], + ); + + // visible 变化时重置搜索状态 + useEffect(() => { + if (!internalVisible) { + resetFilter(); + } + }, [internalVisible, resetFilter]); + + // filterable 关闭时重置搜索状态 + useEffect(() => { + if (!filterable && isSearching) { + resetFilter(); + } + }, [filterable, isSearching, resetFilter]); + return ( = (props) => { {parseTNode(header)}
- {labelList.length > 0 && ( -
- {theme === 'step' ? ( -
- {labelList.map((labelItem, index) => ( -
setStepIndex(index)}> -
-
- {parseTNode(labelItem.label)} -
- + {/* 搜索框 */} + {filterable && ( +
+ +
+ )} + + {!isSearching ? ( + <> + {labelList.length > 0 && ( +
+ {theme === 'step' ? ( +
+ {labelList.map((labelItem, index) => ( +
setStepIndex(index)}> +
+
+ {parseTNode(labelItem.label)} +
+ +
+ ))}
- ))} + ) : null} + {theme === 'tab' && internalVisible ? ( + ({ + label: item.label, + value: index, + }))} + spaceEvenly={false} + value={stepIndex} + onChange={(nextValue: number) => { + setStepIndex(nextValue); + }} + /> + ) : null}
+ )} + {parseTNode(middleContent)} + {subTitles[stepIndex] ? ( +
{subTitles[stepIndex]}
) : null} - {theme === 'tab' && internalVisible ? ( - ({ - label: item.label, - value: index, - }))} - spaceEvenly={false} - value={stepIndex} - onChange={(nextValue: number) => { - setStepIndex(nextValue); - }} - /> - ) : null} -
- )} - {parseTNode(middleContent)} - {subTitles[stepIndex] ? ( -
{subTitles[stepIndex]}
- ) : null} -
- {optionsList.map((currentOptions, index) => ( -
-
- { - const targetIndex = currentOptions.findIndex((item) => item.value === nextValue); - const target = currentOptions[targetIndex]; - const selectedValues = [...internalSelectedValues.slice(0, index), nextValue]; - - setInternalSelectedValues(selectedValues); - onPick?.({ - value: nextValue, - label: String(target?.label || ''), - index: targetIndex, - level: index, - }); - - if (target?.children === true && load) { - load({ data: target.data, value: target.value, label: target.label }) - .then((loadedChildren) => { - setOptionsList((prev) => [ - ...prev.slice(0, index + 1), - normalizeOptionsByKeys(loadedChildren), - ]); - setStepIndex(index + 1); - }) - .catch((error) => { - console.error('Load children failed:', error); +
+ {optionsList.map((currentOptions, index) => ( +
+
+ { + const targetIndex = currentOptions.findIndex((item) => item.value === nextValue); + const target = currentOptions[targetIndex]; + const selectedValues = [...internalSelectedValues.slice(0, index), nextValue]; + + setInternalSelectedValues(selectedValues); + onPick?.({ + value: nextValue, + label: String(target?.label || ''), + index: targetIndex, + level: index, }); - return; - } - - if (target && Array.isArray(target.children) && target.children.length > 0) { - const children = target.children as TreeOptionData[]; - setOptionsList((prev) => [...prev.slice(0, index + 1), children]); - setStepIndex(index + 1); - return; - } - - setOptionsList((prev) => prev.slice(0, index + 1)); - setStepIndex(index); - onFinish(selectedValues); - }} - /> -
+ + if (target?.children === true && load) { + load({ data: target.data, value: target.value, label: target.label }) + .then((loadedChildren) => { + setOptionsList((prev) => [ + ...prev.slice(0, index + 1), + normalizeOptionsByKeys(loadedChildren), + ]); + setStepIndex(index + 1); + }) + .catch((error) => { + console.error('Load children failed:', error); + }); + return; + } + + if (target && Array.isArray(target.children) && target.children.length > 0) { + const children = target.children as TreeOptionData[]; + setOptionsList((prev) => [...prev.slice(0, index + 1), children]); + setStepIndex(index + 1); + return; + } + + setOptionsList((prev) => prev.slice(0, index + 1)); + setStepIndex(index); + onFinish(selectedValues); + }} + /> +
+
+ ))}
- ))} -
+ + ) : ( + <> + {filterResults.length > 0 ? ( +
+ {filterResults.map((result) => ( +
handleFilterResultTap(result)} + > + {result.fragments.map((frag) => ( + + {frag.text} + + ))} +
+ ))} +
+ ) : ( +
{globalConfig?.cascader?.empty}
+ )} + + )}
diff --git a/src/cascader/__tests__/cascader.test.tsx b/src/cascader/__tests__/cascader.test.tsx index 5d6467d65..7057779d0 100644 --- a/src/cascader/__tests__/cascader.test.tsx +++ b/src/cascader/__tests__/cascader.test.tsx @@ -409,6 +409,231 @@ describe('Cascader', () => { expect(onClose).toHaveBeenCalledWith('finish'); }); + + it(': filterable', async () => { + const filterableOptions = [ + { + label: '北京市', + value: '110000', + children: [ + { + label: '北京市', + value: '110100', + children: [ + { label: '海淀区', value: '110108' }, + { label: '朝阳区', value: '110105' }, + ], + }, + ], + }, + { + label: '上海市', + value: '310000', + children: [{ label: '上海市', value: '310100', children: [{ label: '浦东新区', value: '310115' }] }], + }, + { + label: '广东省', + value: '440000', + children: [{ label: '珠海市', value: '440400' }], + }, + ]; + + const renderCascader = (overrides = {}) => { + const props: CascaderProps = { + visible: true, + filterable: true, + options: filterableOptions, + ...overrides, + }; + return render(); + }; + + const typeInSearch = (value: string) => { + const searchInput = document.querySelector('.t-search input') as HTMLInputElement; + fireEvent.input(searchInput, { target: { value } }); + }; + + // renders search input + renderCascader(); + expect(document.querySelector('.t-search')).toBeTruthy(); + + // default filter matches full-path label + typeInSearch('海'); + await act(async () => { + vi.advanceTimersByTime(300); + }); + const results = document.querySelectorAll(`${name}__filter-result-item`); + expect(results.length).toBeGreaterThan(0); + + // shows empty state when no match + typeInSearch('xxxxx'); + await act(async () => { + vi.advanceTimersByTime(300); + }); + expect(document.querySelector(`${name}__filter-empty`)).toBeTruthy(); + }); + + it(': filterable - custom filter function', async () => { + const filterableOptions = [ + { + label: '北京市', + value: '110000', + children: [ + { + label: '北京市', + value: '110100', + children: [{ label: '海淀区', value: '110108' }], + }, + ], + }, + { + label: '上海市', + value: '310000', + children: [{ label: '上海市', value: '310100', children: [{ label: '浦东新区', value: '310115' }] }], + }, + ]; + + const customFilter = vi.fn((keyword: string, option: any) => option.label === keyword); + render(); + + const searchInput = document.querySelector('.t-search input') as HTMLInputElement; + fireEvent.input(searchInput, { target: { value: '浦东新区' } }); + await act(async () => { + vi.advanceTimersByTime(300); + }); + + expect(customFilter).toHaveBeenCalled(); + }); + + it(': filterable - selecting result closes cascader', async () => { + const filterableOptions = [ + { + label: '北京市', + value: '110000', + children: [ + { + label: '北京市', + value: '110100', + children: [{ label: '海淀区', value: '110108' }], + }, + ], + }, + ]; + + const onChange = vi.fn(); + const onClose = vi.fn(); + render(); + + const searchInput = document.querySelector('.t-search input') as HTMLInputElement; + fireEvent.input(searchInput, { target: { value: '海淀' } }); + await act(async () => { + vi.advanceTimersByTime(300); + }); + + const resultItem = document.querySelector(`${name}__filter-result-item`); + expect(resultItem).toBeTruthy(); + + fireEvent.click(resultItem); + expect(onChange).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalledWith('finish'); + }); + + it(': filterable - checkStrictly matches parent nodes', async () => { + const filterableOptions = [ + { + label: '广东省', + value: '440000', + children: [ + { + label: '深圳市', + value: '440300', + children: [{ label: '福田区', value: '440304' }], + }, + ], + }, + ]; + + const onChange = vi.fn(); + const onClose = vi.fn(); + const { rerender } = await render( + , + ); + + const typeInSearch = (value: string) => { + const searchInput = document.querySelector('.t-search input') as HTMLInputElement; + fireEvent.input(searchInput, { target: { value } }); + }; + + // checkStrictly 下父节点“深圳市”与叶子“福田区”路径均含“深圳”,均可命中 + typeInSearch('深圳'); + await act(async () => { + vi.advanceTimersByTime(300); + }); + const results = document.querySelectorAll(`${name}__filter-result-item`); + expect(results.length).toBe(2); + + // 点击中间父节点结果,selectedOptions 返回从根到该节点的完整路径(与非搜索态一致) + fireEvent.click(results[0]); + expect(onChange).toHaveBeenCalledWith('440300', [ + { label: '广东省', value: '440000' }, + { label: '深圳市', value: '440300' }, + ]); + expect(onClose).toHaveBeenCalledWith('finish'); + + // 非 checkStrictly 下仅叶子节点命中,父节点被排除 + rerender(); + typeInSearch('深圳'); + await act(async () => { + vi.advanceTimersByTime(300); + }); + const leafOnlyResults = document.querySelectorAll(`${name}__filter-result-item`); + expect(leafOnlyResults.length).toBe(1); + }); + + it(': filterable - selecting lazy node triggers load and exits search', async () => { + const option = { value: '440000', label: '广东省', children: true }; + const children = [ + { value: '440300', label: '深圳市', children: [] }, + { value: '440400', label: '珠海市', children: [] }, + ]; + const load = vi.fn().mockResolvedValue(children); + const onPick = vi.fn(); + const onChange = vi.fn(); + const onClose = vi.fn(); + + render( + , + ); + + const searchInput = document.querySelector('.t-search input') as HTMLInputElement; + fireEvent.input(searchInput, { target: { value: '广东' } }); + await act(async () => { + vi.advanceTimersByTime(300); + }); + + const resultItem = document.querySelector(`${name}__filter-result-item`); + expect(resultItem).toBeTruthy(); + + await act(async () => { + fireEvent.click(resultItem); + }); + + expect(onPick).toHaveBeenCalledWith({ value: '440000', label: '广东省', index: 0, level: 0 }); + expect(load).toHaveBeenCalledWith({ data: option, value: option.value, label: option.label }); + // 加载完成后退出搜索态,展示加载出的子级 + expect(document.querySelector(`${name}__filter-result`)).toBeNull(); + expect(document.querySelectorAll(`${name}__options`)).toHaveLength(2); + expect(onChange).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); + }); }); it(': lazy demo unlocks after loading and allows selection', async () => { diff --git a/src/cascader/_example/filterable.tsx b/src/cascader/_example/filterable.tsx new file mode 100644 index 000000000..235fbf187 --- /dev/null +++ b/src/cascader/_example/filterable.tsx @@ -0,0 +1,123 @@ +import React, { useState } from 'react'; + +import { Cascader, Cell } from 'tdesign-mobile-react'; + +const data = { + areaList: [ + { + label: '北京市', + value: '110000', + children: [ + { + value: '110100', + label: '北京市', + children: [ + { value: '110101', label: '东城区' }, + { value: '110102', label: '西城区' }, + { value: '110105', label: '朝阳区' }, + { value: '110106', label: '丰台区' }, + { value: '110107', label: '石景山区' }, + { value: '110108', label: '海淀区' }, + { value: '110109', label: '门头沟区' }, + { value: '110111', label: '房山区' }, + { value: '110112', label: '通州区' }, + { value: '110113', label: '顺义区' }, + { value: '110114', label: '昌平区' }, + { value: '110115', label: '大兴区' }, + { value: '110116', label: '怀柔区' }, + { value: '110117', label: '平谷区' }, + { value: '110118', label: '密云区' }, + { value: '110119', label: '延庆区' }, + ], + }, + ], + }, + { + label: '上海市', + value: '310000', + children: [ + { + value: '310100', + label: '上海市', + children: [ + { value: '310101', label: '黄浦区' }, + { value: '310104', label: '徐汇区' }, + { value: '310105', label: '长宁区' }, + { value: '310106', label: '静安区' }, + { value: '310107', label: '普陀区' }, + { value: '310109', label: '虹口区' }, + { value: '310110', label: '杨浦区' }, + { value: '310112', label: '闵行区' }, + { value: '310113', label: '宝山区' }, + { value: '310114', label: '嘉定区' }, + { value: '310115', label: '浦东新区' }, + ], + }, + ], + }, + { + label: '广东省', + value: '440000', + children: [ + { + value: '440100', + label: '广州市', + children: [ + { value: '440103', label: '荔湾区' }, + { value: '440104', label: '越秀区' }, + { value: '440105', label: '海珠区' }, + { value: '440106', label: '天河区' }, + { value: '440111', label: '白云区' }, + ], + }, + { + value: '440300', + label: '深圳市', + children: [ + { value: '440303', label: '罗湖区' }, + { value: '440304', label: '福田区' }, + { value: '440305', label: '南山区' }, + { value: '440306', label: '宝安区' }, + { value: '440307', label: '龙岗区' }, + ], + }, + ], + }, + ], +}; + +export default function FilterableDemo() { + const [visible, setVisible] = useState(false); + + const [note, setNote] = useState('请选择地址'); + + const [value, setValue] = useState(); + + return ( + <> + { + setVisible(true); + }} + /> + { + setNote((selectedOptions as any).map((item) => item.label).join('/') || ''); + setValue(value); + }} + onClose={() => { + setVisible(false); + }} + /> + + ); +} diff --git a/src/cascader/_example/index.tsx b/src/cascader/_example/index.tsx index e0630c4eb..9f90d6214 100644 --- a/src/cascader/_example/index.tsx +++ b/src/cascader/_example/index.tsx @@ -3,6 +3,7 @@ import TDemoBlock from '../../../site/mobile/components/DemoBlock'; import TDemoHeader from '../../../site/mobile/components/DemoHeader'; import BaseDemo from './base'; import CheckStrictlyDemo from './check-strictly'; +import FilterableDemo from './filterable'; import KeysDemo from './keys'; import LazyDemo from './lazy'; import ThemeTabDemo from './theme-tab'; @@ -36,6 +37,9 @@ export default function CascaderDemo() { + + +
); } diff --git a/src/cascader/cascader.md b/src/cascader/cascader.md index ce6d2b44e..c3c609c30 100644 --- a/src/cascader/cascader.md +++ b/src/cascader/cascader.md @@ -10,9 +10,9 @@ className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N checkStrictly | Boolean | false | 父子节点选中状态不再关联,可各自选中或取消 | N closeBtn | TNode | true | 关闭按钮。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N -filter | Function | - | 【开发中】自定义过滤函数。返回 true 表示匹配,未设置时使用内置匹配规则:对路径中所有 label 拼接后做大小写不敏感的 includes 匹配。TS 类型:`CascaderFilterFunction ` `type CascaderFilterFunction = (keyword: string, option: CascaderOption, path: CascaderOption[]) => boolean`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/cascader/type.ts) | N -filterPlaceholder | String | - | 【开发中】搜索框占位符描述文本 | N -filterable | Boolean | false | 【开发中】是否可搜索,开启后顶部会展示一个搜索框 | N +filter | Function | - | 自定义过滤函数。返回 true 表示匹配,未设置时使用内置匹配规则:对路径中所有 label 拼接后做大小写不敏感的 includes 匹配。TS 类型:`CascaderFilterFunction ` `type CascaderFilterFunction = (keyword: string, option: CascaderOption, path: CascaderOption[]) => boolean`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/cascader/type.ts) | N +filterPlaceholder | String | - | 搜索框占位符描述文本 | N +filterable | Boolean | false | 是否可搜索,开启后顶部会展示一个搜索框 | N header | TElement | - | `0.21.2`。头部。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N keys | Object | - | 用来定义 value / label / children / disabled 在 `options` 中对应的字段别名。TS 类型:`CascaderKeysType` `type CascaderKeysType = TreeKeysType`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/cascader/type.ts) | N load | Function | - | 加载子树数据的方法(仅当节点 children 为 true 时生效)。TS 类型:`(node: CascaderOption) => Promise>` | N diff --git a/src/checkbox/Checkbox.tsx b/src/checkbox/Checkbox.tsx index 5802e1b42..3642388ad 100644 --- a/src/checkbox/Checkbox.tsx +++ b/src/checkbox/Checkbox.tsx @@ -9,7 +9,7 @@ import { MinusIcon, MinusRectangleFilledIcon, } from 'tdesign-icons-react'; -import { TdCheckboxProps } from './type'; +import { TdCheckboxProps, TdCheckboxGroupProps } from './type'; import forwardRefWithStatics from '../_util/forwardRefWithStatics'; import CheckboxGroup from './CheckboxGroup'; import { StyledProps } from '../common'; @@ -22,17 +22,19 @@ import { checkboxDefaultProps } from './defaultProps'; export interface CheckboxProps extends TdCheckboxProps, StyledProps {} +export type CheckboxInjectedProps = CheckboxProps & { direction?: TdCheckboxGroupProps['direction'] }; + export interface CheckContextValue { - inject: (props: CheckboxProps) => CheckboxProps; + inject: (props: CheckboxProps) => CheckboxInjectedProps; } export const CheckContext = React.createContext(null); const Checkbox = forwardRef((_props, ref) => { const context = useContext(CheckContext); - const props = useDefaultProps(context ? context.inject(_props) : _props, checkboxDefaultProps); + const props = useDefaultProps(context ? context.inject(_props) : _props, checkboxDefaultProps); const { classPrefix } = useConfig(); - const classPrefixCheckBox = usePrefixClass('checkbox'); + const checkboxClass = usePrefixClass('checkbox'); const { className, placement, @@ -51,13 +53,15 @@ const Checkbox = forwardRef((_props, ref) => { checkAll, disabled, readonly, + direction, } = props; const [internalChecked, setInternalChecked] = useDefault(checked, defaultChecked, onChange); const checkboxClassName = classNames(`${classPrefix}-checkbox`, { - [`${classPrefixCheckBox}--${placement}`]: true, - [`${classPrefixCheckBox}--checked`]: checked, - [`${classPrefixCheckBox}--block`]: block, + [`${checkboxClass}--${placement}`]: true, + [`${checkboxClass}--checked`]: checked, + [`${checkboxClass}--block`]: block, + [`${checkboxClass}--${direction}`]: !!direction, }); const isChecked = useMemo(() => (checkAll ? checked : internalChecked), [internalChecked, checkAll, checked]); @@ -65,11 +69,11 @@ const Checkbox = forwardRef((_props, ref) => { const checkIcons = useMemo(() => { if (Array.isArray(icon) && icon.length > 1) { return icon.map((i) => - typeof i === 'string' ? : i, + typeof i === 'string' ? : i, ); } return [, ]; - }, [classPrefixCheckBox, icon]); + }, [checkboxClass, icon]); const checkIcon = useMemo(() => { if (icon === 'circle' || icon === true) { @@ -85,7 +89,7 @@ const Checkbox = forwardRef((_props, ref) => { }, [icon, indeterminate]); const renderIconArray = () => { - const className = `${classPrefixCheckBox}__icon-wrapper`; + const className = `${checkboxClass}__icon-wrapper`; if (Array.isArray(icon)) { return parseContentTNode(isChecked ? checkIcons[0] : checkIcons[1], { className, @@ -101,9 +105,9 @@ const Checkbox = forwardRef((_props, ref) => { {(icon === 'circle' || icon === true || icon === 'rectangle') && (
)} @@ -115,10 +119,10 @@ const Checkbox = forwardRef((_props, ref) => { const renderIconNode = () => (
{renderIconArray()} @@ -137,7 +141,7 @@ const Checkbox = forwardRef((_props, ref) => { const renderCheckBoxContent = () => (
{ event.stopPropagation(); @@ -146,9 +150,9 @@ const Checkbox = forwardRef((_props, ref) => { >
@@ -156,8 +160,8 @@ const Checkbox = forwardRef((_props, ref) => {
@@ -171,9 +175,7 @@ const Checkbox = forwardRef((_props, ref) => { {icon && renderIconNode()} {renderCheckBoxContent()} {/* 下边框 */} - {!borderless && ( -
- )} + {!borderless &&
}
); }); diff --git a/src/checkbox/CheckboxGroup.tsx b/src/checkbox/CheckboxGroup.tsx index 97c9830ac..9af48b0ce 100644 --- a/src/checkbox/CheckboxGroup.tsx +++ b/src/checkbox/CheckboxGroup.tsx @@ -43,6 +43,7 @@ const CheckboxGroup: FC = (props) => { children, borderless, readonly = false, + direction, keys, onChange, } = useDefaultProps(props, checkboxGroupDefaultProps); @@ -88,15 +89,15 @@ const CheckboxGroup: FC = (props) => { const context: CheckContextValue = { inject: (checkProps) => { - // 如果已经受控,则不注入 - if (typeof checkProps.checked !== 'undefined') { - return checkProps; + const injectProps = { ...checkProps, direction } as typeof checkProps; + if (typeof injectProps.checked !== 'undefined') { + return injectProps; } - const { value: checkValue } = checkProps; + const { value: checkValue } = injectProps; return { - ...checkProps, + ...injectProps, name, checked: checkProps.checkAll ? checkAllChecked || checkedSet.size !== 0 : checkedSet.has(checkValue), indeterminate: checkProps.checkAll ? indeterminate : checkProps.indeterminate, @@ -171,7 +172,7 @@ const CheckboxGroup: FC = (props) => { }); return ( -
+
{isOptions ? ( {checkboxNode()} diff --git a/src/checkbox/__tests__/checkbox.test.tsx b/src/checkbox/__tests__/checkbox.test.tsx index 33a6c7060..d5826592c 100644 --- a/src/checkbox/__tests__/checkbox.test.tsx +++ b/src/checkbox/__tests__/checkbox.test.tsx @@ -324,6 +324,39 @@ describe(': CheckboxGroup', () => { expect(container.querySelector(`${name}-group`)).toBeTruthy(); }); + it(': direction', () => { + // 默认 vertical,group 与子组件均带 vertical 修饰类 + const { container, rerender } = render(); + expect(container.querySelector(`${name}-group--vertical`)).toBeTruthy(); + expect(container.querySelector(`${name}-group--horizontal`)).toBeFalsy(); + expect(container.querySelectorAll(`${name}--vertical`)).toHaveLength(options.length); + expect(container.querySelectorAll(`${name}--horizontal`)).toHaveLength(0); + + // vertical -> horizontal + rerender(); + expect(container.querySelector(`${name}-group--horizontal`)).toBeTruthy(); + expect(container.querySelector(`${name}-group--vertical`)).toBeFalsy(); + expect(container.querySelectorAll(`${name}--horizontal`)).toHaveLength(options.length); + expect(container.querySelectorAll(`${name}--vertical`)).toHaveLength(0); + + // horizontal -> vertical + rerender(); + expect(container.querySelector(`${name}-group--vertical`)).toBeTruthy(); + expect(container.querySelector(`${name}-group--horizontal`)).toBeFalsy(); + expect(container.querySelectorAll(`${name}--vertical`)).toHaveLength(options.length); + expect(container.querySelectorAll(`${name}--horizontal`)).toHaveLength(0); + + // 受控子项(自身传入 checked)也应从 group 继承 direction 修饰类 + rerender( + + + + , + ); + expect(container.querySelectorAll(`${name}--horizontal`)).toHaveLength(2); + expect(container.querySelectorAll(`${name}--vertical`)).toHaveLength(0); + }); + it(': max', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); render(); diff --git a/src/checkbox/_example/horizontal.tsx b/src/checkbox/_example/horizontal.tsx index c3c7f04cc..6951499d7 100644 --- a/src/checkbox/_example/horizontal.tsx +++ b/src/checkbox/_example/horizontal.tsx @@ -12,11 +12,12 @@ export default function () { className="box horizontal" defaultValue={['checkbox1', 'checkbox2']} borderless + direction="horizontal" onChange={checkboxGroupChange} > - - - + + + ); } diff --git a/src/checkbox/_example/style/index.less b/src/checkbox/_example/style/index.less index 671ccb7ba..2b363b83b 100644 --- a/src/checkbox/_example/style/index.less +++ b/src/checkbox/_example/style/index.less @@ -12,8 +12,6 @@ .horizontal { padding: 16px; - display: flex; - justify-content: space-between; } .card { diff --git a/src/checkbox/checkbox.en-US.md b/src/checkbox/checkbox.en-US.md index 30723571a..5fd225f77 100644 --- a/src/checkbox/checkbox.en-US.md +++ b/src/checkbox/checkbox.en-US.md @@ -36,6 +36,7 @@ name | type | default | description | required className | String | - | className of component | N style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSProperties` | N borderless | Boolean | false | \- | N +direction | String | vertical | `1.0.0`。Radio option arrangement。options: vertical/horizontal | N disabled | Boolean | undefined | \- | N keys | Object | - | Typescript: `KeysType`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N max | Number | undefined | \- | N diff --git a/src/checkbox/checkbox.md b/src/checkbox/checkbox.md index 5d20d8edc..8f4c680bb 100644 --- a/src/checkbox/checkbox.md +++ b/src/checkbox/checkbox.md @@ -36,6 +36,7 @@ onChange | Function | | TS 类型:`(checked: boolean, context: { e: ChangeEve className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N borderless | Boolean | false | 是否开启无边框模式。优先级低于 Checkbox.borderless | N +direction | String | vertical | `1.0.0`。复选框按钮排列方式。可选项:vertical/horizontal | N disabled | Boolean | undefined | 是否禁用组件。优先级:Form.disabled < CheckboxGroup.disabled < Checkbox.disabled | N keys | Object | - | 用来定义 value / label / disabled 在 `options` 中对应的字段别名。TS 类型:`KeysType`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N max | Number | undefined | 支持最多选中的数量 | N diff --git a/src/checkbox/defaultProps.ts b/src/checkbox/defaultProps.ts index c13e93ea9..27b341ce0 100644 --- a/src/checkbox/defaultProps.ts +++ b/src/checkbox/defaultProps.ts @@ -20,6 +20,7 @@ export const checkboxDefaultProps: TdCheckboxProps = { export const checkboxGroupDefaultProps: TdCheckboxGroupProps = { borderless: false, + direction: 'vertical', disabled: undefined, max: undefined, readonly: undefined, diff --git a/src/checkbox/style/index.js b/src/checkbox/style/index.js index 3f5383520..2282d0245 100644 --- a/src/checkbox/style/index.js +++ b/src/checkbox/style/index.js @@ -1,3 +1,4 @@ import '../../_common/style/mobile/components/checkbox/_index.less'; +import '../../_common/style/mobile/components/checkbox-group/_index.less'; import './index.less'; diff --git a/src/checkbox/type.ts b/src/checkbox/type.ts index 5538528b4..68bca5f3f 100644 --- a/src/checkbox/type.ts +++ b/src/checkbox/type.ts @@ -102,6 +102,11 @@ export interface TdCheckboxGroupProps { * @default false */ borderless?: boolean; + /** + * 复选框按钮排列方式 + * @default vertical + */ + direction?: 'vertical' | 'horizontal'; /** * 是否禁用组件。优先级:Form.disabled < CheckboxGroup.disabled < Checkbox.disabled */ diff --git a/src/collapse/__tests__/collapse.test.tsx b/src/collapse/__tests__/collapse.test.tsx index 3affc7df6..b18a47ee9 100644 --- a/src/collapse/__tests__/collapse.test.tsx +++ b/src/collapse/__tests__/collapse.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { describe, expect, it, render, fireEvent, vi, beforeEach, afterEach, waitFor } from '@test/utils'; +import { describe, expect, it, render, fireEvent, vi, beforeEach, afterEach, waitFor, act } from '@test/utils'; import { Collapse, CollapsePanel } from 'tdesign-mobile-react'; describe('Collapse', () => { @@ -426,7 +426,9 @@ describe('Collapse', () => { fireEvent.click(panels[1]); // 此时应该有一次debounced调用 - vi.advanceTimersByTime(150); + act(() => { + vi.advanceTimersByTime(150); + }); // 验证最终状态 expect(onChange).toHaveBeenCalled(); @@ -452,7 +454,9 @@ describe('Collapse', () => { fireEvent.click(panels[0]); // 等待debounce - vi.advanceTimersByTime(150); + act(() => { + vi.advanceTimersByTime(150); + }); // 验证行为 expect(onChange).toHaveBeenCalled(); diff --git a/src/collapse/_example/defaultExpandAll.tsx b/src/collapse/_example/default-expand-all.tsx similarity index 100% rename from src/collapse/_example/defaultExpandAll.tsx rename to src/collapse/_example/default-expand-all.tsx diff --git a/src/color-picker/_example/index.tsx b/src/color-picker/_example/index.tsx index 288ee149f..f3c7f5058 100644 --- a/src/color-picker/_example/index.tsx +++ b/src/color-picker/_example/index.tsx @@ -3,7 +3,7 @@ import TDemoHeader from '../../../site/mobile/components/DemoHeader'; import TDemoBlock from '../../../site/mobile/components/DemoBlock'; import Base from './base'; import Multiple from './multiple'; -import UsePopup from './usePopup'; +import UsePopup from './use-popup'; import Format from './format'; import './style/index.less'; diff --git a/src/color-picker/_example/usePopup.tsx b/src/color-picker/_example/use-popup.tsx similarity index 100% rename from src/color-picker/_example/usePopup.tsx rename to src/color-picker/_example/use-popup.tsx diff --git a/src/comment/Comment.tsx b/src/comment/Comment.tsx new file mode 100644 index 000000000..f70d19175 --- /dev/null +++ b/src/comment/Comment.tsx @@ -0,0 +1,248 @@ +import React, { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import classNames from 'classnames'; +import { isObject } from 'lodash-es'; +import { ChevronDownSIcon, ChevronUpSIcon } from 'tdesign-icons-react'; +import { CommentActionItem, CommentFold, CommentFoldState, TdCommentProps } from './type'; +import { usePrefixClass } from '../hooks/useClass'; +import Avatar, { AvatarProps } from '../avatar'; +import parseTNode, { parseContentTNode } from '../_util/parseTNode'; +import { StyledProps, TNode } from '../common'; + +export interface CommentProps extends TdCommentProps, StyledProps {} + +const DEFAULT_COMMENT_FOLDS: CommentFold = { + state: 'collapsed', + total: 0, + step: 3, + content: { + collapsed: '展开回复', + partial: ['展开更多回复', '收起'], // [主文案, 副文案] + expanded: '收起', + }, +}; +const Comment: React.FC = (props) => { + const { + className, + style, + actions, + author, + avatar, + content, + datetime, + folds, + defaultFolds, + reply, + children, + onActions, + onFolds, + } = props; + const [innerFoldsState, setInnerFoldsState] = useState(defaultFolds?.state || 'collapsed'); + const foldSteps = useRef(0); + const replyBodyRef = useRef(null); + const replyInnerRef = useRef(null); + const rootClassName = usePrefixClass('comment'); + + const isControlled = folds !== undefined; + const commentFolds = folds || defaultFolds || DEFAULT_COMMENT_FOLDS; + + // 受控模式下,当外部 folds.state 变化时同步 foldSteps + useEffect(() => { + if (!isControlled) { + return; + } + if (folds.state === 'collapsed') { + foldSteps.current = 0; + } else if (folds.state === 'expanded') { + foldSteps.current = (commentFolds.step || 3) - 1; + } + }, [isControlled, folds?.state, commentFolds.step]); + + // 当前实际使用的折叠状态 + const curFoldsState: CommentFoldState = isControlled ? folds.state || 'collapsed' : innerFoldsState; + + // 首次渲染前同步设置高度,避免闪烁(无动画) + useLayoutEffect(() => { + const body = replyBodyRef.current; + const inner = replyInnerRef.current; + if (body && inner) { + body.style.height = `${inner.scrollHeight}px`; + } + }, []); + + // 监听回复区内容高度变化,驱动过渡动画 + useEffect(() => { + const body = replyBodyRef.current; + const inner = replyInnerRef.current; + if (!body || !inner) return; + + const observer = new ResizeObserver(() => { + body.style.height = `${inner.scrollHeight}px`; + }); + + observer.observe(inner); + return () => observer.disconnect(); + }, []); + + const handleClickActions = (event: MouseEvent, action: CommentActionItem | TNode) => { + onActions?.({ action, e: event }); + }; + + const handleClickFold = (event: MouseEvent, isExpand: boolean) => { + if (!isExpand) { + // 收起:重置为 collapsed + foldSteps.current = 0; + if (!isControlled) { + setInnerFoldsState('collapsed'); + } + onFolds?.({ + fold: { ...commentFolds, state: 'collapsed' }, + e: event, + }); + return; + } + + // 展开:递增步进并计算下一状态 + foldSteps.current += 1; + const nextState: CommentFoldState = foldSteps.current >= (commentFolds.step || 3) - 1 ? 'expanded' : 'partial'; + + if (!isControlled) { + setInnerFoldsState(nextState); + } + onFolds?.({ + fold: { ...commentFolds, state: nextState }, + e: event, + }); + }; + + const renderAvatar = () => { + if (isObject(avatar) && !React.isValidElement(avatar) && typeof avatar !== 'function') { + return ; + } + return parseTNode(avatar); + }; + + const renderAuthor = () =>
{parseTNode(author)}
; + const renderContent = () =>
{parseTNode(content)}
; + const renderFooter = () => { + const renderActions = () => { + if (!actions) { + return null; + } + if (!Array.isArray(actions)) { + return ( +
handleClickActions(e as unknown as MouseEvent, actions)}>{parseTNode(actions)}
+ ); + } + const renderLeftPlacement = () => + actions + .filter((item) => item.placement === 'start') + .map((item) => ( +
handleClickActions(e as unknown as MouseEvent, item)}> + {parseContentTNode(item.content, { disabled: item.disabled })} +
+ )); + const renderRightPlacement = () => + actions + .filter((item) => item.placement === 'end') + .map((item) => ( +
handleClickActions(e as unknown as MouseEvent, item)}> + {parseContentTNode(item.content, { disabled: item.disabled })} +
+ )); + return ( + <> +
{renderLeftPlacement()}
+
{renderRightPlacement()}
+ + ); + }; + return ( +
+
{parseTNode(datetime)}
+
{renderActions()}
+
+ ); + }; + + const renderFolds = () => { + const renderFoldsContent = () => { + if (curFoldsState === 'collapsed') { + // 全收起 + return ( +
handleClickFold(e as unknown as MouseEvent, true)} + > + {commentFolds.content.collapsed as ReactNode} + +
+ ); + } + if (curFoldsState === 'partial') { + // 半展开 + return ( + <> +
handleClickFold(e as unknown as MouseEvent, true)} + > + {commentFolds.content.partial[0] as ReactNode} + +
+
handleClickFold(e as unknown as MouseEvent, false)} + > + {commentFolds.content.partial[1] as ReactNode} + +
+ + ); + } + // 全展开 + return ( +
handleClickFold(e as unknown as MouseEvent, false)} + > + {commentFolds.content.expanded as ReactNode} + +
+ ); + }; + if (!commentFolds.total || commentFolds.total <= 1) { + return null; + } + return
{renderFoldsContent()}
; + }; + + const renderReply = () => { + const replyContent = reply || children; + return ( +
+
+
+ {parseTNode(replyContent)} +
+
+ {renderFolds()} +
+ ); + }; + + return ( +
+
+ {renderAvatar()} +
+ {renderAuthor()} + {renderContent()} + {renderFooter()} +
+
+ {renderReply()} +
+ ); +}; + +export default Comment; diff --git a/src/comment/_example/base.tsx b/src/comment/_example/base.tsx new file mode 100644 index 000000000..47a2188ec --- /dev/null +++ b/src/comment/_example/base.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Button, Comment, Tag } from 'tdesign-mobile-react'; +import { ThumbUpIcon, Uncomfortable1Icon } from 'tdesign-icons-react'; + +export default function () { + return ( +
+ +
Name1 名称
+ + 作者 + +
+ } + actions={[ + { placement: 'start', content:
回复
, key: 'reply' }, + { + placement: 'end', + content: ( +
+ +
+ ), + key: 'actions', + }, + ]} + content="这是一段很长很长很长很长很长很长的评论内容。" + datetime="今天16:38·广东" + /> +
+ ); +} diff --git a/src/comment/_example/collapse.tsx b/src/comment/_example/collapse.tsx new file mode 100644 index 000000000..89dd9958a --- /dev/null +++ b/src/comment/_example/collapse.tsx @@ -0,0 +1,283 @@ +import React, { useEffect, useRef } from 'react'; +import { ActionSheet, Button, Comment, CommentFold, Tag } from 'tdesign-mobile-react'; +import { SendIcon, ThumbUpIcon, Uncomfortable1Icon } from 'tdesign-icons-react'; + +const REPLY_COUNT = 6; // 回复评论总数 +const MAIN_COMMENT_AUTHOR = 'Name1 名称'; +const SHOW_REPLY_INPUT = false; // 回复输入框待对齐视觉,暂不展示;对齐后改为 true 即可恢复 + +interface ReplyItem { + author: string; + avatar: string; + content: string; + datetime: string; + likeNum: number; + replyTo?: string; // 回复对象的名称 +} + +export default function () { + const [commentFold, setCommentFold] = React.useState({ + state: 'collapsed', + total: REPLY_COUNT, + step: 3, + content: { + collapsed: `展开${REPLY_COUNT - 1}条回复`, + partial: ['展开更多回复', '收起'], + expanded: '收起', + }, + }); + const [replyInfo, setReplyInfo] = React.useState([]); + const [showActionSheet, setShowActionSheet] = React.useState(false); + const [replyValue, setReplyValue] = React.useState(''); + const [replyTarget, setReplyTarget] = React.useState(null); // 当前回复的目标用户 + const timerRef = React.useRef | null>(null); + const inputRef = useRef(null); + const longPressTargetRef = useRef(null); // 长按时记录目标评论的作者 + const lastReplyRef = useRef(null); // 最后一条回复的 ref,用于滚动定位 + + // 触发回复某人 + const handleReply = (authorName: string) => { + setReplyTarget(authorName); + // 延迟聚焦,确保 state 更新后 input 已渲染 + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + }; + + const handleTouchStart = (e: React.TouchEvent, author: string) => { + e.stopPropagation(); + longPressTargetRef.current = author; + timerRef.current = setTimeout(() => { + setShowActionSheet(true); + }, 800); + }; + + const handleTouchEnd = () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + }; + + useEffect(() => { + setReplyInfo( + new Array(REPLY_COUNT).fill({}).map((__, index) => ({ + author: `Name${index + 1} 名称`, + avatar: 'https://tdesign.gtimg.com/mobile/demos/avatar1.png', + content: '这是一段很长很长很长很长很长很长的评论内容。', + datetime: '今天16:38·广东', + likeNum: 6, + })), + ); + }, []); + + const handleUpdateFold = (context: { e: Event; fold: CommentFold }) => { + setCommentFold(context.fold); + }; + + const handleSubmit = () => { + if (!replyValue.trim()) return; + + const now = new Date(); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + + const newReply: ReplyItem = { + author: '我', + avatar: 'https://tdesign.gtimg.com/mobile/demos/avatar2.png', + content: replyValue, + datetime: `今天${hours}:${minutes}·广东`, + likeNum: 0, + ...(replyTarget ? { replyTo: replyTarget } : {}), + }; + + setReplyInfo((prev) => [...prev, newReply]); + + // 更新折叠状态:展开全部并更新总数 + setCommentFold((prev) => { + const newTotal = replyInfo.length + 1; + return { + ...prev, + state: 'expanded', + total: newTotal, + content: { + collapsed: `展开${newTotal - 1}条回复`, + partial: ['展开更多回复', '收起'], + expanded: '收起', + }, + }; + }); + + setTimeout(() => { + lastReplyRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 500); + + setReplyValue(''); + setReplyTarget(null); + }; + + const renderReply = () => { + const getRenderReplyItem = () => { + if (commentFold.state === 'collapsed') { + return replyInfo.slice(0, 1); + } + if (commentFold.state === 'partial') { + return replyInfo.slice(0, 3); + } + return replyInfo; + }; + const visibleItems = getRenderReplyItem(); + return visibleItems.map((item, index) => ( +
handleTouchStart(e, item.author)} + onTouchEnd={handleTouchEnd} + onTouchMove={handleTouchEnd} + > + +
{item.author}
+
+ } + actions={[ + { + placement: 'start', + content: ( +
handleReply(item.author)}> + 回复 +
+ ), + key: 'reply', + }, + { + placement: 'end', + content: ( +
+ +
+ ), + key: 'actions', + }, + ]} + content={ + item.replyTo ? ( + + 回复{item.replyTo}:{item.content} + + ) : ( + item.content + ) + } + datetime={item.datetime} + /> +
+ )); + }; + return ( +
+
handleTouchStart(e, MAIN_COMMENT_AUTHOR)} + onTouchEnd={handleTouchEnd} + onTouchMove={handleTouchEnd} + > + +
{MAIN_COMMENT_AUTHOR}
+ + 作者 + +
+ } + actions={[ + { + placement: 'start', + content: ( +
handleReply(MAIN_COMMENT_AUTHOR)}> + 回复 +
+ ), + key: 'reply', + }, + { + placement: 'end', + content: ( +
+ +
+ ), + key: 'actions', + }, + ]} + content="这是一段很长很长很长很长很长很长的评论内容。" + datetime="今天16:38·广东" + folds={commentFold} + reply={renderReply()} + onFolds={handleUpdateFold} + /> +
+ {/* 这是一段有效的示例,待对齐视觉后把 SHOW_REPLY_INPUT 改为 true 即可展示,请勿删除 */} + {SHOW_REPLY_INPUT && ( +
+ setReplyValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSubmit(); + } + }} + /> +
+ )} + { + const label = typeof selected === 'string' ? selected : selected.label; + if (label === '回复' && longPressTargetRef.current) { + handleReply(longPressTargetRef.current); + } + setShowActionSheet(false); + }} + onClose={() => { + setShowActionSheet(false); + }} + onCancel={() => { + setShowActionSheet(false); + }} + /> +
+ ); +} diff --git a/src/comment/_example/image.tsx b/src/comment/_example/image.tsx new file mode 100644 index 000000000..7c5f47900 --- /dev/null +++ b/src/comment/_example/image.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +import { Button, Comment, Image, ImageViewer, Tag } from 'tdesign-mobile-react'; +import { ThumbUpIcon, Uncomfortable1Icon } from 'tdesign-icons-react'; + +const images = ['https://tdesign.gtimg.com/mobile/demos/swiper1.png']; + +export default function ImageDemo() { + const [visible, setVisible] = useState(false); + const [viewerIndex, setViewerIndex] = useState(0); + + const handleImageClick = (index: number) => { + setViewerIndex(index); + setVisible(true); + }; + + const handleClose = () => { + setVisible(false); + }; + + return ( +
+ +
Name1 名称
+ + 作者 + +
+ } + content={ +
+ {images.map((src, index) => ( +
handleImageClick(index)}> + +
+ ))} +
+ } + actions={[ + { placement: 'start', content:
回复
, key: 'reply' }, + { + placement: 'end', + content: ( +
+ +
+ ), + key: 'actions', + }, + ]} + datetime="今天16:38·广东" + /> + setViewerIndex(index)} + /> +
+ ); +} diff --git a/src/comment/_example/index.tsx b/src/comment/_example/index.tsx new file mode 100644 index 000000000..2aaa7c2e4 --- /dev/null +++ b/src/comment/_example/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import TDemoHeader from '../../../site/mobile/components/DemoHeader'; +import TDemoBlock from '../../../site/mobile/components/DemoBlock'; +import Base from './base'; +import Image from './image'; +import Mixed from './mixed'; +import Collapse from './collapse'; +import './style/index.less'; + +export default function CommentDemo() { + return ( +
+ + + + + + + + + + + + + +
+ ); +} diff --git a/src/comment/_example/mixed.tsx b/src/comment/_example/mixed.tsx new file mode 100644 index 000000000..6e18c1778 --- /dev/null +++ b/src/comment/_example/mixed.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { Button, Comment, Image, ImageViewer, Tag } from 'tdesign-mobile-react'; +import { ThumbUpIcon, Uncomfortable1Icon } from 'tdesign-icons-react'; + +const images = [ + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', + 'https://tdesign.gtimg.com/mobile/demos/swiper1.png', +]; + +const MAX_SHOW = 3; + +export default function ImageDemo() { + const [visible, setVisible] = useState(false); + const [viewerIndex, setViewerIndex] = useState(0); + + const showImages = images.slice(0, MAX_SHOW); + + const handleImageClick = (index: number) => { + setViewerIndex(index); + setVisible(true); + }; + + const handleClose = () => { + setVisible(false); + }; + + return ( +
+ +
Name1 名称
+ + 作者 + +
+ } + content={ + <> +
这是一段很长很长很长很长很长很长的评论内容。
+
+ {showImages.map((src, index) => ( +
handleImageClick(index)}> + + {index === MAX_SHOW - 1 && images.length > MAX_SHOW && ( +
共{images.length}张
+ )} +
+ ))} +
+ + } + actions={[ + { placement: 'start', content:
回复
, key: 'reply' }, + { + placement: 'end', + content: ( +
+ +
+ ), + key: 'actions', + }, + ]} + datetime="今天16:38·广东" + /> + setViewerIndex(index)} + /> +
+ ); +} diff --git a/src/comment/_example/style/index.less b/src/comment/_example/style/index.less new file mode 100644 index 000000000..c26a6815f --- /dev/null +++ b/src/comment/_example/style/index.less @@ -0,0 +1,103 @@ +.tdesign-mobile-demo { + --bg-color-demo: var(--td-bg-color-container); + // 禁用按钮点击态样式 + --td-button-default-text-active-bg-color: transparent; + --td-button-default-active-border-color: transparent; +} + +.mobile-comment { + box-sizing: border-box; + + &__author { + display: flex; + align-items: center; + font: var(--td-font-body-small); + + &-tag { + border: none; + --td-tag-medium-padding: 0 var(--td-spacer); + --td-tag-primary-light-color: transparent; + } + } + + &__reply { + &-button { + color: var(--td-text-color-secondary); + font: var(--td-font-body-small); + } + + &-name { + color: var(--td-text-color-secondary); + font: var(--td-font-body-medium); + } + } + + &__actions { + display: flex; + align-items: center; + gap: var(--td-spacer-2); + + &-button { + padding: 2px calc(var(--td-spacer) / 4); + color: var(--td-text-color-placeholder); + font: var(--td-font-body-small); + box-sizing: border-box; + height: var(--td-spacer-3); + } + } + + &__text { + margin-bottom: calc(var(--td-spacer) / 2); + } + + &__images { + display: flex; + gap: var(--td-spacer); + + &-item { + position: relative; + width: 78px; + height: 78px; + border-radius: 8px; + overflow: hidden; + flex-shrink: 0; + cursor: pointer; + } + + &-badge { + position: absolute; + right: 4px; + bottom: 4px; + padding: 0 var(--td-spacer); + background: var(--td-mask-active); + color: var(--td-text-color-anti); + font: var(--td-font-body-extraSmall); + border-radius: 12px; + white-space: nowrap; + } + } + + &__input { + display: flex; + padding: 12px; + gap: var(--td-spacer); + border-radius: 16px; + border: 1px solid #DCDCDC; + background: #FFFFFF; + /* Shadow/Shadow-3 上层投影 */ + box-shadow: 0 6px 30px 5px rgba(0, 0, 0, 0.05), 0 16px 24px 2px rgba(0, 0, 0, 0.04), 0 8px 10px -5px rgba(0, 0, 0, 0.08); + margin-top: 48px; + + &-content { + border: none; + flex: 1; + background: transparent; + } + + &-button { + --td-button-primary-disabled-bg: #DCDCDC; + --td-button-primary-disabled-border-color: transparent; + transform: rotate(-90deg); + } + } +} diff --git a/src/comment/comment.en-US.md b/src/comment/comment.en-US.md new file mode 100644 index 000000000..5e299f6e9 --- /dev/null +++ b/src/comment/comment.en-US.md @@ -0,0 +1,20 @@ +:: BASE_DOC :: + +## API + +### Comment Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | className of component | N +style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSProperties` | N +actions | TNode | - | Typescript: `Array \| TNode ` `type ActionPlacement = 'start' \| 'end'` `interface CommentActionItem { key: string; content?: string \| TNode; placement?: ActionPlacement; disabled?: boolean}`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/comment/type.ts) | N +author | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +avatar | TNode | - | Typescript: `string \| AvatarProps \| TNode`,[Avatar API Documents](./avatar?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/comment/type.ts) | N +children | TNode | - | children, same as `reply`。Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +content | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +datetime | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +folds | Object | - | Typescript: `CommentFold ` `type CommentFoldState = 'collapsed' \| 'partial' \| 'expanded'` `interface CommentFold { state: CommentFoldState; total?: number; step?: number; content?: Partial>}`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/comment/type.ts) | N +reply | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +onActions | Function | | Typescript: `(context: { action: CommentActionItem \| TNode; e: Event }) => void`
[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +onFolds | Function | | Typescript: `(context: { fold: CommentFold; e: Event }) => void`
| N diff --git a/src/comment/comment.md b/src/comment/comment.md new file mode 100644 index 000000000..663e0e164 --- /dev/null +++ b/src/comment/comment.md @@ -0,0 +1,20 @@ +:: BASE_DOC :: + +## API + +### Comment Props + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +actions | TNode | - | 操作。TS 类型:`Array \| TNode ` `type ActionPlacement = 'start' \| 'end'` `interface CommentActionItem { key: string; content?: string \| TNode; placement?: ActionPlacement; disabled?: boolean}`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/comment/type.ts) | N +author | TNode | - | 作者。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +avatar | TNode | - | 头像。TS 类型:`string \| AvatarProps \| TNode`,[Avatar API Documents](./avatar?tab=api)。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/comment/type.ts) | N +children | TNode | - | 子元素内容,同 reply。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +content | TNode | - | 内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +datetime | TNode | - | 时间。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +folds | Object | - | 折叠/展开配置项。TS 类型:`CommentFold ` `type CommentFoldState = 'collapsed' \| 'partial' \| 'expanded'` `interface CommentFold { state: CommentFoldState; total?: number; step?: number; content?: Partial>}`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/comment/type.ts) | N +reply | TNode | - | 回复。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +onActions | Function | | TS 类型:`(context: { action: CommentActionItem \| TNode; e: Event }) => void`
点击任一操作项(如回复/点赞/不喜欢)时触发并返回当前操作项。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +onFolds | Function | | TS 类型:`(context: { fold: CommentFold; e: Event }) => void`
用户点击“展开回复/收起”时触发,并返回点击后的 fold 配置 | N diff --git a/src/comment/index.tsx b/src/comment/index.tsx new file mode 100644 index 000000000..a7914d9ba --- /dev/null +++ b/src/comment/index.tsx @@ -0,0 +1,8 @@ +import _Comment from './Comment'; +import './style'; + +export type { CommentProps } from './Comment'; +export * from './type'; + +export const Comment = _Comment; +export default Comment; diff --git a/src/comment/style/css.js b/src/comment/style/css.js new file mode 100644 index 000000000..6a9a4b132 --- /dev/null +++ b/src/comment/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/comment/style/index.js b/src/comment/style/index.js new file mode 100644 index 000000000..34fb7863e --- /dev/null +++ b/src/comment/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/mobile/components/comment/_index.less'; diff --git a/src/comment/type.ts b/src/comment/type.ts new file mode 100644 index 000000000..409c9bba1 --- /dev/null +++ b/src/comment/type.ts @@ -0,0 +1,73 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { AvatarProps } from '../avatar'; +import type { TNode } from '../common'; + +export interface TdCommentProps { + /** + * 操作 + */ + actions?: Array | TNode; + /** + * 作者 + */ + author?: TNode; + /** + * 头像 + */ + avatar?: AvatarProps | TNode; + /** + * 子元素内容,同 reply + */ + children?: TNode; + /** + * 内容 + */ + content?: TNode; + /** + * 时间 + */ + datetime?: TNode; + /** + * 折叠/展开配置项 + */ + folds?: CommentFold; + /** + * 折叠/展开配置项,非受控属性 + */ + defaultFolds?: CommentFold; + /** + * 回复 + */ + reply?: TNode; + /** + * 点击任一操作项(如回复/点赞/不喜欢)时触发并返回当前操作项 + */ + onActions?: (context: { action: CommentActionItem | TNode; e: Event }) => void; + /** + * 用户点击“展开回复/收起”时触发,并返回点击后的 fold 配置 + */ + onFolds?: (context: { fold: CommentFold; e: Event }) => void; +} + +export type ActionPlacement = 'start' | 'end'; + +export interface CommentActionItem { + key: string; + content?: string | TNode; + placement?: ActionPlacement; + disabled?: boolean; +} + +export type CommentFoldState = 'collapsed' | 'partial' | 'expanded'; + +export interface CommentFold { + state: CommentFoldState; + total?: number; + step?: number; + content?: Partial>; +} diff --git a/src/config-provider/config-provider.en-US.md b/src/config-provider/config-provider.en-US.md index a707b8e79..15842d946 100644 --- a/src/config-provider/config-provider.en-US.md +++ b/src/config-provider/config-provider.en-US.md @@ -50,6 +50,8 @@ weekdays | Array | - | Typescript: `string[]` | N name | type | default | description | required -- | -- | -- | -- | -- +empty | String | - | \- | N +filterPlaceholder | String | - | \- | N placeholder | String | - | \- | N title | String | - | \- | N diff --git a/src/config-provider/config-provider.md b/src/config-provider/config-provider.md index aacb5a48f..ba7959640 100644 --- a/src/config-provider/config-provider.md +++ b/src/config-provider/config-provider.md @@ -50,6 +50,8 @@ weekdays | Array | - | 星期文本描述,默认值:['日', '一', '二', ' 名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- +empty | String | - | 语言配置,“暂无数据”描述文本 | N +filterPlaceholder | String | - | 语言配置,“搜索框”占位描述文本 | N placeholder | String | - | 语言配置,未选中时的提示文案“选择选项”描述文本 | N title | String | - | 语言配置,组件标题“选择地址”描述文本 | N diff --git a/src/config-provider/type.ts b/src/config-provider/type.ts index f7f72e787..0bf632b1d 100644 --- a/src/config-provider/type.ts +++ b/src/config-provider/type.ts @@ -122,6 +122,16 @@ export interface CalendarConfig { } export interface CascaderConfig { + /** + * 语言配置,“暂无数据”描述文本 + * @default '' + */ + empty?: string; + /** + * 语言配置,“搜索框”占位描述文本 + * @default '' + */ + filterPlaceholder?: string; /** * 语言配置,未选中时的提示文案“选择选项”描述文本 * @default '' diff --git a/src/date-time-picker/_example/index.tsx b/src/date-time-picker/_example/index.tsx index 2a862c8b3..c03e374d0 100644 --- a/src/date-time-picker/_example/index.tsx +++ b/src/date-time-picker/_example/index.tsx @@ -10,6 +10,7 @@ import DateSecondDemo from './date-all'; import WithTitleDemo from './with-title'; import WithoutTitleDemo from './without-title'; import ShowWeekDemo from './show-week'; +import StepsDemo from './steps'; export default function DateTimePickerDemo() { return ( @@ -33,6 +34,9 @@ export default function DateTimePickerDemo() { + + + diff --git a/src/date-time-picker/_example/steps.tsx b/src/date-time-picker/_example/steps.tsx new file mode 100644 index 000000000..c1fafbb90 --- /dev/null +++ b/src/date-time-picker/_example/steps.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { DateTimePicker, Cell, Popup } from 'tdesign-mobile-react'; + +export default function DateTimePickerDemo() { + const [visible, setVisible] = useState(false); + const [pickerValue, setPickerValue] = useState(''); + const [pickerValueText, setPickerValueText] = useState(''); + + const onPick = (value: string) => { + console.log('[onPick]', value); + }; + const onChange = (value: string) => { + console.log('[onChange]', value); + }; + + const onCancel = () => { + console.log('[onCancel]'); + setVisible(false); + }; + + const onConfirm = (value: string) => { + console.log('[onConfirm]', value); + setPickerValueText(value); + setPickerValue(value); + setVisible(false); + }; + + return ( +
+ setVisible(true)} + /> + + + +
+ ); +} diff --git a/src/dialog/__tests__/dialog.test.tsx b/src/dialog/__tests__/dialog.test.tsx index 1d6d5225f..509ad7aa0 100644 --- a/src/dialog/__tests__/dialog.test.tsx +++ b/src/dialog/__tests__/dialog.test.tsx @@ -201,12 +201,14 @@ describe('Dialog', () => { onClosed, }); }); - instance.hide(); - - setTimeout(() => { - expect(document.querySelector(name)).toBeFalsy(); - expect(onClosed).toHaveBeenCalled(); - }, 1000); + await act(async () => { + instance.hide(); + }); + await act(async () => { + vi.advanceTimersByTime(1000); + }); + expect(document.querySelector(name)).toBeFalsy(); + expect(onClosed).toHaveBeenCalled(); }); it(': update', async () => { @@ -218,10 +220,10 @@ describe('Dialog', () => { onCancel: vi.fn(), }); }); - instance.update({ title: '新标题' }); - setInterval(() => { - expect(document.querySelector(`${name}__header`).textContent).toBe('新标题'); - }, 1000); + await act(async () => { + instance.update({ title: '新标题' }); + }); + expect(document.querySelector(`${name}__header`).textContent).toBe('新标题'); }); it(': destroy', async () => { diff --git a/src/dialog/_example/index.tsx b/src/dialog/_example/index.tsx index 711c4c2e9..9b14b0e7b 100644 --- a/src/dialog/_example/index.tsx +++ b/src/dialog/_example/index.tsx @@ -27,7 +27,7 @@ export default function Base() { - + {/* diff --git a/src/dialog/dialog.md b/src/dialog/dialog.md index 6aef30a0e..0671cd1ea 100644 --- a/src/dialog/dialog.md +++ b/src/dialog/dialog.md @@ -24,7 +24,7 @@ title | TNode | - | 标题。TS 类型:`string \| TNode`。[通用类型定义 top | TElement | - | 顶部自定义内容。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N visible | Boolean | - | 控制对话框是否显示 | N width | String / Number | - | 对话框宽度,示例:320, '500px', '80%' | N -zIndex | Number | - | 对话框层级,Web 侧样式默认为 2500,移动端和小程序样式默认为 1500 | N +zIndex | Number | - | 对话框层级,Web 侧样式默认为 2500,移动端样式默认 1500,小程序样式默认为 11500 | N onCancel | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
如果“取消”按钮存在,则点击“取消”按钮时触发,同时触发关闭事件 | N onClose | Function | | TS 类型:`(context: DialogCloseContext) => void`
关闭事件,点击 取消按钮 或 点击蒙层 时触发。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/dialog/type.ts)。
`type DialogEventSource = 'cancel' \| 'overlay' \| 'close-btn'`

`interface DialogCloseContext { trigger: DialogEventSource; e: MouseEvent }`
| N onClosed | Function | | TS 类型:`() => void`
对话框消失动画效果结束后触发 | N diff --git a/src/dialog/type.ts b/src/dialog/type.ts index 669fd9a93..1be2b91a9 100644 --- a/src/dialog/type.ts +++ b/src/dialog/type.ts @@ -82,7 +82,7 @@ export interface TdDialogProps { */ width?: string | number; /** - * 对话框层级,Web 侧样式默认为 2500,移动端和小程序样式默认为 1500 + * 对话框层级,Web 侧样式默认为 2500,移动端样式默认 1500,小程序样式默认为 11500 */ zIndex?: number; /** diff --git a/src/dropdown-menu/defaultProps.ts b/src/dropdown-menu/defaultProps.ts index 9a38702f7..d3bfc03f8 100644 --- a/src/dropdown-menu/defaultProps.ts +++ b/src/dropdown-menu/defaultProps.ts @@ -9,7 +9,6 @@ export const dropdownMenuDefaultProps: TdDropdownMenuProps = { direction: 'down', duration: 200, showOverlay: true, - zIndex: 11600, }; export const dropdownItemDefaultProps: TdDropdownItemProps = { diff --git a/src/dropdown-menu/dropdown-menu.en-US.md b/src/dropdown-menu/dropdown-menu.en-US.md index 92ec3c20f..e8ad2bbf8 100644 --- a/src/dropdown-menu/dropdown-menu.en-US.md +++ b/src/dropdown-menu/dropdown-menu.en-US.md @@ -12,7 +12,7 @@ closeOnClickOverlay | Boolean | true | \- | N direction | String | down | options: down/up | N duration | String / Number | 200 | \- | N showOverlay | Boolean | true | \- | N -zIndex | Number | 11600 | \- | N +zIndex | Number | - | \- | N ### DropdownItem Props @@ -27,7 +27,7 @@ icon | TNode | undefined | Typescript: `TNode \| TNode[] \| undefined`。[see mo keys | Object | - | Typescript: `KeysType`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N label | String | - | \- | N multiple | Boolean | false | \- | N -options | Array | [] | Typescript: `Array` `interface DropdownOption { label: string; disabled: boolean; value: DropdownValue; }`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/dropdown-menu/type.ts) | N +options | Array | [] | Typescript: `Array` `interface DropdownOption { label: string; disabled?: boolean; value: DropdownValue; }`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/dropdown-menu/type.ts) | N optionsColumns | String / Number | 1 | \- | N placement | String | left | options: left/right | N value | String / Number / Array | undefined | Typescript: `DropdownValue ` `type DropdownValue = string \| number \| Array;`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/dropdown-menu/type.ts) | N diff --git a/src/dropdown-menu/dropdown-menu.md b/src/dropdown-menu/dropdown-menu.md index 9c15e15aa..2b8f4035c 100644 --- a/src/dropdown-menu/dropdown-menu.md +++ b/src/dropdown-menu/dropdown-menu.md @@ -12,7 +12,7 @@ closeOnClickOverlay | Boolean | true | 是否在点击遮罩层后关闭菜单 | direction | String | down | 菜单展开方向。可选项:down/up | N duration | String / Number | 200 | 动画时长 | N showOverlay | Boolean | true | 是否显示遮罩层 | N -zIndex | Number | 11600 | 菜单栏 z-index 层级 | N +zIndex | Number | - | 菜单栏 z-index 层级,默认为 1600 | N ### DropdownItem Props @@ -27,7 +27,7 @@ icon | TNode | undefined | 自定义菜单子项图标,值为 `undefined` 表 keys | Object | - | 用来定义 value / label / disabled 在 `options` 中对应的字段别名。TS 类型:`KeysType`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N label | String | - | 标题 | N multiple | Boolean | false | 是否多选 | N -options | Array | [] | 选项数据。TS 类型:`Array` `interface DropdownOption { label: string; disabled: boolean; value: DropdownValue; }`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/dropdown-menu/type.ts) | N +options | Array | [] | 选项数据。TS 类型:`Array` `interface DropdownOption { label: string; disabled?: boolean; value: DropdownValue; }`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/dropdown-menu/type.ts) | N optionsColumns | String / Number | 1 | 选项分栏(1-3) | N placement | String | left | 复选框和内容相对位置,仅单选菜单栏有效。可选项:left/right | N value | String / Number / Array | undefined | 选中值。TS 类型:`DropdownValue ` `type DropdownValue = string \| number \| Array;`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/dropdown-menu/type.ts) | N diff --git a/src/dropdown-menu/type.ts b/src/dropdown-menu/type.ts index 96dd4b4c4..015b25398 100644 --- a/src/dropdown-menu/type.ts +++ b/src/dropdown-menu/type.ts @@ -28,8 +28,7 @@ export interface TdDropdownMenuProps { */ showOverlay?: boolean; /** - * 菜单栏 z-index 层级 - * @default 11600 + * 菜单栏 z-index 层级,默认为 1600 */ zIndex?: number; } @@ -117,7 +116,7 @@ export interface TdDropdownItemProps { export interface DropdownOption { label: string; - disabled: boolean; + disabled?: boolean; value: DropdownValue; } diff --git a/src/empty/_example/buttonEmpty.tsx b/src/empty/_example/button-empty.tsx similarity index 100% rename from src/empty/_example/buttonEmpty.tsx rename to src/empty/_example/button-empty.tsx diff --git a/src/empty/_example/imageEmpty.tsx b/src/empty/_example/image-empty.tsx similarity index 100% rename from src/empty/_example/imageEmpty.tsx rename to src/empty/_example/image-empty.tsx diff --git a/src/empty/_example/index.tsx b/src/empty/_example/index.tsx index 62b9014c7..5fdaecee8 100644 --- a/src/empty/_example/index.tsx +++ b/src/empty/_example/index.tsx @@ -2,8 +2,8 @@ import React from 'react'; import TDemoBlock from '../../../site/mobile/components/DemoBlock'; import TDemoHeader from '../../../site/mobile/components/DemoHeader'; import BaseDemo from './base'; -import ImageEmptyDemo from './imageEmpty'; -import ButtonEmptyDemo from './buttonEmpty'; +import ImageEmptyDemo from './image-empty'; +import ButtonEmptyDemo from './button-empty'; import './style/index.less'; diff --git a/src/fab/Fab.tsx b/src/fab/Fab.tsx index aebf64eb0..3fe4652fe 100644 --- a/src/fab/Fab.tsx +++ b/src/fab/Fab.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect, useLayoutEffect } from 'react'; +import React, { useRef, useState, useLayoutEffect, useEffect } from 'react'; import { reconvertUnit } from '../_util/convertUnit'; import Button from '../button'; import { TdFabProps } from './type'; @@ -6,6 +6,7 @@ import { fabDefaultProps } from './defaultProps'; import { StyledProps } from '../common'; import { usePrefixClass } from '../hooks/useClass'; import useDefaultProps from '../hooks/useDefaultProps'; +import useEventListener from '../hooks/useEventListener'; import parseTNode from '../_util/parseTNode'; export interface FabProps extends TdFabProps, StyledProps { @@ -20,23 +21,13 @@ const Fab: React.FC = (originProps) => { const fabRef = useRef(null); + const [mounted, setMounted] = useState(false); + const [fabButtonSize, setFabButtonSize] = useState({ - width: 0, - height: 0, + width: 48, + height: 48, }); - useLayoutEffect(() => { - const button = document.querySelector(`.${fabClass}__button`); - if (button) { - const info = window.getComputedStyle(button); - - setFabButtonSize({ - width: +reconvertUnit(info.width), - height: +reconvertUnit(info.height), - }); - } - }, [fabClass]); - const [btnSwitchPos, setBtnSwitchPos] = useState({ x: 16, y: 32, @@ -51,89 +42,78 @@ const Fab: React.FC = (originProps) => { endY: 0, }); - const onClickHandle = (e: React.MouseEvent) => { - onClick({ e }); + const getDocSize = () => ({ + width: Math.min(window.innerWidth, document.documentElement.clientWidth, screen.width), + height: Math.min(window.innerHeight, document.documentElement.clientHeight, screen.height), + }); + + const getBounds = () => ({ + topThreshold: reconvertUnit(props.yBounds?.[0] ?? 0), + bottomThreshold: reconvertUnit(props.yBounds?.[1] ?? 0), + leftBound: reconvertUnit(props.xBounds?.[0] ?? 0), + rightBound: reconvertUnit(props.xBounds?.[1] ?? 0), + }); + + const handleClick = (e: React.MouseEvent) => { + onClick?.({ e }); }; const onTouchStart = (e: React.TouchEvent) => { props.onDragStart?.({ e }); - switchPosRef.current = { - ...switchPosRef.current, - x: btnSwitchPos.x, - y: btnSwitchPos.y, - startX: e.touches[0].clientX, - startY: e.touches[0].clientY, - }; + switchPosRef.current.startX = e.touches[0].clientX; + switchPosRef.current.startY = e.touches[0].clientY; }; - const getSwitchButtonSafeAreaXY = (x: number, y: number) => { - const bottomThreshold = reconvertUnit(props.yBounds?.[1] ?? 0); - const topThreshold = reconvertUnit(props.yBounds?.[0] ?? 0); - - const docWidth = Math.min(window.innerWidth, document.documentElement.clientWidth, screen.width); - const docHeight = Math.min(window.innerHeight, document.documentElement.clientHeight, screen.height); + const getSwitchButtonSafeAreaXY = (x: number, y: number): [number, number] => { + const { topThreshold, bottomThreshold, leftBound, rightBound } = getBounds(); + const { width: docWidth, height: docHeight } = getDocSize(); const maxY = docHeight - fabButtonSize.height - topThreshold; - const maxX = docWidth - fabButtonSize.width; + // x 是 right 值:最小为右边界 rightBound(最靠右),最大为 docWidth - width - leftBound(最靠左) + const minX = rightBound; + const maxX = docWidth - fabButtonSize.width - leftBound; - const resultX = Math.max(0, Math.min(maxX, x)); - const resultY = Math.max(bottomThreshold, Math.min(maxY, y)); - - return [resultX, resultY]; + return [Math.max(minX, Math.min(maxX, x)), Math.max(bottomThreshold, Math.min(maxY, y))]; }; const onTouchMove = (e: TouchEvent) => { e.stopPropagation(); e.preventDefault(); - if (!props.draggable) { - return; - } - - if (e.touches.length <= 0) { + if (!props.draggable || e.touches.length <= 0) { return; } const offsetX = e.touches[0].clientX - switchPosRef.current.startX; const offsetY = e.touches[0].clientY - switchPosRef.current.startY; - const x = Math.floor(switchPosRef.current.x - offsetX); - const y = Math.floor(switchPosRef.current.y - offsetY); - - const [newX, newY] = getSwitchButtonSafeAreaXY(x, y); + const [newX, newY] = getSwitchButtonSafeAreaXY( + Math.floor(switchPosRef.current.x - offsetX), + Math.floor(switchPosRef.current.y - offsetY), + ); - const toChangeData = { ...btnSwitchPos }; - const toChangePos = { ...switchPosRef.current, hasMoved: true }; + switchPosRef.current.hasMoved = true; - if (props.draggable !== 'vertical') { - toChangeData.x = newX; - toChangePos.endX = newX; - } - if (props.draggable !== 'horizontal') { - toChangeData.y = newY; - toChangePos.endY = newY; - } - switchPosRef.current = toChangePos; - setBtnSwitchPos(toChangeData); + setBtnSwitchPos((prev) => { + const next = { ...prev }; + if (props.draggable !== 'vertical') { + next.x = newX; + switchPosRef.current.endX = newX; + } + if (props.draggable !== 'horizontal') { + next.y = newY; + switchPosRef.current.endY = newY; + } + return next; + }); }; - useEffect(() => { - const fab = fabRef.current; - fab?.addEventListener('touchmove', onTouchMove, { passive: false }); - - return () => { - fab?.removeEventListener('touchmove', onTouchMove); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.draggable, fabButtonSize.width, fabButtonSize.height]); + useEventListener(fabRef, 'touchmove', onTouchMove, { passive: false }); const setSwitchPosition = (switchX: number, switchY: number) => { const [newSwitchX, newSwitchY] = getSwitchButtonSafeAreaXY(switchX, switchY); - switchPosRef.current = { - ...switchPosRef.current, - x: newSwitchX, - y: newSwitchY, - }; + switchPosRef.current.x = newSwitchX; + switchPosRef.current.y = newSwitchY; // 使用函数式更新避免闭包陷阱 setBtnSwitchPos((prev) => { @@ -148,19 +128,85 @@ const Fab: React.FC = (originProps) => { }); }; + const handleMagnet = () => { + if (!props.magnet) { + return; + } + const { width: docWidth } = getDocSize(); + const { leftBound, rightBound } = getBounds(); + const leftX = docWidth - fabButtonSize.width - leftBound; + const rightX = rightBound; + + let targetX: number; + if (props.magnet === 'left') { + targetX = leftX; + } else if (props.magnet === 'right') { + targetX = rightX; + } else { + const currentRight = switchPosRef.current.x; + const currentLeft = docWidth - fabButtonSize.width - currentRight; + targetX = currentLeft < currentRight ? leftX : rightX; + } + + setSwitchPosition(targetX, switchPosRef.current.y); + }; + const onTouchEnd = (e: React.TouchEvent) => { if (!switchPosRef.current.hasMoved) { return; } props.onDragEnd?.({ e }); - switchPosRef.current = { - ...switchPosRef.current, - startX: 0, - startY: 0, - hasMoved: false, - }; + switchPosRef.current.startX = 0; + switchPosRef.current.startY = 0; + switchPosRef.current.hasMoved = false; setSwitchPosition(switchPosRef.current.endX, switchPosRef.current.endY); + + handleMagnet(); }; + + const getFabOriginStyle = () => { + if (!fabRef.current) { + return { right: 0, bottom: 0 }; + } + const info = window.getComputedStyle(fabRef.current); + return { + right: +(reconvertUnit(info.right) || 0), + bottom: +(reconvertUnit(info.bottom) || 0), + }; + }; + + const resetDraggableParams = () => { + const { right, bottom } = getFabOriginStyle(); + + switchPosRef.current.x = right; + switchPosRef.current.y = bottom; + setBtnSwitchPos({ x: right, y: bottom }); + }; + + // useLayoutEffect 在绘制前同步初始化位置,避免首次渲染闪动 + useLayoutEffect(() => { + setMounted(true); + resetDraggableParams(); + + const button = fabRef.current?.querySelector(`.${fabClass}__button`) || fabRef.current?.firstElementChild || null; + if (button) { + const info = window.getComputedStyle(button); + setFabButtonSize({ + width: +reconvertUnit(info.width), + height: +reconvertUnit(info.height), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // style 变更时重置拖拽参数 + useEffect(() => { + if (mounted) { + resetDraggableParams(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.style]); + const defaultContent = ( ); - const fabStyle = props.draggable - ? { - right: `${btnSwitchPos.x}px`, - bottom: `${btnSwitchPos.y}px`, - } - : props.style || { - right: `${btnSwitchPos.x}px`, - bottom: `${btnSwitchPos.y}px`, - }; + const fabClasses = [fabClass, !switchPosRef.current.hasMoved && props.magnet ? `${fabClass}--animation` : ''] + .filter(Boolean) + .join(' '); + + const fabStyle = + mounted && props.draggable + ? { + right: `${btnSwitchPos.x}px`, + bottom: `${btnSwitchPos.y}px`, + } + : props.style || { + right: `${btnSwitchPos.x}px`, + bottom: `${btnSwitchPos.y}px`, + }; return (
diff --git a/src/fab/__tests__/fab.test.tsx b/src/fab/__tests__/fab.test.tsx index d450b6ef3..2fec26802 100644 --- a/src/fab/__tests__/fab.test.tsx +++ b/src/fab/__tests__/fab.test.tsx @@ -1,11 +1,52 @@ import React from 'react'; -import { describe, it, expect, render, vi, fireEvent, waitFor, act } from '@test/utils'; +import { describe, it, expect, render, vi, fireEvent, waitFor, act, beforeEach, afterEach } from '@test/utils'; import { AddIcon } from 'tdesign-icons-react'; import Fab from '../index'; const prefix = 't'; const name = `.${prefix}-fab`; +// Mock 屏幕尺寸 +const mockScreenWidth = 375; +const mockScreenHeight = 667; + +beforeEach(() => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: mockScreenWidth, + }); + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: mockScreenHeight, + }); + Object.defineProperty(screen, 'width', { + writable: true, + configurable: true, + value: mockScreenWidth, + }); + Object.defineProperty(screen, 'height', { + writable: true, + configurable: true, + value: mockScreenHeight, + }); + Object.defineProperty(document.documentElement, 'clientWidth', { + writable: true, + configurable: true, + value: mockScreenWidth, + }); + Object.defineProperty(document.documentElement, 'clientHeight', { + writable: true, + configurable: true, + value: mockScreenHeight, + }); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + describe('Fab', () => { const createTouchEvent = (type: string, touches: any[], changedTouches?: any[]) => { const event = new TouchEvent(type, { @@ -70,6 +111,30 @@ describe('Fab', () => { expect(fabElement.style.bottom).toBe('32px'); }); + it(': xBounds', async () => { + const xBounds = [100, 16]; + const { container } = render(); + const fabElement = container.querySelector(name) as HTMLElement; + + act(() => { + const touchStart = createTouchEvent('touchstart', [{ clientX: 200, clientY: 100 }]); + fabElement!.dispatchEvent(touchStart); + }); + + act(() => { + const touchMove = createTouchEvent('touchmove', [{ clientX: 50, clientY: 100 }]); + fabElement!.dispatchEvent(touchMove); + }); + + act(() => { + const touchEnd = createTouchEvent('touchend', [], [{ clientX: 50, clientY: 100 }]); + fabElement!.dispatchEvent(touchEnd); + }); + + const rightVal = parseFloat(fabElement.style.right); + expect(rightVal).toBeLessThanOrEqual(375 - 48 - 100); + }); + it(': draggable', async () => { const onDragEnd = vi.fn(); // draggable=true @@ -223,6 +288,112 @@ describe('Fab', () => { expect(onDragEnd).toHaveBeenCalledTimes(1); }); }); + + it(': magnet - left', async () => { + const { container } = render(); + + act(() => { + const touchStart = createTouchEvent('touchstart', [{ clientX: 200, clientY: 100 }]); + container.querySelector(name)!.dispatchEvent(touchStart); + }); + + act(() => { + const touchMove = createTouchEvent('touchmove', [{ clientX: 100, clientY: 100 }]); + container.querySelector(name)!.dispatchEvent(touchMove); + }); + + act(() => { + const touchEnd = createTouchEvent('touchend', [], [{ clientX: 100, clientY: 100 }]); + container.querySelector(name)!.dispatchEvent(touchEnd); + }); + + await waitFor(() => { + const fabElement = container.querySelector(name) as HTMLElement; + const rightVal = parseFloat(fabElement.style.right); + expect(rightVal).toBeGreaterThan(16); + }); + }); + + it(': magnet - right', async () => { + const { container } = render(); + + act(() => { + const touchStart = createTouchEvent('touchstart', [{ clientX: 200, clientY: 100 }]); + container.querySelector(name)!.dispatchEvent(touchStart); + }); + + act(() => { + const touchMove = createTouchEvent('touchmove', [{ clientX: 100, clientY: 100 }]); + container.querySelector(name)!.dispatchEvent(touchMove); + }); + + act(() => { + const touchEnd = createTouchEvent('touchend', [], [{ clientX: 100, clientY: 100 }]); + container.querySelector(name)!.dispatchEvent(touchEnd); + }); + + await waitFor(() => { + const fabElement = container.querySelector(name) as HTMLElement; + const rightVal = parseFloat(fabElement.style.right); + expect(rightVal).toBeLessThanOrEqual(16); + }); + }); + + it(': magnet - true (auto)', async () => { + const { container } = render(); + + act(() => { + const touchStart = createTouchEvent('touchstart', [{ clientX: 200, clientY: 100 }]); + container.querySelector(name)!.dispatchEvent(touchStart); + }); + + act(() => { + const touchMove = createTouchEvent('touchmove', [{ clientX: 100, clientY: 100 }]); + container.querySelector(name)!.dispatchEvent(touchMove); + }); + + act(() => { + const touchEnd = createTouchEvent('touchend', [], [{ clientX: 100, clientY: 100 }]); + container.querySelector(name)!.dispatchEvent(touchEnd); + }); + + await waitFor(() => { + const fabElement = container.querySelector(name) as HTMLElement; + const rightVal = parseFloat(fabElement.style.right); + expect(rightVal).toBeLessThanOrEqual(16); + }); + }); + + it(': magnet animation class', async () => { + const { container } = render(); + const fabElement = container.querySelector(name) as HTMLElement; + + expect(fabElement.classList.contains(`${prefix}-fab--animation`)).toBeTruthy(); + + act(() => { + const touchStart = createTouchEvent('touchstart', [{ clientX: 200, clientY: 100 }]); + fabElement!.dispatchEvent(touchStart); + }); + + act(() => { + const touchMove = createTouchEvent('touchmove', [{ clientX: 100, clientY: 100 }]); + fabElement!.dispatchEvent(touchMove); + }); + + await waitFor(() => { + const updatedFab = container.querySelector(name) as HTMLElement; + expect(updatedFab.classList.contains(`${prefix}-fab--animation`)).toBeFalsy(); + }); + act(() => { + const touchEnd = createTouchEvent('touchend', [], [{ clientX: 100, clientY: 100 }]); + fabElement!.dispatchEvent(touchEnd); + }); + + await waitFor(() => { + const updatedFab = container.querySelector(name) as HTMLElement; + expect(updatedFab.classList.contains(`${prefix}-fab--animation`)).toBeTruthy(); + }); + }); }); describe('slots', () => { diff --git a/src/fab/fab.en-US.md b/src/fab/fab.en-US.md index 39b286b2a..0bc25cc72 100644 --- a/src/fab/fab.en-US.md +++ b/src/fab/fab.en-US.md @@ -11,9 +11,9 @@ style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSPropert buttonProps | Object | - | Typescript: `ButtonProps`,[Button API Documents](./button?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N draggable | String / Boolean | false | Typescript: `boolean \| FabDirectionEnum ` `type FabDirectionEnum = 'all' \| 'vertical' \| 'horizontal'`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N icon | TElement | - | Typescript: `TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N -magnet | String / Boolean | - | `0.21.2`。Typescript: `boolean \| MagnetEnum ` `type MagnetEnum = 'left' \| 'right'`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N +magnet | String / Boolean | - | `1.0.0`。Typescript: `boolean \| MagnetEnum ` `type MagnetEnum = 'left' \| 'right'`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N text | String | - | \- | N -xBounds | Array | - | `0.21.2`。Typescript: `Array` | N +xBounds | Array | - | `1.0.0`。Typescript: `Array` | N yBounds | Array | - | Typescript: `Array` | N onClick | Function | | Typescript: `(context: {e: MouseEvent}) => void`
| N onDragEnd | Function | | Typescript: `(context: { e: TouchEvent }) => void`
| N diff --git a/src/fab/fab.md b/src/fab/fab.md index 48c51087f..273ab9e40 100644 --- a/src/fab/fab.md +++ b/src/fab/fab.md @@ -11,9 +11,9 @@ style | Object | - | 样式,TS 类型:`React.CSSProperties` | N buttonProps | Object | - | 透传至 Button 组件。TS 类型:`ButtonProps`,[Button API Documents](./button?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N draggable | String / Boolean | false | 是否可拖拽。`true` / `'all'`可拖动
`'vertical'`可垂直拖动
`'horizontal'`可水平拖动
`false`禁止拖动。TS 类型:`boolean \| FabDirectionEnum ` `type FabDirectionEnum = 'all' \| 'vertical' \| 'horizontal'`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N icon | TElement | - | 图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N -magnet | String / Boolean | - | 【开发中】是否吸附。`true` 启用左右吸附,`left` 仅吸附到左边,`right` 仅吸附到右边,`false` 不吸附。TS 类型:`boolean \| MagnetEnum ` `type MagnetEnum = 'left' \| 'right'`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N +magnet | String / Boolean | - | `1.0.0`。是否吸附,仅在拖拽场景下生效。`true` 启用左右吸附,`left` 仅吸附到左边,`right` 仅吸附到右边,`false` 不吸附。TS 类型:`boolean \| MagnetEnum ` `type MagnetEnum = 'left' \| 'right'`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/fab/type.ts) | N text | String | - | 文本内容 | N -xBounds | Array | - | 【开发中】设置水平方向边界限制,示例:[16, 16] 或 ['16px', 16]。TS 类型:`Array` | N +xBounds | Array | - | `1.0.0`。设置水平方向边界限制,示例:[16, 16] 或 ['16px', 16]。TS 类型:`Array` | N yBounds | Array | - | 设置垂直方向边界限制,示例:[48, 48] 或 ['96px', 80]。TS 类型:`Array` | N onClick | Function | | TS 类型:`(context: {e: MouseEvent}) => void`
悬浮按钮点击事件 | N onDragEnd | Function | | TS 类型:`(context: { e: TouchEvent }) => void`
结束拖拽时触发 | N diff --git a/src/fab/type.ts b/src/fab/type.ts index d2af9a752..4d66e5396 100644 --- a/src/fab/type.ts +++ b/src/fab/type.ts @@ -23,7 +23,7 @@ export interface TdFabProps { */ icon?: TElement; /** - * 是否吸附。`true` 启用左右吸附,`left` 仅吸附到左边,`right` 仅吸附到右边,`false` 不吸附 + * 是否吸附,仅在拖拽场景下生效。`true` 启用左右吸附,`left` 仅吸附到左边,`right` 仅吸附到右边,`false` 不吸附 */ magnet?: boolean | MagnetEnum; /** diff --git a/src/form/FormItem.tsx b/src/form/FormItem.tsx index fc4bdfc6a..bcef6912a 100644 --- a/src/form/FormItem.tsx +++ b/src/form/FormItem.tsx @@ -507,7 +507,7 @@ const FormItem = forwardRef((props, ref) => { if (!arrow) { return null; } - return ; + return ; }; const renderLabelContent = (): React.ReactNode => { diff --git a/src/form/__tests__/form.test.tsx b/src/form/__tests__/form.test.tsx index 52c17ac63..a89d7f1df 100644 --- a/src/form/__tests__/form.test.tsx +++ b/src/form/__tests__/form.test.tsx @@ -1690,7 +1690,7 @@ describe('Form 组件测试', () => { expect(() => requestSubmit({} as any)).toThrow('target must be HTMLFormElement'); }); - it('HTMLFormElement 正常工作', () => { + it('HTMLFormElement 正常工作', async () => { const { container } = render(
@@ -1698,7 +1698,9 @@ describe('Form 组件测试', () => {
, ); - expect(() => requestSubmit(container.querySelector('form') as HTMLFormElement)).not.toThrow(); + await act(async () => { + expect(() => requestSubmit(container.querySelector('form') as HTMLFormElement)).not.toThrow(); + }); }); }); @@ -1804,7 +1806,7 @@ describe('Form 组件测试', () => { expect((getByPlaceholderText('name') as HTMLInputElement).value).toBe('delayed'); }); - it('taskQueue 延迟执行 - setFields/submit/reset/clearValidate/setValidateMessage', () => { + it('taskQueue 延迟执行 - setFields/submit/reset/clearValidate/setValidateMessage', async () => { const submitFn = vi.fn(); const TestForm = () => { const [form] = Form.useForm(); @@ -1832,6 +1834,7 @@ describe('Form 组件测试', () => { }; render(); // taskQueue 方法在 flashQueue 时执行,不崩溃即可 + await mockDelay(); }); }); @@ -2490,6 +2493,7 @@ describe('Form 组件测试', () => { ); // 无 onSubmit 时不崩溃 expect(() => fireEvent.click(getByText('submit'))).not.toThrow(); + await mockDelay(); }); it('onReset 不传时 reset 正常工作', async () => { diff --git a/src/form/_example/horizontal.tsx b/src/form/_example/horizontal.tsx index 2f9754ee5..b7584ffd4 100644 --- a/src/form/_example/horizontal.tsx +++ b/src/form/_example/horizontal.tsx @@ -1,10 +1,153 @@ -import React, { useMemo, useRef } from 'react'; -import { Button, Form, Input, Radio, RadioGroup, Rate, Stepper, Textarea } from 'tdesign-mobile-react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { + Button, + Cascader, + DateTimePicker, + Form, + Input, + Popup, + Radio, + RadioGroup, + Rate, + Stepper, + Textarea, + Upload, + type UploadFile, +} from 'tdesign-mobile-react'; import { BrowseOffIcon } from 'tdesign-icons-react'; const RATE_GAP = 8; -export default function HorizontalForm({ disabled }) { - const formRef = useRef(null); + +const action = 'https://service-bv448zsw-1257786608.gz.apigw.tencentcs.com/api/upload-demo'; + +const initialPhoto: UploadFile[] = [ + { + url: 'https://tdesign.gtimg.com/mobile/demos/example4.png', + name: 'uploaded1.png', + type: 'image', + }, + { + url: 'https://tdesign.gtimg.com/mobile/demos/example6.png', + name: 'uploaded2.png', + type: 'image', + }, +]; + +const initialData = { + name: '', + password: '', + gender: '', + birth: '', + place: '', + age: 3, + description: 2, + resume: '', + photo: initialPhoto, +}; + +const addressOptions = [ + { + label: '北京市', + value: '110000', + children: [ + { + value: '110100', + label: '北京市', + children: [ + { value: '110101', label: '东城区' }, + { value: '110102', label: '西城区' }, + { value: '110105', label: '朝阳区' }, + { value: '110106', label: '丰台区' }, + { value: '110107', label: '石景山区' }, + { value: '110108', label: '海淀区' }, + { value: '110109', label: '门头沟区' }, + { value: '110111', label: '房山区' }, + { value: '110112', label: '通州区' }, + { value: '110113', label: '顺义区' }, + { value: '110114', label: '昌平区' }, + { value: '110115', label: '大兴区' }, + { value: '110116', label: '怀柔区' }, + { value: '110117', label: '平谷区' }, + { value: '110118', label: '密云区' }, + { value: '110119', label: '延庆区' }, + ], + }, + ], + }, + { + label: '天津市', + value: '120000', + children: [ + { + value: '120100', + label: '天津市', + children: [ + { value: '120101', label: '和平区' }, + { value: '120102', label: '河东区' }, + { value: '120103', label: '河西区' }, + { value: '120104', label: '南开区' }, + { value: '120105', label: '河北区' }, + { value: '120106', label: '红桥区' }, + { value: '120110', label: '东丽区' }, + { value: '120111', label: '西青区' }, + { value: '120112', label: '津南区' }, + { value: '120113', label: '北辰区' }, + { value: '120114', label: '武清区' }, + { value: '120115', label: '宝坻区' }, + { value: '120116', label: '滨海新区' }, + { value: '120117', label: '宁河区' }, + { value: '120118', label: '静海区' }, + { value: '120119', label: '蓟州区' }, + ], + }, + ], + }, +]; + +const onFail = ({ file, e }: any) => { + console.log('[onFail]', { file, e }); +}; + +const onProgress = ({ file, percent, type, e }: any) => { + console.log('[onProgress]', { file, percent, type, e }); +}; + +const onPreview = ({ file, e }: any) => { + console.log('[onPreview]', { file, e }); +}; + +const onSuccess = ({ file, fileList, response, e }: any) => { + console.log('[onSuccess]', { file, fileList, e, response }); +}; + +const onRemove = ({ index, file, e }: any) => { + console.log('[onRemove]', { index, file, e }); +}; + +const onSelectChange = (files: UploadFile[]) => { + console.log('[onSelectChange]', files); +}; + +export default function HorizontalForm({ disabled }: { disabled?: boolean }) { + const [form] = Form.useForm(); + const [birthVisible, setBirthVisible] = useState(false); + const [placeVisible, setPlaceVisible] = useState(false); + const [placeValue, setPlaceValue] = useState('120119'); + const birthInputRef = useRef(null); + const placeInputRef = useRef(null); + + useEffect(() => { + if (!birthVisible) { + birthInputRef.current?.blur?.(); + } + }, [birthVisible]); + + useEffect(() => { + if (!placeVisible) { + placeInputRef.current?.blur?.(); + } + }, [placeVisible]); + const rules = useMemo( () => ({ name: [{ validator: (val: any) => val?.length === 8, message: '只能输入8个字符英文' }], @@ -18,55 +161,152 @@ export default function HorizontalForm({ disabled }) { [], ); + const groupChangeFn = (value: any, context: any) => { + console.log(value, context); + }; + + const onBirthChange = (value: any) => { + console.log('[change]', value); + }; + + const onBirthPick = (value: any) => { + console.log('[pick]', value); + }; + + const onBirthCancel = () => { + console.log('[cancel]'); + setBirthVisible(false); + }; + + const onBirthConfirm = (value: any) => { + console.log('[confirm]', value); + form.setFieldsValue({ birth: String(value) }); + setBirthVisible(false); + }; + + const onPlaceChange = (value: string | number, selectedOptions: any) => { + setPlaceValue(value); + setPlaceVisible(false); + form.setFieldsValue({ place: selectedOptions?.map((item: any) => item.label).join('/') }); + }; + const onReset = () => { - console.log('===onReset'); + console.log('[onReset]'); }; + const onSubmit = (e: any) => { - console.log('===onSubmit', e); + console.log('[onSubmit]', e); }; + return ( -
- - - - - } /> - - - - - - - - - - - - - - - -
0/50
"`; +exports[`ssr snapshot test > ssr test src/form/_example/horizontal.tsx 1`] = `"
输入用户名
男
女
保密
0/50
"`; -exports[`ssr snapshot test > ssr test src/form/_example/index.tsx 1`] = `"

Form 表单

用以收集、校验和提交数据,一般由输入框、单选框、复选框、选择器等控件组成。

01 组件类型

基础表单

禁用态
请输入用户名
男
女
保密
0/50
"`; +exports[`ssr snapshot test > ssr test src/form/_example/index.tsx 1`] = `"

Form 表单

用以收集、校验和提交数据,一般由输入框、单选框、复选框、选择器等控件组成。

01 组件类型

基础表单

禁用态
输入用户名
男
女
保密
0/50
"`; -exports[`ssr snapshot test > ssr test src/form/_example/vertical.tsx 1`] = `"
请输入用户名
男
女
保密
0/50
"`; +exports[`ssr snapshot test > ssr test src/form/_example/vertical.tsx 1`] = `"
输入用户名
男
女
保密
0/50
"`; exports[`ssr snapshot test > ssr test src/grid/_example/badge.tsx 1`] = `"
标题文字
8
标题文字
13
标题五字内
NEW
标题五字内
"`; @@ -165997,17 +175937,17 @@ exports[`ssr snapshot test > ssr test src/input/_example/bordered.tsx 1`] = `" ssr test src/input/_example/custom.tsx 1`] = `"
标签文字
"`; -exports[`ssr snapshot test > ssr test src/input/_example/index.tsx 1`] = `"

Input 输入框

用于单行文本信息输入。

01 组件类型

基础输入框

标签文字
标签文字

带字数限制输入框

标签文字
最大输入10个字符
标签文字
最大输入10个字符,汉字算两个

带操作输入框

标签文字
标签文字
标签文字

带图标输入框

标签文字

特定类型输入框

输入密码
验证码
手机号
发送验证码
价格
元
数量
个

02 组件状态

输入框状态

标签文字
辅助说明
不可编辑

信息超长状态

标签超长时最多十个字

03 组件样式

内容位置

左对齐
居中
右对齐

竖排样式

标签文字

非通栏样式

标签文字

标签外置样式

标签文字

自定义样式输入框

标签文字
"`; +exports[`ssr snapshot test > ssr test src/input/_example/index.tsx 1`] = `"

Input 输入框

用于单行文本信息输入。

01 组件类型

基础输入框

标签文字
标签文字

带字数限制输入框

标签文字
最大输入10个字符
标签文字
最大输入10个字符,汉字算两个

带操作输入框

标签文字
标签文字
标签文字

带图标输入框

标签文字

特定类型输入框

输入密码
验证码
手机号
发送验证码
价格
元
数量
个

02 组件状态

输入框状态

标签文字
辅助说明
不可编辑

信息超长状态

标签超长时最多十个字

03 组件样式

内容位置

左对齐
居中
右对齐

竖排样式

标签文字

非通栏样式

标签文字

标签外置样式

标签文字

自定义样式输入框

标签文字
"`; exports[`ssr snapshot test > ssr test src/input/_example/label.tsx 1`] = `"
标签超长时最多十个字
"`; exports[`ssr snapshot test > ssr test src/input/_example/layout.tsx 1`] = `"
标签文字
"`; -exports[`ssr snapshot test > ssr test src/input/_example/maxLength.tsx 1`] = `"
标签文字
最大输入10个字符
标签文字
最大输入10个字符,汉字算两个
"`; +exports[`ssr snapshot test > ssr test src/input/_example/maxlength.tsx 1`] = `"
标签文字
最大输入10个字符
标签文字
最大输入10个字符,汉字算两个
"`; exports[`ssr snapshot test > ssr test src/input/_example/prefix.tsx 1`] = `"
标签文字
"`; -exports[`ssr snapshot test > ssr test src/input/_example/special.tsx 1`] = `"
输入密码
验证码
手机号
发送验证码
价格
元
数量
个
"`; +exports[`ssr snapshot test > ssr test src/input/_example/special.tsx 1`] = `"
输入密码
验证码
手机号
发送验证码
价格
元
数量
个
"`; exports[`ssr snapshot test > ssr test src/input/_example/status.tsx 1`] = `"
标签文字
辅助说明
不可编辑
"`; @@ -166023,7 +175963,7 @@ exports[`ssr snapshot test > ssr test src/link/_example/base.tsx 1`] = `"
ssr test src/link/_example/index.tsx 1`] = `"

Link 链接

文字超链接用于跳转一个新页面,如当前项目跳转,友情链接等。

01 组件类型

基础文字链接

下划线文字链接

前置图标文字链接

后置图标文字链接

03 组件样式

链接尺寸

"`; -exports[`ssr snapshot test > ssr test src/link/_example/linkSize.tsx 1`] = `""`; +exports[`ssr snapshot test > ssr test src/link/_example/link-size.tsx 1`] = `""`; exports[`ssr snapshot test > ssr test src/link/_example/prefix.tsx 1`] = `""`; @@ -166079,7 +176019,7 @@ exports[`ssr snapshot test > ssr test src/navbar/_example/custom-color.tsx 1`] = exports[`ssr snapshot test > ssr test src/navbar/_example/img.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test src/navbar/_example/index.tsx 1`] = `"
Navbar 导航条

NavBar 导航栏

用于不同页面之间切换或者跳转,位于内容区的上方,系统状态栏的下方。

01 组件类型

基础导航栏

标题文字
标题文字
标题文字

带搜索导航栏

带图片导航栏

02 组件样式

标题对齐

标题居中
标题左对齐

标题尺寸

标题文字
返回
大尺寸标题

自定义颜色

标题文字
"`; +exports[`ssr snapshot test > ssr test src/navbar/_example/index.tsx 1`] = `"
Navbar 导航条

Navbar 导航栏

用于不同页面之间切换或者跳转,位于内容区的上方,系统状态栏的下方。

01 组件类型

基础导航栏

标题文字
标题文字
标题文字

带搜索导航栏

带图片导航栏

02 组件样式

标题对齐

标题居中
标题左对齐

标题尺寸

标题文字
返回
大尺寸标题

自定义颜色

标题文字
"`; exports[`ssr snapshot test > ssr test src/navbar/_example/left-title.tsx 1`] = `"
标题居中
标题左对齐
"`; @@ -166095,13 +176035,13 @@ exports[`ssr snapshot test > ssr test src/notice-bar/_example/customization.tsx exports[`ssr snapshot test > ssr test src/notice-bar/_example/event.tsx 1`] = `"
这是一条普通的消息通知
"`; -exports[`ssr snapshot test > ssr test src/notice-bar/_example/iconDemo.tsx 1`] = `"
提示文字描述提示文字描述提示文字描述
"`; +exports[`ssr snapshot test > ssr test src/notice-bar/_example/icon-demo.tsx 1`] = `"
提示文字描述提示文字描述提示文字描述
"`; exports[`ssr snapshot test > ssr test src/notice-bar/_example/index.tsx 1`] = `"

NoticeBar 公告栏

在导航栏下方,用于给用户显示提示消息。

01 组件类型

纯文字的公告栏

这是一条普通的通知消息

可滚动的公告栏

提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述
这是一条带图标的水平滚动公告栏
这是一条带图标的纵向滚动公告栏
这是一条带图标的纵向滚动公告栏
这是一条带图标的纵向滚动公告栏

带图标的公告栏

提示文字描述提示文字描述提示文字描述

带关闭的公告栏

这是一条普通的通知信息

带入口的公告栏

这是一条普通的消息通知

自定义样式的公告栏

提示文字描述提示文字描述提示文字描述

自定义内容的公告栏

提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述详情

02 组件状态

普通通知
这是一条普通的通知信息
成功通知
这是一条成功的通知信息
警示通知
这是一条警示的通知信息
错误通知
这是一条错误的通知信息
"`; exports[`ssr snapshot test > ssr test src/notice-bar/_example/scrolling.tsx 1`] = `"
提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述
这是一条带图标的水平滚动公告栏
这是一条带图标的纵向滚动公告栏
这是一条带图标的纵向滚动公告栏
这是一条带图标的纵向滚动公告栏
"`; -exports[`ssr snapshot test > ssr test src/notice-bar/_example/suffixIcon.tsx 1`] = `"
这是一条普通的通知信息
"`; +exports[`ssr snapshot test > ssr test src/notice-bar/_example/suffix-icon.tsx 1`] = `"
这是一条普通的通知信息
"`; exports[`ssr snapshot test > ssr test src/notice-bar/_example/theme.tsx 1`] = `"
普通通知
这是一条普通的通知信息
成功通知
这是一条成功的通知信息
警示通知
这是一条警示的通知信息
错误通知
这是一条错误的通知信息
"`; @@ -166115,7 +176055,7 @@ exports[`ssr snapshot test > ssr test src/picker/_example/base.tsx 1`] = `"
ssr test src/picker/_example/custom-height.tsx 1`] = `"
自定义高度选择器
"`; -exports[`ssr snapshot test > ssr test src/picker/_example/index.tsx 1`] = `"

Picker 选择器

用于一组预设数据中的选择。

01 组件类型

基础选择器

选择地区
选择季节
选择地区

02 组件样式

是否带标题

带标题选择器
无标题选择器
自定义高度选择器
"`; +exports[`ssr snapshot test > ssr test src/picker/_example/index.tsx 1`] = `"

Picker 选择器

用于一组预设数据中的选择。

01 组件类型

基础选择器

选择地区
选择季节
选择地区

02 组件样式

是否带标题

带标题选择器
无标题选择器
自定义高度选择器
"`; exports[`ssr snapshot test > ssr test src/picker/_example/with-title.tsx 1`] = `"
带标题选择器
无标题选择器
"`; @@ -166163,9 +176103,11 @@ exports[`ssr snapshot test > ssr test src/qrcode/_example/borderless.tsx 1`] = ` exports[`ssr snapshot test > ssr test src/qrcode/_example/color.tsx 1`] = `"

二维码背景颜色

"`; +exports[`ssr snapshot test > ssr test src/qrcode/_example/download.tsx 1`] = `"
"`; + exports[`ssr snapshot test > ssr test src/qrcode/_example/icon.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test src/qrcode/_example/index.tsx 1`] = `"

QRCode 二维码

能够将文本转换生成二维码的组件,支持自定义配色和 Logo 配置。

01 组件类型

基本用法

带 Icon 的二维码

无边框的二维码

二维码纠错等级

02 组件状态

active

expired

二维码过期

点击刷新

loading

scanned

已扫描

03 组件样式

二维码颜色

二维码背景颜色

二维码尺寸

"`; +exports[`ssr snapshot test > ssr test src/qrcode/_example/index.tsx 1`] = `"

QRCode 二维码

能够将文本转换生成二维码的组件,支持自定义配色和 Logo 配置。

01 组件类型

基本用法

指定二维码类型

带 Icon 的二维码

无边框的二维码

二维码纠错等级

02 组件状态

active

expired

二维码过期

点击刷新

loading

scanned

已扫描

自定义状态渲染

加载中...

二维码过期

点击刷新

已扫描

03 组件样式

二维码颜色

二维码背景颜色

二维码尺寸

04 进阶

下载二维码

"`; exports[`ssr snapshot test > ssr test src/qrcode/_example/level.tsx 1`] = `"
"`; @@ -166173,17 +176115,21 @@ exports[`ssr snapshot test > ssr test src/qrcode/_example/size.tsx 1`] = `"
ssr test src/qrcode/_example/status.tsx 1`] = `"

expired

二维码过期

点击刷新

loading

scanned

已扫描

"`; -exports[`ssr snapshot test > ssr test src/radio/_example/base.tsx 1`] = `"
单选
单选
单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
单选
描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息
"`; +exports[`ssr snapshot test > ssr test src/qrcode/_example/status-render.tsx 1`] = `"

加载中...

二维码过期

点击刷新

已扫描

"`; + +exports[`ssr snapshot test > ssr test src/qrcode/_example/type.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test src/radio/_example/card.tsx 1`] = `"
单选
单选
单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
"`; +exports[`ssr snapshot test > ssr test src/radio/_example/base.tsx 1`] = `"
单选
单选
单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
单选
描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息
"`; -exports[`ssr snapshot test > ssr test src/radio/_example/custom.tsx 1`] = `"

纵向卡片单选框

单选
描述信息描述信息描述信息描述信息描述信息
单选
描述信息描述信息描述信息描述信息描述信息
单选
描述信息描述信息描述信息描述信息描述信息

横向卡片单选框

单选
单选
单选
"`; +exports[`ssr snapshot test > ssr test src/radio/_example/card.tsx 1`] = `"
单选
单选
单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
"`; -exports[`ssr snapshot test > ssr test src/radio/_example/horizontal.tsx 1`] = `"
单选标题
单选标题
上限四字
"`; +exports[`ssr snapshot test > ssr test src/radio/_example/custom.tsx 1`] = `"

纵向卡片单选框

单选
描述信息描述信息描述信息描述信息描述信息
单选
描述信息描述信息描述信息描述信息描述信息
单选
描述信息描述信息描述信息描述信息描述信息

横向卡片单选框

单选
单选
单选
"`; + +exports[`ssr snapshot test > ssr test src/radio/_example/horizontal.tsx 1`] = `"
单选标题
单选标题
上限四字
"`; exports[`ssr snapshot test > ssr test src/radio/_example/icon.tsx 1`] = `"
单选
单选
"`; -exports[`ssr snapshot test > ssr test src/radio/_example/index.tsx 1`] = `"

Radio 单选框

用于在预设的一组选项中执行单项选择,并呈现选择结果。

01 组件类型

纵向单选框

单选
单选
单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
单选
描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息

横向单选框

单选标题
单选标题
上限四字

02 组件状态

单选框状态

选项禁用-已选
选项禁用-默认

03 组件样式

勾选样式

单选
单选

勾选显示位置

单选
单选

非通栏单选样式

单选
单选
单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行

04 特殊样式

纵向卡片单选框

单选
描述信息描述信息描述信息描述信息描述信息
单选
描述信息描述信息描述信息描述信息描述信息
单选
描述信息描述信息描述信息描述信息描述信息

横向卡片单选框

单选
单选
单选
"`; +exports[`ssr snapshot test > ssr test src/radio/_example/index.tsx 1`] = `"

Radio 单选框

用于在预设的一组选项中执行单项选择,并呈现选择结果。

01 组件类型

纵向单选框

单选
单选
单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
单选
描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息

横向单选框

单选标题
单选标题
上限四字

02 组件状态

单选框状态

选项禁用-已选
选项禁用-默认

03 组件样式

勾选样式

单选
单选

勾选显示位置

单选
单选

非通栏单选样式

单选
单选
单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行

04 特殊样式

纵向卡片单选框

单选
描述信息描述信息描述信息描述信息描述信息
单选
描述信息描述信息描述信息描述信息描述信息
单选
描述信息描述信息描述信息描述信息描述信息

横向卡片单选框

单选
单选
单选
"`; exports[`ssr snapshot test > ssr test src/radio/_example/placement.tsx 1`] = `"
单选
单选
"`; @@ -166263,7 +176209,7 @@ exports[`ssr snapshot test > ssr test src/slider/_example/capsule.tsx 1`] = `" ssr test src/slider/_example/disabled.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test src/slider/_example/index.tsx 1`] = `"

Slider 滑动选择器

用于选择横轴上的数值、区间、档位。

01 组件类型

单游标滑块

双游标滑块

带数值滑动选择器

10
0
0
0
100

带刻度滑动选择器

02 组件状态

滑块禁用状态

03 特殊样式

胶囊型滑块

0%
0%
"`; +exports[`ssr snapshot test > ssr test src/slider/_example/index.tsx 1`] = `"

Slider 滑动选择器

用于选择横轴上的数值、区间、档位。

01 组件类型

单游标滑块

双游标滑块

带数值滑动选择器

10
0
0
0
100

带刻度滑动选择器

02 组件状态

滑块禁用状态

03 特殊样式

胶囊型滑块

0%
0%

垂直滑块

23
23
"`; exports[`ssr snapshot test > ssr test src/slider/_example/label.tsx 1`] = `"
10
0
0
0
100
"`; @@ -166271,6 +176217,8 @@ exports[`ssr snapshot test > ssr test src/slider/_example/range.tsx 1`] = `"
ssr test src/slider/_example/step.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test src/slider/_example/vertical.tsx 1`] = `"
23
23
"`; + exports[`ssr snapshot test > ssr test src/stepper/_example/base.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test src/stepper/_example/index.tsx 1`] = `"

Stepper 步进器

用于数量的增减。

01 组件类型

基础步进器

02 组件状态

最大最小状态

禁用状态

03 组件样式

步进器样式

步进器尺寸

"`; @@ -166303,11 +176251,11 @@ exports[`ssr snapshot test > ssr test src/sticky/_example/offset.tsx 1`] = `" ssr test src/swipe-cell/_example/double.tsx 1`] = `"

左右滑操作

删除
左右滑操作
辅助信息
收藏
删除
"`; -exports[`ssr snapshot test > ssr test src/swipe-cell/_example/event.tsx 1`] = `"

带二次确认的操作

编辑
删除
带二次确认的操作
辅助信息
编辑
删除
"`; +exports[`ssr snapshot test > ssr test src/swipe-cell/_example/event.tsx 1`] = `"

带二次确认的操作

编辑
删除
带二次确认的操作
辅助信息
编辑
删除
"`; exports[`ssr snapshot test > ssr test src/swipe-cell/_example/icon.tsx 1`] = `"

带图标的滑动操作

图标加文字横排
辅助信息
编辑
删除
纯图标
辅助信息
图标加文字竖排
一段很长很长的内容文字
辅助信息
编辑
删除
"`; -exports[`ssr snapshot test > ssr test src/swipe-cell/_example/index.tsx 1`] = `"

SwipeCell 滑动操作

用于承载列表中的更多操作,通过左右滑动来展示,按钮的宽度固定高度根据列表高度而变化。

左滑单操作

左滑单操作
辅助信息
删除
左滑单操作
一段很长很长的内容文字
辅助信息
删除
左滑双操作
辅助信息
收藏
编辑
左滑三操作
辅助信息
收藏
编辑
删除

右滑单操作

选择
右滑单操作
辅助信息

左右滑操作

删除
左右滑操作
辅助信息
收藏
删除

带图标的滑动操作

图标加文字横排
辅助信息
编辑
删除
纯图标
辅助信息
图标加文字竖排
一段很长很长的内容文字
辅助信息
编辑
删除

带二次确认的操作

编辑
删除
带二次确认的操作
辅助信息
编辑
删除
"`; +exports[`ssr snapshot test > ssr test src/swipe-cell/_example/index.tsx 1`] = `"

SwipeCell 滑动操作

用于承载列表中的更多操作,通过左右滑动来展示,按钮的宽度固定高度根据列表高度而变化。

01 组件类型

左滑单操作

左滑单操作
辅助信息
删除
左滑单操作
一段很长很长的内容文字
辅助信息
删除
左滑双操作
辅助信息
收藏
编辑
左滑三操作
辅助信息
收藏
编辑
删除

右滑单操作

选择
右滑单操作
辅助信息

左右滑操作

删除
左右滑操作
辅助信息
收藏
删除

带图标的滑动操作

图标加文字横排
辅助信息
编辑
删除
纯图标
辅助信息
图标加文字竖排
一段很长很长的内容文字
辅助信息
编辑
删除

带二次确认的操作

编辑
删除
带二次确认的操作
辅助信息
编辑
删除
"`; exports[`ssr snapshot test > ssr test src/swipe-cell/_example/left.tsx 1`] = `"

左滑单操作

左滑单操作
辅助信息
删除
左滑单操作
一段很长很长的内容文字
辅助信息
删除
左滑双操作
辅助信息
收藏
编辑
左滑三操作
辅助信息
收藏
编辑
删除
"`; @@ -166411,7 +176359,7 @@ exports[`ssr snapshot test > ssr test src/tag/_example/checkable.tsx 1`] = `" ssr test src/tag/_example/closable.tsx 1`] = `"
可关闭标签
文字标签文字标签
"`; -exports[`ssr snapshot test > ssr test src/tag/_example/index.tsx 1`] = `"

Tag 标签

用于表明主体的类目,属性或状态。

01 组件类型

基础标签
标签文字标签文字
圆弧标签
标签文字标签文字标签文字
带图标的标签
标签文字标签文字
超长文本省略标签
听说超长可以省略听说超长
可关闭标签
文字标签文字标签
可选中的标签
light
未选中态已选中态
dark
未选中态已选中态
outline
未选中态已选中态
light-outline
未选中态已选中态

02 组件状态

展示型标签
默认主要警告危险成功
默认主要警告危险成功
默认主要警告危险成功
默认主要警告危险成功

03 组件尺寸

加大尺寸大尺寸中尺寸小尺寸
加大尺寸大尺寸中尺寸小尺寸
"`; +exports[`ssr snapshot test > ssr test src/tag/_example/index.tsx 1`] = `"

Tag 标签

用于表明主体的类目,属性或状态。

01 组件类型

基础标签
标签文字标签文字
圆弧标签
标签文字标签文字标签文字
带图标的标签
标签文字标签文字
超长文本省略标签
听说超长可以省略听说超长
可关闭标签
文字标签文字标签
可选中的标签
light
未选中态已选中态
dark
未选中态已选中态
outline
未选中态已选中态
light-outline
未选中态已选中态

02 组件状态

展示型标签
默认主要警告危险成功
默认主要警告危险成功
默认主要警告危险成功
默认主要警告危险成功

03 组件尺寸

加大尺寸大尺寸中尺寸小尺寸
加大尺寸大尺寸中尺寸小尺寸
"`; exports[`ssr snapshot test > ssr test src/tag/_example/size.tsx 1`] = `"
加大尺寸大尺寸中尺寸小尺寸
加大尺寸大尺寸中尺寸小尺寸
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index 06b96a04c..88bb89277 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -84,21 +84,23 @@ exports[`ssr snapshot test > ssr test src/calendar/_example/switch-mode.tsx 1`] exports[`ssr snapshot test > ssr test src/calendar/_example/without-popup.tsx 1`] = `"
请选择日期
日
一
二
三
四
五
六
2020 年 12 月
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2021 年 1 月
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2021 年 2 月
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2021 年 3 月
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2021 年 4 月
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2021 年 5 月
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2021 年 6 月
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"`; -exports[`ssr snapshot test > ssr test src/cascader/_example/base.tsx 1`] = `"
地址
请选择地址
"`; +exports[`ssr snapshot test > ssr test src/cascader/_example/base.tsx 1`] = `"
地址
请选择地址
"`; -exports[`ssr snapshot test > ssr test src/cascader/_example/check-strictly.tsx 1`] = `"
地址
请选择地址
"`; +exports[`ssr snapshot test > ssr test src/cascader/_example/check-strictly.tsx 1`] = `"
地址
请选择地址
"`; -exports[`ssr snapshot test > ssr test src/cascader/_example/index.tsx 1`] = `"

Cascader 级联选择器

用于多层级数据的逐级选择。

01 组件类型

垂直级联选择器

地址
请选择地址

水平级联选择器

地址
请选择地址

02 进阶

带初始值

地址
请选择地址

自定义 keys

地址
请选择地址

使用次级标题

地址
请选择地址

异步加载

地址
请选择地址

选择任意一项

地址
请选择地址
"`; +exports[`ssr snapshot test > ssr test src/cascader/_example/filterable.tsx 1`] = `"
地址
请选择地址
"`; -exports[`ssr snapshot test > ssr test src/cascader/_example/keys.tsx 1`] = `"
地址
请选择地址
"`; +exports[`ssr snapshot test > ssr test src/cascader/_example/index.tsx 1`] = `"

Cascader 级联选择器

用于多层级数据的逐级选择。

01 组件类型

垂直级联选择器

地址
请选择地址

水平级联选择器

地址
请选择地址

02 进阶

带初始值

地址
请选择地址

自定义 keys

地址
请选择地址

使用次级标题

地址
请选择地址

异步加载

地址
请选择地址

选择任意一项

地址
请选择地址

支持搜索

地址
请选择地址
"`; -exports[`ssr snapshot test > ssr test src/cascader/_example/lazy.tsx 1`] = `"
地址
请选择地址
"`; +exports[`ssr snapshot test > ssr test src/cascader/_example/keys.tsx 1`] = `"
地址
请选择地址
"`; -exports[`ssr snapshot test > ssr test src/cascader/_example/theme-tab.tsx 1`] = `"
地址
请选择地址
"`; +exports[`ssr snapshot test > ssr test src/cascader/_example/lazy.tsx 1`] = `"
地址
请选择地址
"`; -exports[`ssr snapshot test > ssr test src/cascader/_example/with-title.tsx 1`] = `"
地址
请选择地址
"`; +exports[`ssr snapshot test > ssr test src/cascader/_example/theme-tab.tsx 1`] = `"
地址
请选择地址
"`; -exports[`ssr snapshot test > ssr test src/cascader/_example/with-value.tsx 1`] = `"
地址
请选择地址
"`; +exports[`ssr snapshot test > ssr test src/cascader/_example/with-title.tsx 1`] = `"
地址
请选择地址
"`; + +exports[`ssr snapshot test > ssr test src/cascader/_example/with-value.tsx 1`] = `"
地址
请选择地址
"`; exports[`ssr snapshot test > ssr test src/cell/_example/group.tsx 1`] = `"
单行标题
单行标题
单行标题
"`; @@ -108,21 +110,21 @@ exports[`ssr snapshot test > ssr test src/cell/_example/multiple.tsx 1`] = `"
  • ssr test src/cell/_example/single.tsx 1`] = `"
    单行标题
    单行标题 *
    单行标题
    16
    单行标题
    单行标题
    辅助信息
    单行标题
    "`; -exports[`ssr snapshot test > ssr test src/checkbox/_example/all.tsx 1`] = `"
    全选
    多选
    多选
    多选
    单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息
    "`; +exports[`ssr snapshot test > ssr test src/checkbox/_example/all.tsx 1`] = `"
    全选
    多选
    多选
    多选
    单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息
    "`; -exports[`ssr snapshot test > ssr test src/checkbox/_example/base.tsx 1`] = `"
    多选
    多选
    多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行
    多选
    描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息
    "`; +exports[`ssr snapshot test > ssr test src/checkbox/_example/base.tsx 1`] = `"
    多选
    多选
    多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行
    多选
    描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息
    "`; -exports[`ssr snapshot test > ssr test src/checkbox/_example/card.tsx 1`] = `"
    多选
    多选
    多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行多选标题
    "`; +exports[`ssr snapshot test > ssr test src/checkbox/_example/card.tsx 1`] = `"
    多选
    多选
    多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行多选标题
    "`; -exports[`ssr snapshot test > ssr test src/checkbox/_example/horizontal.tsx 1`] = `"
    多选标题
    多选标题
    上限四字
    "`; +exports[`ssr snapshot test > ssr test src/checkbox/_example/horizontal.tsx 1`] = `"
    多选标题
    多选标题
    上限四字
    "`; -exports[`ssr snapshot test > ssr test src/checkbox/_example/index.tsx 1`] = `"

    Checkbox 多选框

    用于预设的一组选项中执行多项选择,并呈现选择结果。

    01 组件类型

    纵向多选框

    多选
    多选
    多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行
    多选
    描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息

    横向多选框

    多选标题
    多选标题
    上限四字

    带全选多选框

    全选
    多选
    多选
    多选
    单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息

    02 组件状态

    多选框状态

    选项禁用-已选
    选项禁用-默认

    03 组件样式

    勾选样式

    多选
    多选
    图片图标

    勾选显示位置

    多选
    多选

    非通栏多选样式

    多选
    多选
    多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行多选标题

    04 组件规格

    多选框尺寸规格

    多选
    描述信息描述信息描述信息描述信息描述信息
    多选
    描述信息描述信息描述信息描述信息描述信息
    多选
    描述信息描述信息描述信息描述信息描述信息
    横向卡片多选框
    多选
    多选
    多选
    "`; +exports[`ssr snapshot test > ssr test src/checkbox/_example/index.tsx 1`] = `"

    Checkbox 多选框

    用于预设的一组选项中执行多项选择,并呈现选择结果。

    01 组件类型

    纵向多选框

    多选
    多选
    多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行
    多选
    描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息

    横向多选框

    多选标题
    多选标题
    上限四字

    带全选多选框

    全选
    多选
    多选
    多选
    单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息单选描述信息

    02 组件状态

    多选框状态

    选项禁用-已选
    选项禁用-默认

    03 组件样式

    勾选样式

    多选
    多选
    图片图标

    勾选显示位置

    多选
    多选

    非通栏多选样式

    多选
    多选
    多选标题多行多选标题多行多选标题多行多选标题多行多选标题多行多选标题

    04 组件规格

    多选框尺寸规格

    多选
    描述信息描述信息描述信息描述信息描述信息
    多选
    描述信息描述信息描述信息描述信息描述信息
    多选
    描述信息描述信息描述信息描述信息描述信息
    横向卡片多选框
    多选
    多选
    多选
    "`; exports[`ssr snapshot test > ssr test src/checkbox/_example/right.tsx 1`] = `"
    多选
    多选
    "`; -exports[`ssr snapshot test > ssr test src/checkbox/_example/special.tsx 1`] = `"
    多选
    描述信息描述信息描述信息描述信息描述信息
    多选
    描述信息描述信息描述信息描述信息描述信息
    多选
    描述信息描述信息描述信息描述信息描述信息
    横向卡片多选框
    多选
    多选
    多选
    "`; +exports[`ssr snapshot test > ssr test src/checkbox/_example/special.tsx 1`] = `"
    多选
    描述信息描述信息描述信息描述信息描述信息
    多选
    描述信息描述信息描述信息描述信息描述信息
    多选
    描述信息描述信息描述信息描述信息描述信息
    横向卡片多选框
    多选
    多选
    多选
    "`; -exports[`ssr snapshot test > ssr test src/checkbox/_example/status.tsx 1`] = `"
    选项禁用-已选
    选项禁用-默认
    "`; +exports[`ssr snapshot test > ssr test src/checkbox/_example/status.tsx 1`] = `"
    选项禁用-已选
    选项禁用-默认
    "`; exports[`ssr snapshot test > ssr test src/checkbox/_example/type.tsx 1`] = `"
    多选
    多选
    图片图标
    "`; @@ -134,7 +136,7 @@ exports[`ssr snapshot test > ssr test src/collapse/_example/base.tsx 1`] = `" ssr test src/collapse/_example/card.tsx 1`] = `"
    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容
    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容
    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容
    "`; -exports[`ssr snapshot test > ssr test src/collapse/_example/defaultExpandAll.tsx 1`] = `"
    面板1
    内容1
    面板2
    内容2
    面板2
    内容2
    "`; +exports[`ssr snapshot test > ssr test src/collapse/_example/default-expand-all.tsx 1`] = `"
    面板1
    内容1
    面板2
    内容2
    面板2
    内容2
    "`; exports[`ssr snapshot test > ssr test src/collapse/_example/index.tsx 1`] = `"

    Collapse 折叠面板

    可以折叠/展开的内容区域。

    01 组件类型

    基础折叠面板

    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容

    向上展开

    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容可自定义内容

    带操作说明

    折叠面板标题
    收起
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容

    手风琴式

    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容
    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容
    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容
    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容

    02 组件样式

    卡片折叠面板

    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容
    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容
    折叠面板标题
    此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容此处可自定义内容
    "`; @@ -148,7 +150,17 @@ exports[`ssr snapshot test > ssr test src/color-picker/_example/index.tsx 1`] = exports[`ssr snapshot test > ssr test src/color-picker/_example/multiple.tsx 1`] = `"
    "`; -exports[`ssr snapshot test > ssr test src/color-picker/_example/usePopup.tsx 1`] = `"
    "`; +exports[`ssr snapshot test > ssr test src/color-picker/_example/use-popup.tsx 1`] = `"
    "`; + +exports[`ssr snapshot test > ssr test src/comment/_example/base.tsx 1`] = `"
    Name1 名称
    作者
    这是一段很长很长很长很长很长很长的评论内容。
    今天16:38·广东
    回复
    "`; + +exports[`ssr snapshot test > ssr test src/comment/_example/collapse.tsx 1`] = `"
    Name1 名称
    作者
    这是一段很长很长很长很长很长很长的评论内容。
    今天16:38·广东
    回复
    展开5条回复
    "`; + +exports[`ssr snapshot test > ssr test src/comment/_example/image.tsx 1`] = `"
    Name1 名称
    作者
    今天16:38·广东
    回复
    "`; + +exports[`ssr snapshot test > ssr test src/comment/_example/index.tsx 1`] = `"

    Comment 评论

    评论用于对页面内容的反馈、评价、讨论等,如对文章的评价,对话题的讨论等。

    01 组件类型

    基础评论

    Name1 名称
    作者
    这是一段很长很长很长很长很长很长的评论内容。
    今天16:38·广东
    回复

    图片评论

    Name1 名称
    作者
    今天16:38·广东
    回复

    文字+图片评论

    Name1 名称
    作者
    这是一段很长很长很长很长很长很长的评论内容。
    共9张
    今天16:38·广东
    回复

    02 组件样式

    展开+收起

    Name1 名称
    作者
    这是一段很长很长很长很长很长很长的评论内容。
    今天16:38·广东
    回复
    展开5条回复
    "`; + +exports[`ssr snapshot test > ssr test src/comment/_example/mixed.tsx 1`] = `"
    Name1 名称
    作者
    这是一段很长很长很长很长很长很长的评论内容。
    共9张
    今天16:38·广东
    回复
    "`; exports[`ssr snapshot test > ssr test src/config-provider/_example/index.tsx 1`] = `"

    ConfigProvider 全局特性配置

    全局特性配置包含各个组件的文本语言配置及其他通用配置,可以减少重复的通用配置。

    01 配置示例

    Upload

    Table

    Type
    Platform
    Property
    暂无数据

    其他组件

    Rating
    3 score
    Single select date
    "`; @@ -170,10 +182,12 @@ exports[`ssr snapshot test > ssr test src/date-time-picker/_example/hour-minute. exports[`ssr snapshot test > ssr test src/date-time-picker/_example/hour-minute-second.tsx 1`] = `"
    选择时间
    10:00:00
    "`; -exports[`ssr snapshot test > ssr test src/date-time-picker/_example/index.tsx 1`] = `"

    DateTimePicker 时间选择器

    用于选择一个时间点或者一个时间段。

    01 组件类型

    年月日选择器

    选择时间
    2025-07-09

    年月选择器

    选择时间
    2025-07

    时分秒选择器

    选择时间
    10:00:00

    时分选择器

    选择时间
    10:00

    年月日时分秒选择器

    选择时间
    2025-07-09 10:10:10

    年月日带星期选择器

    选择时间
    2025-07-09 Wed.

    02 组件样式

    是否带标题

    带标题时间选择器
    2025-07-09
    无标题时间选择器
    2025-07-10
    "`; +exports[`ssr snapshot test > ssr test src/date-time-picker/_example/index.tsx 1`] = `"

    DateTimePicker 时间选择器

    用于选择一个时间点或者一个时间段。

    01 组件类型

    年月日选择器

    选择时间
    2025-07-09

    年月选择器

    选择时间
    2025-07

    时分秒选择器

    选择时间
    10:00:00

    时分选择器

    选择时间
    10:00

    年月日时分秒选择器

    选择时间
    2025-07-09 10:10:10

    年月日带星期选择器

    选择时间
    2025-07-09 Wed.

    间隔时间选择器

    选择时间(间隔)
    时 分

    02 组件样式

    是否带标题

    带标题时间选择器
    2025-07-09
    无标题时间选择器
    2025-07-10
    "`; exports[`ssr snapshot test > ssr test src/date-time-picker/_example/show-week.tsx 1`] = `"
    选择时间
    2025-07-09 Wed.
    "`; +exports[`ssr snapshot test > ssr test src/date-time-picker/_example/steps.tsx 1`] = `"
    选择时间(间隔)
    时 分
    "`; + exports[`ssr snapshot test > ssr test src/date-time-picker/_example/with-title.tsx 1`] = `"
    带标题时间选择器
    2025-07-09
    "`; exports[`ssr snapshot test > ssr test src/date-time-picker/_example/without-title.tsx 1`] = `"
    无标题时间选择器
    2025-07-10
    "`; @@ -188,7 +202,7 @@ exports[`ssr snapshot test > ssr test src/dialog/_example/feedback.tsx 1`] = `"< exports[`ssr snapshot test > ssr test src/dialog/_example/image-dialog.tsx 1`] = `"
    "`; -exports[`ssr snapshot test > ssr test src/dialog/_example/index.tsx 1`] = `"

    Dialog 对话框

    用于显示重要提示或请求用户进行重要操作,一种打断当前操作的模态视图。

    01 组件类型

    反馈类对话框

    确认类对话框

    输入类对话框

    带图片的对话框

    02 组件状态

    文字按钮

    水平基础按钮
    垂直基础按钮
    带关闭按钮的对话框
    "`; +exports[`ssr snapshot test > ssr test src/dialog/_example/index.tsx 1`] = `"

    Dialog 对话框

    用于显示重要提示或请求用户进行重要操作,一种打断当前操作的模态视图。

    01 组件类型

    反馈类对话框

    确认类对话框

    输入类对话框

    带图片的对话框

    02 组件样式

    文字按钮

    水平基础按钮
    垂直基础按钮
    带关闭按钮的对话框
    "`; exports[`ssr snapshot test > ssr test src/dialog/_example/input.tsx 1`] = `"
    "`; @@ -230,9 +244,9 @@ exports[`ssr snapshot test > ssr test src/dropdown-menu/_example/single.tsx 1`] exports[`ssr snapshot test > ssr test src/empty/_example/base.tsx 1`] = `"
    描述文字
    "`; -exports[`ssr snapshot test > ssr test src/empty/_example/buttonEmpty.tsx 1`] = `"
    描述文字
    "`; +exports[`ssr snapshot test > ssr test src/empty/_example/button-empty.tsx 1`] = `"
    描述文字
    "`; -exports[`ssr snapshot test > ssr test src/empty/_example/imageEmpty.tsx 1`] = `"
    描述文字
    "`; +exports[`ssr snapshot test > ssr test src/empty/_example/image-empty.tsx 1`] = `"
    描述文字
    "`; exports[`ssr snapshot test > ssr test src/empty/_example/index.tsx 1`] = `"

    Empty 空状态

    用于空状态时的占位提示。

    组件类型

    图标空状态

    描述文字

    自定义图片空状态

    描述文字

    带操作空状态

    描述文字
    "`; @@ -254,11 +268,11 @@ exports[`ssr snapshot test > ssr test src/footer/_example/links.tsx 1`] = `"
    ssr test src/footer/_example/logo.tsx 1`] = `""`; -exports[`ssr snapshot test > ssr test src/form/_example/horizontal.tsx 1`] = `"
    请输入用户名
    男
    女
    保密
    0/50
    "`; +exports[`ssr snapshot test > ssr test src/form/_example/horizontal.tsx 1`] = `"
    输入用户名
    男
    女
    保密
    0/50
    "`; -exports[`ssr snapshot test > ssr test src/form/_example/index.tsx 1`] = `"

    Form 表单

    用以收集、校验和提交数据,一般由输入框、单选框、复选框、选择器等控件组成。

    01 组件类型

    基础表单

    禁用态
    请输入用户名
    男
    女
    保密
    0/50
    "`; +exports[`ssr snapshot test > ssr test src/form/_example/index.tsx 1`] = `"

    Form 表单

    用以收集、校验和提交数据,一般由输入框、单选框、复选框、选择器等控件组成。

    01 组件类型

    基础表单

    禁用态
    输入用户名
    男
    女
    保密
    0/50
    "`; -exports[`ssr snapshot test > ssr test src/form/_example/vertical.tsx 1`] = `"
    请输入用户名
    男
    女
    保密
    0/50
    "`; +exports[`ssr snapshot test > ssr test src/form/_example/vertical.tsx 1`] = `"
    输入用户名
    男
    女
    保密
    0/50
    "`; exports[`ssr snapshot test > ssr test src/grid/_example/badge.tsx 1`] = `"
    标题文字
    8
    标题文字
    13
    标题五字内
    NEW
    标题五字内
    "`; @@ -338,17 +352,17 @@ exports[`ssr snapshot test > ssr test src/input/_example/bordered.tsx 1`] = `" ssr test src/input/_example/custom.tsx 1`] = `"
    标签文字
    "`; -exports[`ssr snapshot test > ssr test src/input/_example/index.tsx 1`] = `"

    Input 输入框

    用于单行文本信息输入。

    01 组件类型

    基础输入框

    标签文字
    标签文字

    带字数限制输入框

    标签文字
    最大输入10个字符
    标签文字
    最大输入10个字符,汉字算两个

    带操作输入框

    标签文字
    标签文字
    标签文字

    带图标输入框

    标签文字

    特定类型输入框

    输入密码
    验证码
    手机号
    发送验证码
    价格
    元
    数量
    个

    02 组件状态

    输入框状态

    标签文字
    辅助说明
    不可编辑

    信息超长状态

    标签超长时最多十个字

    03 组件样式

    内容位置

    左对齐
    居中
    右对齐

    竖排样式

    标签文字

    非通栏样式

    标签文字

    标签外置样式

    标签文字

    自定义样式输入框

    标签文字
    "`; +exports[`ssr snapshot test > ssr test src/input/_example/index.tsx 1`] = `"

    Input 输入框

    用于单行文本信息输入。

    01 组件类型

    基础输入框

    标签文字
    标签文字

    带字数限制输入框

    标签文字
    最大输入10个字符
    标签文字
    最大输入10个字符,汉字算两个

    带操作输入框

    标签文字
    标签文字
    标签文字

    带图标输入框

    标签文字

    特定类型输入框

    输入密码
    验证码
    手机号
    发送验证码
    价格
    元
    数量
    个

    02 组件状态

    输入框状态

    标签文字
    辅助说明
    不可编辑

    信息超长状态

    标签超长时最多十个字

    03 组件样式

    内容位置

    左对齐
    居中
    右对齐

    竖排样式

    标签文字

    非通栏样式

    标签文字

    标签外置样式

    标签文字

    自定义样式输入框

    标签文字
    "`; exports[`ssr snapshot test > ssr test src/input/_example/label.tsx 1`] = `"
    标签超长时最多十个字
    "`; exports[`ssr snapshot test > ssr test src/input/_example/layout.tsx 1`] = `"
    标签文字
    "`; -exports[`ssr snapshot test > ssr test src/input/_example/maxLength.tsx 1`] = `"
    标签文字
    最大输入10个字符
    标签文字
    最大输入10个字符,汉字算两个
    "`; +exports[`ssr snapshot test > ssr test src/input/_example/maxlength.tsx 1`] = `"
    标签文字
    最大输入10个字符
    标签文字
    最大输入10个字符,汉字算两个
    "`; exports[`ssr snapshot test > ssr test src/input/_example/prefix.tsx 1`] = `"
    标签文字
    "`; -exports[`ssr snapshot test > ssr test src/input/_example/special.tsx 1`] = `"
    输入密码
    验证码
    手机号
    发送验证码
    价格
    元
    数量
    个
    "`; +exports[`ssr snapshot test > ssr test src/input/_example/special.tsx 1`] = `"
    输入密码
    验证码
    手机号
    发送验证码
    价格
    元
    数量
    个
    "`; exports[`ssr snapshot test > ssr test src/input/_example/status.tsx 1`] = `"
    标签文字
    辅助说明
    不可编辑
    "`; @@ -364,7 +378,7 @@ exports[`ssr snapshot test > ssr test src/link/_example/base.tsx 1`] = `"
    ssr test src/link/_example/index.tsx 1`] = `"

    Link 链接

    文字超链接用于跳转一个新页面,如当前项目跳转,友情链接等。

    01 组件类型

    基础文字链接

    下划线文字链接

    前置图标文字链接

    后置图标文字链接

    03 组件样式

    链接尺寸

    "`; -exports[`ssr snapshot test > ssr test src/link/_example/linkSize.tsx 1`] = `""`; +exports[`ssr snapshot test > ssr test src/link/_example/link-size.tsx 1`] = `""`; exports[`ssr snapshot test > ssr test src/link/_example/prefix.tsx 1`] = `""`; @@ -420,7 +434,7 @@ exports[`ssr snapshot test > ssr test src/navbar/_example/custom-color.tsx 1`] = exports[`ssr snapshot test > ssr test src/navbar/_example/img.tsx 1`] = `"
    "`; -exports[`ssr snapshot test > ssr test src/navbar/_example/index.tsx 1`] = `"
    Navbar 导航条

    NavBar 导航栏

    用于不同页面之间切换或者跳转,位于内容区的上方,系统状态栏的下方。

    01 组件类型

    基础导航栏

    标题文字
    标题文字
    标题文字

    带搜索导航栏

    带图片导航栏

    02 组件样式

    标题对齐

    标题居中
    标题左对齐

    标题尺寸

    标题文字
    返回
    大尺寸标题

    自定义颜色

    标题文字
    "`; +exports[`ssr snapshot test > ssr test src/navbar/_example/index.tsx 1`] = `"
    Navbar 导航条

    Navbar 导航栏

    用于不同页面之间切换或者跳转,位于内容区的上方,系统状态栏的下方。

    01 组件类型

    基础导航栏

    标题文字
    标题文字
    标题文字

    带搜索导航栏

    带图片导航栏

    02 组件样式

    标题对齐

    标题居中
    标题左对齐

    标题尺寸

    标题文字
    返回
    大尺寸标题

    自定义颜色

    标题文字
    "`; exports[`ssr snapshot test > ssr test src/navbar/_example/left-title.tsx 1`] = `"
    标题居中
    标题左对齐
    "`; @@ -436,13 +450,13 @@ exports[`ssr snapshot test > ssr test src/notice-bar/_example/customization.tsx exports[`ssr snapshot test > ssr test src/notice-bar/_example/event.tsx 1`] = `"
    这是一条普通的消息通知
    "`; -exports[`ssr snapshot test > ssr test src/notice-bar/_example/iconDemo.tsx 1`] = `"
    提示文字描述提示文字描述提示文字描述
    "`; +exports[`ssr snapshot test > ssr test src/notice-bar/_example/icon-demo.tsx 1`] = `"
    提示文字描述提示文字描述提示文字描述
    "`; exports[`ssr snapshot test > ssr test src/notice-bar/_example/index.tsx 1`] = `"

    NoticeBar 公告栏

    在导航栏下方,用于给用户显示提示消息。

    01 组件类型

    纯文字的公告栏

    这是一条普通的通知消息

    可滚动的公告栏

    提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述
    这是一条带图标的水平滚动公告栏
    这是一条带图标的纵向滚动公告栏
    这是一条带图标的纵向滚动公告栏
    这是一条带图标的纵向滚动公告栏

    带图标的公告栏

    提示文字描述提示文字描述提示文字描述

    带关闭的公告栏

    这是一条普通的通知信息

    带入口的公告栏

    这是一条普通的消息通知

    自定义样式的公告栏

    提示文字描述提示文字描述提示文字描述

    自定义内容的公告栏

    提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述详情

    02 组件状态

    普通通知
    这是一条普通的通知信息
    成功通知
    这是一条成功的通知信息
    警示通知
    这是一条警示的通知信息
    错误通知
    这是一条错误的通知信息
    "`; exports[`ssr snapshot test > ssr test src/notice-bar/_example/scrolling.tsx 1`] = `"
    提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述
    这是一条带图标的水平滚动公告栏
    这是一条带图标的纵向滚动公告栏
    这是一条带图标的纵向滚动公告栏
    这是一条带图标的纵向滚动公告栏
    "`; -exports[`ssr snapshot test > ssr test src/notice-bar/_example/suffixIcon.tsx 1`] = `"
    这是一条普通的通知信息
    "`; +exports[`ssr snapshot test > ssr test src/notice-bar/_example/suffix-icon.tsx 1`] = `"
    这是一条普通的通知信息
    "`; exports[`ssr snapshot test > ssr test src/notice-bar/_example/theme.tsx 1`] = `"
    普通通知
    这是一条普通的通知信息
    成功通知
    这是一条成功的通知信息
    警示通知
    这是一条警示的通知信息
    错误通知
    这是一条错误的通知信息
    "`; @@ -456,7 +470,7 @@ exports[`ssr snapshot test > ssr test src/picker/_example/base.tsx 1`] = `"
    ssr test src/picker/_example/custom-height.tsx 1`] = `"
    自定义高度选择器
    "`; -exports[`ssr snapshot test > ssr test src/picker/_example/index.tsx 1`] = `"

    Picker 选择器

    用于一组预设数据中的选择。

    01 组件类型

    基础选择器

    选择地区
    选择季节
    选择地区

    02 组件样式

    是否带标题

    带标题选择器
    无标题选择器
    自定义高度选择器
    "`; +exports[`ssr snapshot test > ssr test src/picker/_example/index.tsx 1`] = `"

    Picker 选择器

    用于一组预设数据中的选择。

    01 组件类型

    基础选择器

    选择地区
    选择季节
    选择地区

    02 组件样式

    是否带标题

    带标题选择器
    无标题选择器
    自定义高度选择器
    "`; exports[`ssr snapshot test > ssr test src/picker/_example/with-title.tsx 1`] = `"
    带标题选择器
    无标题选择器
    "`; @@ -504,9 +518,11 @@ exports[`ssr snapshot test > ssr test src/qrcode/_example/borderless.tsx 1`] = ` exports[`ssr snapshot test > ssr test src/qrcode/_example/color.tsx 1`] = `"

    二维码背景颜色

    "`; +exports[`ssr snapshot test > ssr test src/qrcode/_example/download.tsx 1`] = `"
    "`; + exports[`ssr snapshot test > ssr test src/qrcode/_example/icon.tsx 1`] = `"
    "`; -exports[`ssr snapshot test > ssr test src/qrcode/_example/index.tsx 1`] = `"

    QRCode 二维码

    能够将文本转换生成二维码的组件,支持自定义配色和 Logo 配置。

    01 组件类型

    基本用法

    带 Icon 的二维码

    无边框的二维码

    二维码纠错等级

    02 组件状态

    active

    expired

    二维码过期

    点击刷新

    loading

    scanned

    已扫描

    03 组件样式

    二维码颜色

    二维码背景颜色

    二维码尺寸

    "`; +exports[`ssr snapshot test > ssr test src/qrcode/_example/index.tsx 1`] = `"

    QRCode 二维码

    能够将文本转换生成二维码的组件,支持自定义配色和 Logo 配置。

    01 组件类型

    基本用法

    指定二维码类型

    带 Icon 的二维码

    无边框的二维码

    二维码纠错等级

    02 组件状态

    active

    expired

    二维码过期

    点击刷新

    loading

    scanned

    已扫描

    自定义状态渲染

    加载中...

    二维码过期

    点击刷新

    已扫描

    03 组件样式

    二维码颜色

    二维码背景颜色

    二维码尺寸

    04 进阶

    下载二维码

    "`; exports[`ssr snapshot test > ssr test src/qrcode/_example/level.tsx 1`] = `"
    "`; @@ -514,17 +530,21 @@ exports[`ssr snapshot test > ssr test src/qrcode/_example/size.tsx 1`] = `"
    ssr test src/qrcode/_example/status.tsx 1`] = `"

    expired

    二维码过期

    点击刷新

    loading

    scanned

    已扫描

    "`; -exports[`ssr snapshot test > ssr test src/radio/_example/base.tsx 1`] = `"
    单选
    单选
    单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
    单选
    描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息
    "`; +exports[`ssr snapshot test > ssr test src/qrcode/_example/status-render.tsx 1`] = `"

    加载中...

    二维码过期

    点击刷新

    已扫描

    "`; + +exports[`ssr snapshot test > ssr test src/qrcode/_example/type.tsx 1`] = `"
    "`; -exports[`ssr snapshot test > ssr test src/radio/_example/card.tsx 1`] = `"
    单选
    单选
    单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
    "`; +exports[`ssr snapshot test > ssr test src/radio/_example/base.tsx 1`] = `"
    单选
    单选
    单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
    单选
    描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息
    "`; -exports[`ssr snapshot test > ssr test src/radio/_example/custom.tsx 1`] = `"

    纵向卡片单选框

    单选
    描述信息描述信息描述信息描述信息描述信息
    单选
    描述信息描述信息描述信息描述信息描述信息
    单选
    描述信息描述信息描述信息描述信息描述信息

    横向卡片单选框

    单选
    单选
    单选
    "`; +exports[`ssr snapshot test > ssr test src/radio/_example/card.tsx 1`] = `"
    单选
    单选
    单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
    "`; -exports[`ssr snapshot test > ssr test src/radio/_example/horizontal.tsx 1`] = `"
    单选标题
    单选标题
    上限四字
    "`; +exports[`ssr snapshot test > ssr test src/radio/_example/custom.tsx 1`] = `"

    纵向卡片单选框

    单选
    描述信息描述信息描述信息描述信息描述信息
    单选
    描述信息描述信息描述信息描述信息描述信息
    单选
    描述信息描述信息描述信息描述信息描述信息

    横向卡片单选框

    单选
    单选
    单选
    "`; + +exports[`ssr snapshot test > ssr test src/radio/_example/horizontal.tsx 1`] = `"
    单选标题
    单选标题
    上限四字
    "`; exports[`ssr snapshot test > ssr test src/radio/_example/icon.tsx 1`] = `"
    单选
    单选
    "`; -exports[`ssr snapshot test > ssr test src/radio/_example/index.tsx 1`] = `"

    Radio 单选框

    用于在预设的一组选项中执行单项选择,并呈现选择结果。

    01 组件类型

    纵向单选框

    单选
    单选
    单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
    单选
    描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息

    横向单选框

    单选标题
    单选标题
    上限四字

    02 组件状态

    单选框状态

    选项禁用-已选
    选项禁用-默认

    03 组件样式

    勾选样式

    单选
    单选

    勾选显示位置

    单选
    单选

    非通栏单选样式

    单选
    单选
    单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行

    04 特殊样式

    纵向卡片单选框

    单选
    描述信息描述信息描述信息描述信息描述信息
    单选
    描述信息描述信息描述信息描述信息描述信息
    单选
    描述信息描述信息描述信息描述信息描述信息

    横向卡片单选框

    单选
    单选
    单选
    "`; +exports[`ssr snapshot test > ssr test src/radio/_example/index.tsx 1`] = `"

    Radio 单选框

    用于在预设的一组选项中执行单项选择,并呈现选择结果。

    01 组件类型

    纵向单选框

    单选
    单选
    单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行
    单选
    描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息描述信息

    横向单选框

    单选标题
    单选标题
    上限四字

    02 组件状态

    单选框状态

    选项禁用-已选
    选项禁用-默认

    03 组件样式

    勾选样式

    单选
    单选

    勾选显示位置

    单选
    单选

    非通栏单选样式

    单选
    单选
    单选标题多行单选标题多行单选标题多行单选标题多行单选标题多行

    04 特殊样式

    纵向卡片单选框

    单选
    描述信息描述信息描述信息描述信息描述信息
    单选
    描述信息描述信息描述信息描述信息描述信息
    单选
    描述信息描述信息描述信息描述信息描述信息

    横向卡片单选框

    单选
    单选
    单选
    "`; exports[`ssr snapshot test > ssr test src/radio/_example/placement.tsx 1`] = `"
    单选
    单选
    "`; @@ -604,7 +624,7 @@ exports[`ssr snapshot test > ssr test src/slider/_example/capsule.tsx 1`] = `" ssr test src/slider/_example/disabled.tsx 1`] = `"
    "`; -exports[`ssr snapshot test > ssr test src/slider/_example/index.tsx 1`] = `"

    Slider 滑动选择器

    用于选择横轴上的数值、区间、档位。

    01 组件类型

    单游标滑块

    双游标滑块

    带数值滑动选择器

    10
    0
    0
    0
    100

    带刻度滑动选择器

    02 组件状态

    滑块禁用状态

    03 特殊样式

    胶囊型滑块

    0%
    0%
    "`; +exports[`ssr snapshot test > ssr test src/slider/_example/index.tsx 1`] = `"

    Slider 滑动选择器

    用于选择横轴上的数值、区间、档位。

    01 组件类型

    单游标滑块

    双游标滑块

    带数值滑动选择器

    10
    0
    0
    0
    100

    带刻度滑动选择器

    02 组件状态

    滑块禁用状态

    03 特殊样式

    胶囊型滑块

    0%
    0%

    垂直滑块

    23
    23
    "`; exports[`ssr snapshot test > ssr test src/slider/_example/label.tsx 1`] = `"
    10
    0
    0
    0
    100
    "`; @@ -612,6 +632,8 @@ exports[`ssr snapshot test > ssr test src/slider/_example/range.tsx 1`] = `"
    ssr test src/slider/_example/step.tsx 1`] = `"
    "`; +exports[`ssr snapshot test > ssr test src/slider/_example/vertical.tsx 1`] = `"
    23
    23
    "`; + exports[`ssr snapshot test > ssr test src/stepper/_example/base.tsx 1`] = `"
    "`; exports[`ssr snapshot test > ssr test src/stepper/_example/index.tsx 1`] = `"

    Stepper 步进器

    用于数量的增减。

    01 组件类型

    基础步进器

    02 组件状态

    最大最小状态

    禁用状态

    03 组件样式

    步进器样式

    步进器尺寸

    "`; @@ -644,11 +666,11 @@ exports[`ssr snapshot test > ssr test src/sticky/_example/offset.tsx 1`] = `" ssr test src/swipe-cell/_example/double.tsx 1`] = `"

    左右滑操作

    删除
    左右滑操作
    辅助信息
    收藏
    删除
    "`; -exports[`ssr snapshot test > ssr test src/swipe-cell/_example/event.tsx 1`] = `"

    带二次确认的操作

    编辑
    删除
    带二次确认的操作
    辅助信息
    编辑
    删除
    "`; +exports[`ssr snapshot test > ssr test src/swipe-cell/_example/event.tsx 1`] = `"

    带二次确认的操作

    编辑
    删除
    带二次确认的操作
    辅助信息
    编辑
    删除
    "`; exports[`ssr snapshot test > ssr test src/swipe-cell/_example/icon.tsx 1`] = `"

    带图标的滑动操作

    图标加文字横排
    辅助信息
    编辑
    删除
    纯图标
    辅助信息
    图标加文字竖排
    一段很长很长的内容文字
    辅助信息
    编辑
    删除
    "`; -exports[`ssr snapshot test > ssr test src/swipe-cell/_example/index.tsx 1`] = `"

    SwipeCell 滑动操作

    用于承载列表中的更多操作,通过左右滑动来展示,按钮的宽度固定高度根据列表高度而变化。

    左滑单操作

    左滑单操作
    辅助信息
    删除
    左滑单操作
    一段很长很长的内容文字
    辅助信息
    删除
    左滑双操作
    辅助信息
    收藏
    编辑
    左滑三操作
    辅助信息
    收藏
    编辑
    删除

    右滑单操作

    选择
    右滑单操作
    辅助信息

    左右滑操作

    删除
    左右滑操作
    辅助信息
    收藏
    删除

    带图标的滑动操作

    图标加文字横排
    辅助信息
    编辑
    删除
    纯图标
    辅助信息
    图标加文字竖排
    一段很长很长的内容文字
    辅助信息
    编辑
    删除

    带二次确认的操作

    编辑
    删除
    带二次确认的操作
    辅助信息
    编辑
    删除
    "`; +exports[`ssr snapshot test > ssr test src/swipe-cell/_example/index.tsx 1`] = `"

    SwipeCell 滑动操作

    用于承载列表中的更多操作,通过左右滑动来展示,按钮的宽度固定高度根据列表高度而变化。

    01 组件类型

    左滑单操作

    左滑单操作
    辅助信息
    删除
    左滑单操作
    一段很长很长的内容文字
    辅助信息
    删除
    左滑双操作
    辅助信息
    收藏
    编辑
    左滑三操作
    辅助信息
    收藏
    编辑
    删除

    右滑单操作

    选择
    右滑单操作
    辅助信息

    左右滑操作

    删除
    左右滑操作
    辅助信息
    收藏
    删除

    带图标的滑动操作

    图标加文字横排
    辅助信息
    编辑
    删除
    纯图标
    辅助信息
    图标加文字竖排
    一段很长很长的内容文字
    辅助信息
    编辑
    删除

    带二次确认的操作

    编辑
    删除
    带二次确认的操作
    辅助信息
    编辑
    删除
    "`; exports[`ssr snapshot test > ssr test src/swipe-cell/_example/left.tsx 1`] = `"

    左滑单操作

    左滑单操作
    辅助信息
    删除
    左滑单操作
    一段很长很长的内容文字
    辅助信息
    删除
    左滑双操作
    辅助信息
    收藏
    编辑
    左滑三操作
    辅助信息
    收藏
    编辑
    删除
    "`; @@ -752,7 +774,7 @@ exports[`ssr snapshot test > ssr test src/tag/_example/checkable.tsx 1`] = `" ssr test src/tag/_example/closable.tsx 1`] = `"
    可关闭标签
    文字标签文字标签
    "`; -exports[`ssr snapshot test > ssr test src/tag/_example/index.tsx 1`] = `"

    Tag 标签

    用于表明主体的类目,属性或状态。

    01 组件类型

    基础标签
    标签文字标签文字
    圆弧标签
    标签文字标签文字标签文字
    带图标的标签
    标签文字标签文字
    超长文本省略标签
    听说超长可以省略听说超长
    可关闭标签
    文字标签文字标签
    可选中的标签
    light
    未选中态已选中态
    dark
    未选中态已选中态
    outline
    未选中态已选中态
    light-outline
    未选中态已选中态

    02 组件状态

    展示型标签
    默认主要警告危险成功
    默认主要警告危险成功
    默认主要警告危险成功
    默认主要警告危险成功

    03 组件尺寸

    加大尺寸大尺寸中尺寸小尺寸
    加大尺寸大尺寸中尺寸小尺寸
    "`; +exports[`ssr snapshot test > ssr test src/tag/_example/index.tsx 1`] = `"

    Tag 标签

    用于表明主体的类目,属性或状态。

    01 组件类型

    基础标签
    标签文字标签文字
    圆弧标签
    标签文字标签文字标签文字
    带图标的标签
    标签文字标签文字
    超长文本省略标签
    听说超长可以省略听说超长
    可关闭标签
    文字标签文字标签
    可选中的标签
    light
    未选中态已选中态
    dark
    未选中态已选中态
    outline
    未选中态已选中态
    light-outline
    未选中态已选中态

    02 组件状态

    展示型标签
    默认主要警告危险成功
    默认主要警告危险成功
    默认主要警告危险成功
    默认主要警告危险成功

    03 组件尺寸

    加大尺寸大尺寸中尺寸小尺寸
    加大尺寸大尺寸中尺寸小尺寸
    "`; exports[`ssr snapshot test > ssr test src/tag/_example/size.tsx 1`] = `"
    加大尺寸大尺寸中尺寸小尺寸
    加大尺寸大尺寸中尺寸小尺寸
    "`; diff --git a/tsconfig.build.json b/tsconfig.build.json index 94446d9ce..077da8962 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig", - "include": ["./src", "./globals.d.ts"], + "include": ["./src"], "exclude": [ "**/**/__tests__/*", "**/**/_example/*", diff --git a/tsconfig.json b/tsconfig.json index 00432d3dd..ea46760ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,7 @@ "skipLibCheck": true, "types": ["node", "vitest/globals"] }, - "include": ["./src", "./global.d.ts"], + "include": ["./src"], "exclude": ["node_modules", "src/_common", "dist", "lib", "cjs", "esm", "es", "site", "**/*.jsx"], "compileOnSave": false }