From e92a880c1bada90b79ad540585592edd8c4b0e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?w=C5=AB=20y=C4=81ng?= Date: Fri, 15 Dec 2023 18:33:37 +0800 Subject: [PATCH] chore: release 1.8.3-naruto (#2989) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: radio/checkbox选中判断改回正则 (#2927) * fix(table): row-edit event not trigger (#2934) * fix(statistic): fix with missing default precision (#2933) * fix(radio): 选项内容变化后样式问题修复 (#2936) fix #2930 * feat: cascader value-display (#2938) * feat: 增加 cascader 对 valueDisplay 的支持 * test: update snap * fix(pagination): when total is 0 and pageSize change, current value is 0 (#2937) * fix(tree): tree 节点禁用状态逻辑改进 (#2935) * test(tree): 完善 tree 组件的禁用示例 * fix(tree): tree 组件, 禁用状态过滤函数不再仅生效于UI层,将传递到模型侧参与状态计算 * fix(tree): tree 组件完善禁用逻辑 * fix(tree): tree 组件添加 refresh 方法刷新节点状态 * test(tree): tree 组件,完善 disabled 相关示例 * fix(tree): tree 组件,完善半选状态下的状态切换 * test(tree): tree 组件,完善操作示例,提供 disabled 状态与其他状态的整合演示 * chore(tree): tree 组件,更新 common 依赖 * test(tree): tree 组件,添加禁用状态相关测试 * chore(tree): tree 组件,更新 common 依赖,补充文档 * test(tree): tree 组件,update snapshot * fix(auto-complete): using lodash escapeRegExp transform keyword text (#2943) * fix(auto-complete): using lodash escapeRegExp transform keyword text * docs(auto-complete): update auto-complete custom filter example * chore: update contributing (#2950) * chore: update contributing * chore: update docs * fix(tree): tree 组件, value, active, expanded 属性, 支持数组操作触发视图变更 (#2951) * fix(tree): tree 组件, value, active, expanded 属性, 支持数组操作触发视图变更 * refactor(tree): tree 组件,改进属性监听代码结构 * fix(tree): tree 组件,解决 value 传递 undefined,可能导致视图操作报错的问题 * ci: add workflow for issue label (#2953) * fix(table): pagination (#2954) * fix(image-viewer): 修改键盘事件绑定对象 (#2958) * feat(Table): fix some pagination problems (#2962) * feat(table): pagination * feat: code save * fix(table): pagination * fix: row select and drag sort * docs(table): demo * feat(menu): close menu after clicking (#2963) * feat(tabs): scroll to better position when active tab is in middle (#2964) * chore: release 1.8.1 (#2965) * chore: release 1.8.1 * chore: changelog's changes --------- Co-authored-by: github-actions[bot] * feat(upload): uploadPastedFiles (#2966) * fix(tree): the height attribute does not work (#2968) * fix(tree): the height attribute does not work * test(tree): the height attribute does not work * chore: update publish frequency descriptions (#2969) * chore: update Publish.md * chore: fix table test * chore: update PUBLISH.md * fix(image-viewer): 滚轮缩放符合操作直觉 (#2974) 自然滚动与标准滚动对图片放大缩小的操作均符合直觉 #2825 * fix(date-range-picker): 修复12月时选择同一个月内的日期后,第一次打开面板左右月份一样的问题 (#2972) * fix(drawer): unable to close by click on escape (#2967) * chore: fix ssr error (#2985) * chore: release 1.8.2 (#2986) * chore: release 1.8.2 * chore: changelog's changes --------- Co-authored-by: github-actions[bot] * chore: release 1.8.3 (#2988) * chore: release 1.8.3 * chore: release 1.8.3 * chore: release 1.8.3 * chore: release 1.8.3-naruto --------- Co-authored-by: liweijie0812 <674416404@qq.com> Co-authored-by: sheepluo Co-authored-by: 李江辰 Co-authored-by: hkaikai <617760820@qq.com> Co-authored-by: PY Co-authored-by: betavs <34408516+betavs@users.noreply.github.com> Co-authored-by: TabSpace Co-authored-by: kang Co-authored-by: Y Co-authored-by: sinbadmaster <40019023+sinbadmaster@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Lyan-u <46185702+Lyan-u@users.noreply.github.com> --- .github/issue-shoot.md | 7 + .github/workflows/issue-label.yml | 50 + CHANGELOG.md | 32 + CONTRIBUTING.md | 131 +- PUBLISH.md | 48 +- package.json | 2 +- src/_common | 2 +- src/auto-complete/_example/filter.vue | 4 +- src/auto-complete/highlight-option.tsx | 3 +- src/auto-complete/option-list.tsx | 3 +- src/cascader/_example/value-display.vue | 85 + src/cascader/cascader.tsx | 25 +- src/cascader/hooks.ts | 11 + src/checkbox/hooks/useKeyboardEvent.ts | 4 +- src/common.ts | 4 +- src/date-picker/DateRangePicker.tsx | 2 +- src/drawer/drawer.tsx | 4 +- src/dropdown/dropdown-menu.tsx | 24 +- src/image-viewer/base/ImageItem.tsx | 4 +- src/image-viewer/image-viewer.tsx | 33 +- src/image/props.ts | 2 +- src/menu/menu-item.tsx | 1 + src/pagination/pagination.tsx | 2 +- src/radio/group.tsx | 27 +- src/statistic/statistic.tsx | 28 +- src/table/__tests__/column.test.jsx | 6 +- .../__tests__/pagination/controlled.test.jsx | 173 ++ src/table/__tests__/pagination/mount.jsx | 323 ++ .../pagination/uncontrolled.test.jsx | 114 + .../serial-number/controlled.test.jsx | 106 + src/table/_example/drag-sort.vue | 10 +- src/table/_example/pagination.vue | 16 +- src/table/base-table.tsx | 6 +- src/table/hooks/useDragSort.ts | 16 +- src/table/hooks/usePagination.tsx | 119 +- src/table/hooks/useRowSelect.tsx | 6 +- src/table/primary-table.tsx | 24 +- src/tabs/_example/combination.vue | 4 +- src/tabs/tab-nav.tsx | 10 +- src/tree/__tests__/activable.test.jsx | 45 + src/tree/__tests__/adapt.js | 2 +- src/tree/__tests__/checkable.test.jsx | 35 + src/tree/__tests__/disabled.test.jsx | 391 +++ src/tree/__tests__/expand.test.jsx | 53 + src/tree/_example/disabled.vue | 141 +- src/tree/_example/operations.vue | 23 + src/tree/hooks/useRenderLabel.tsx | 7 +- src/tree/hooks/useTreeAction.ts | 12 +- src/tree/hooks/useTreeStore.ts | 44 +- src/tree/hooks/useTreeStyles.ts | 1 + src/tree/tree.en-US.md | 3 +- src/tree/tree.md | 3 +- src/tree/tree.tsx | 4 + src/tree/type.ts | 4 + src/upload/hooks/useUpload.ts | 3 +- src/upload/props.ts | 5 +- src/upload/type.ts | 2 +- src/upload/upload.en-US.md | 2 +- src/upload/upload.md | 2 +- src/upload/upload.tsx | 15 +- test/snap/__snapshots__/csr.test.js.snap | 2610 ++++++++++------- test/snap/__snapshots__/ssr.test.js.snap | 24 +- vitest.config.js | 2 +- 63 files changed, 3447 insertions(+), 1457 deletions(-) create mode 100644 .github/issue-shoot.md create mode 100644 .github/workflows/issue-label.yml create mode 100644 src/cascader/_example/value-display.vue create mode 100644 src/table/__tests__/pagination/controlled.test.jsx create mode 100644 src/table/__tests__/pagination/mount.jsx create mode 100644 src/table/__tests__/pagination/uncontrolled.test.jsx create mode 100644 src/table/__tests__/serial-number/controlled.test.jsx create mode 100644 src/tree/__tests__/disabled.test.jsx diff --git a/.github/issue-shoot.md b/.github/issue-shoot.md new file mode 100644 index 000000000..ec7496d35 --- /dev/null +++ b/.github/issue-shoot.md @@ -0,0 +1,7 @@ +## IssueShoot +- 预估时长: {{ .duration }} +- 期望完成时间: {{ .deadline }} +- 开发难度: {{ .level }} +- 参与人数: 1 +- 验收标准: 实现期望改造效果,提 PR 并通过验收无误 +- 备注: 最终激励以实际提交 `pull request` 并合并为准 diff --git a/.github/workflows/issue-label.yml b/.github/workflows/issue-label.yml new file mode 100644 index 000000000..ec009a0c0 --- /dev/null +++ b/.github/workflows/issue-label.yml @@ -0,0 +1,50 @@ +name: issue on label +on: + issues: + types: ['labeled'] +jobs: + add-issueshoot-template: + runs-on: ubuntu-latest + if: contains(fromJSON('["easy", "middle", "hard"]'), github.event.label.name) + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Get token + id: token + run: | + label=${{ github.event.label.name }} + if [[ $label = "easy" ]] + then + echo "level=低" >> $GITHUB_OUTPUT + echo "duration=1" >> $GITHUB_OUTPUT + deadline=$(date -d "+3 days" +'%Y-%m-%d') + echo "deadline=${deadline}" >> $GITHUB_OUTPUT + elif [[ $label = "middle" ]] + then + echo "level=中" >> $GITHUB_OUTPUT + echo "duration=3" >> $GITHUB_OUTPUT + deadline=$(date -d "+7 days" +'%Y-%m-%d') + echo "deadline=${deadline}" >> $GITHUB_OUTPUT + else + echo "level=高" >> $GITHUB_OUTPUT + echo "duration=5" >> $GITHUB_OUTPUT + deadline=$(date -d "+10 days" +'%Y-%m-%d') + echo "deadline=${deadline}" >> $GITHUB_OUTPUT + fi + - name: Create template + id: template + uses: chuhlomin/render-template@v1.4 + with: + template: .github/issue-shoot.md + vars: | + level: ${{ steps.token.outputs.level }} + duration: ${{ steps.token.outputs.duration }} + deadline: ${{ steps.token.outputs.deadline }} + - name: Update issue + uses: actions-cool/issues-helper@v3 + with: + actions: 'update-issue' + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: ${{ steps.template.outputs.result }} + update-mode: 'append' \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9987029c6..d97b692f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ toc: false docClass: timeline --- +## 🌈 1.8.3 `2023-12-15` +### 🚀 Features +- `Upload`: 新增支持 `uploadPastedFiles`,用于控制是否允许用户粘贴文件上传,默认允许 @chaishi ([#2966](https://github.com/Tencent/tdesign-vue/pull/2966)) +- Dropdown: 移除对left的item样式特殊处理 @uyarn [common#1677](https://github.com/Tencent/tdesign-common/pull/1677) +### 🐞 Bug Fixes +- `DatePicker`: 修复选择同一个月内的日期后,打开面板左右月份一样的问题 @Lyan-u ([#2972](https://github.com/Tencent/tdesign-vue/pull/2972)) +- `Drawer`: 处理点击esc无法关闭的问题 @betavs ([#2967](https://github.com/Tencent/tdesign-vue/pull/2967)) +- `ImageViewer`: 滚轮缩放符合操作直觉 @sinbadmaster ([#2974](https://github.com/Tencent/tdesign-vue/pull/2974)) +- `SSR`: 修复 `SSR` 场景使用报错的问题 @uyarn ([#2985](https://github.com/Tencent/tdesign-vue/pull/2985)) +- `Tree`: 处理 `height` 属性无效的问题 @betavs ([#2968](https://github.com/Tencent/tdesign-vue/pull/2968)) +- `Tree`: 解决初始化节点选中态异常的问题 @TabSpace ([#2985](https://github.com/Tencent/tdesign-vue/pull/2985)) +- `Upload`: 卡片式文件上传,修复取消上传时,文件依然显示的问题 [issue#2955](https://github.com/Tencent/tdesign-vue/issues/2955) @chaishi ([#2966](https://github.com/Tencent/tdesign-vue/pull/2966)) + +## 🌈 1.8.1 `2023-12-07` +### 🚀 Features +- `Cascader`: 新增 `valueDisplay` API @PengYYYYY ([#2938](https://github.com/Tencent/tdesign-vue/pull/2938)) +- `Menu`: 选中后关闭菜单,与其他组件保持交互行为一致 @uyarn ([#2963](https://github.com/Tencent/tdesign-vue/pull/2963)) +- `Tabs`: 优化初始化滚动的场景,对处于中间的部分场景进行进一步优化 @uyarn ([#2964](https://github.com/Tencent/tdesign-vue/pull/2964)) +### 🐞 Bug Fixes +- `Radio`: 选项内容变化后样式问题修复 @hkaikai ([#2936](https://github.com/Tencent/tdesign-vue/pull/2936)) +- `Pagination`: 修复当 `total` 为 0 并且 `pageSize` 改变时, `current` 值为 0 的问题 @betavs ([#2937](https://github.com/Tencent/tdesign-vue/pull/2937)) +- `Tree`: @TabSpace + - 改进节点禁用状态的逻辑 ([#2935](https://github.com/Tencent/tdesign-vue/pull/2935)) + - value、active和expanded 属性, 支持数组操作触发视图变更 ([#2951](https://github.com/Tencent/tdesign-vue/pull/2951)) +- `Table`: @chaishi + - 修复分页场景,动态切换分页数据从 undefined 到具体真实数据时,分页无效的问题 [#2867](https://github.com/Tencent/tdesign-vue/issues/2867) ([#2954](https://github.com/Tencent/tdesign-vue/pull/2954)) + - 修复分页功能在序号、行选择、行拖拽排序等场景的问题 ([#2962](https://github.com/Tencent/tdesign-vue/pull/2962)) + - 修复可编辑表格的 `row-edit` 事件没有触发的问题 ([#2934](https://github.com/Tencent/tdesign-vue/pull/2934)) + - `ImageViewer`: 修复在抽屉组件等组件中使用图片预览组件,按下 `esc` 键抽屉组件和图片预览组件会同时关闭的问题 @sinbadmaster ([#2958](https://github.com/Tencent/tdesign-vue/pull/2958)) + - `AutoComplete`: 修复匹配特殊字符报错的问题 @ZWkang ([#2943](https://github.com/Tencent/tdesign-vue/pull/2943)) + - `Dropdown`:处理禁用状态可点击的问题 @betavs ([issue #3693](https://github.com/Tencent/tdesign-vue-next/issues/3693)) + ## 🌈 1.8.0 `2023-11-23` ### 🚀 Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2e5c2f4c..0a5ec7acb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,24 +1,32 @@ # CONTRIBUTING -`tdesign-vue` 包含 `vue` 代码和一个子仓库,子仓库指向 `tdesign-common`仓库 +`tdesign-vue` 包含 `vue` 代码和一个子仓库,子仓库指向 `tdesign-common` 仓库 ## 开发 -### 安装依赖 +建议使用 node 18 版本进行开发 + +### 1.初始化子仓库 + +```bash +git submodule init && git submodule update +``` + +### 2.安装依赖 ```bash npm i ``` -### 本地开发 +### 3.本地开发 ```bash npm run start ``` -浏览器访问 +完成以上 3 个步骤,浏览器访问 即可调该框架的任何内容 -### 目录结构 +## 目录结构 ```text ├── script // 构建代码 @@ -33,7 +41,7 @@ npm run start ### 组件页路由配置 -每一个组件页,都是一个 md 文件,参考 `/site/config/index.js` 已有定义,直接按照模板添加即可 +每一个组件都有自己的路由,页面配置都是一个 markdown 文件,如`button.md`,具体路径可参考 `/site/site.config.mjs`。如果有新增组件,直接按照模板添加即可 ```js { @@ -43,21 +51,21 @@ npm run start { title: 'Button 按钮', name: 'button', - component: () => import(`@/examples/components/button/button.md`), + component: () => import(`tdesign-vue/button/button.md`), }, { title: 'Icon 图标', name: 'icon', - component: () => import(`@/examples/components/icon/icon.md`), + component: () => import(`tdesign-vue/icon/icon.md`), }, ... ], }, ``` -### Markdown 文件 demo 引用 +### Markdown 文件的 demo 引用 -文档 demo 排列与 common 仓库中的 UI demo 展示一致 +文档 demo 排列与 common 子仓库中的 UI demo 展示一致,如 button 组件页面的展示顺序,由子仓库的 docs/web/api/button.md 内的顺序决定。 ```markdown {{ base }} @@ -66,60 +74,31 @@ npm run start ### Demo 调试 -当一个 md 文件插入了很多个 demo 之后,一些组件生命周期方法调试起来会变得困难,若想对某个 demo 单独调试,可以访问路由:/demos/组件名/demo 名, - -例如: - -### 单元测试 & e2e 测试文档 - -[组件测试文档](./test/README.md) +我们可以通过打开组件的路由页进行开发调试, -## git +如 button,则打开 进行开发调试; -### 分支 +但当组件的 markdown 文件插入了很多个 demo 之后,一些组件生命周期方法调试起来会变得困难,若想对某个 demo 单独调试,可以访问路由:/demos/组件名/demo 名, -主仓库遵循使用 `git flow` 规范,新组件分支从 `develop checkout`:[https://nvie.com/posts/a-successful-git-branching-model/](https://nvie.com/posts/a-successful-git-branching-model/) +如: -如果是贡献组件,则从 `develop checkout` 分支如:`feature/button`,记得如果同时要在子仓库开发 UI,子仓库也要 `checkout` 同名分支 - -> 关于 fork - -以下内容处理 `fork` 仓库后,远端仓库的更新如何同步到 `fork` 仓库 - -```bash -# 建立 upstream remote -git remote add upstream git@github.com:Tencent/tdesign-vue.git - -# 更新 upstream -git fetch upstream develop - -# 合并 upstream develop 到本地 -git checkout develop - -git merge upstream/develop -``` - -## 提交说明 - -项目使用基于 angular 提交规范:[https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional) - -每次提交会自动触发提交验证 +### 单元测试 & e2e 测试文档 -- 使用工具 commitizen 协助规范 git commit 信息 -- fix & feat 的提交会被用来生成 changelog -- 提交会触发 git pre-commit 检查,修复提示的 eslint 错误, +[组件测试文档](./test/README.md) -## 公共子仓库 tdesign-common +## 子仓库 tdesign-common -本项目以子仓库的形式引入 `tdesign-common` 公共仓库,对应 `src/\_common` 文件夹 +TDesign 的项目都会以子仓库的形式引入 `tdesign-common` 公共仓库,对应 `src/\_common` 文件夹, 公共仓库中包含 -- 一些公共的工具函数 -- 组件库 `UI` 开发内容,既 `html` 结构和 `css` 样式(React/Vue 共用) +- 部分组件的一些框架无关的公共的工具函数 +- `组件库UI`,既 `html` 结构和 `css` 样式(多框架共用) + +大部分的功能和改动都只需要调整主仓库的代码即可,但涉及部分公共函数、样式或者部分文档的调整,需要改动子仓库的代码。 ### 初始化子仓库 -- 初次克隆代码后需要初始化子仓库: `git submodule init && git submodule update` +- 如开发部分提到的,初次克隆代码后需要初始化子仓库: `git submodule init && git submodule update` - git submodule update 之后子仓库不指向任何分支,只是一个指向某一个提交的游离状态 ### 子仓库开发 @@ -129,9 +108,10 @@ git merge upstream/develop - 先进入 `src/\_common` 文件夹,正常将样式修改添加提交 - 回到主仓库,此时应该会看到 `src/\_common` 文件夹是修改状态,按照正常步骤添加提交即可 -## 关于组件库 UI +### 组件库 UI -UI 开发(HTML & CSS)是多个框架共用的,比如 React-web/Vue-web/Vue-next web。各个框架组件实现应该要复用 UI 开发的 html 结构,引用其组件 CSS 与 Demo CSS(本仓库已在入口处引用了),UI 开发一般可由单独的 UI 开发同学认领完成或各框架组件开发同学的其中一名同学完成 +UI 是多个框架共用的,比如 PC 端的 react/vue/vue-next 都是复用子仓库的 UI 代码。 +各个框架组件实现应该要复用 UI 开发的 html 结构,引用其组件 CSS 与 Demo CSS(本仓库已在入口处引用了),UI 开发一般可由单独的 UI 开发同学认领完成或各框架组件开发同学的其中一名同学完成 - 如果开发前已有某个组件的 UI 开发内容,直接在主仓库使用即可 - 如果没有,且你也负责 UI 开发:参考 UI 开发规范完成 UI 开发内容、然后再开发主仓库组件 @@ -151,24 +131,51 @@ UI 开发(HTML & CSS)是多个框架共用的,比如 React-web/Vue-web/Vue import './button.less'; ``` -## 开发规范 +## 分支规范 + +### 分支 + +遵循使用 `git flow` 规范,新组件分支从 `develop checkout`:[https://nvie.com/posts/a-successful-git-branching-model/](https://nvie.com/posts/a-successful-git-branching-model/) -UI 开发规范参考子仓库 README [子仓库 README](https://github.com/Tencent/tdesign-common/blob/main/style/web/README.md) +如果是贡献组件,则从 `develop checkout` 分支如:`feature/button`,记得如果同时要在子仓库开发 UI,子仓库也要 `checkout` 同名分支 -### 新建组件 +> 关于 fork -```shell -npm run generate:component +以下内容处理 `fork` 仓库后,远端仓库的更新如何同步到 `fork` 仓库 + +```bash +# 建立 upstream remote +git remote add upstream git@github.com:Tencent/tdesign-vue.git + +# 更新 upstream +git fetch upstream develop + +# 合并 upstream develop 到本地 +git checkout develop + +git merge upstream/develop ``` +### 提交说明 + +项目使用基于 angular 提交规范:[https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional) + +每次提交会自动触发提交验证 + +- 使用工具 commitizen 协助规范 git commit 信息 +- fix & feat 的提交会被用来生成 changelog +- 提交会触发 git pre-commit 检查,修复提示的 eslint 错误, + +## 开发规范 + ### API 规范 -API 由 API 平台统一管理生成 https://github.com/tdesignoteam/tdesign-api +API 由 API 平台统一管理生成,如果涉及组件文档的改动(如`src/button/type.ts`的内容),都需要同时在 API 平台提交 PR,进行统一维护管理 https://github.com/tdesignoteam/tdesign-api -### 前缀 +### 前缀规范 组件和 `CSS` 前缀以 `t-` 开头,无论 `js` 还是 `css` 都使用变量定义前缀,方便后续替换 -### CSS +### CSS 规范 组件样式在 `common` 子仓库开发,遵循 [tdesign-common 仓库 UI 开发规范](https://github.com/Tencent/tdesign-common/blob/main/style/web/README.md) diff --git a/PUBLISH.md b/PUBLISH.md index e0127d94f..8b82e73de 100644 --- a/PUBLISH.md +++ b/PUBLISH.md @@ -2,50 +2,18 @@ ## 发布频率 -组件库正常每周滚动发布版本,一般在周三/周四,尽量不在周五或晚上发布,防止周末非工作时间响应不及时 +组件库正常每两周滚动发布版本,一般在周三/周四,尽量不在周五或晚上发布,防止周末非工作时间响应不及时 如果遇到用户要求紧急修复 bug,可以视情况发布 PATCH 或先行版本,判断标准: -- 影响范围大,大多数用户都可能会遇到问题:请遵照正常发布流程严格测试产物质量及整理 changelog 后发布 PATCH 版本,以使用户可以自动更新到 +- 影响范围大,大多数用户都可能会遇到问题:请遵照正常发布流程严格测试产物质量及整理 CHANGELOG 后发布 PATCH 版本,以使用户可以自动更新到 - 新上线的功能,仅有少量用户使用:可以不整理 changelog,直接发布先行版本供用户使用,如 `x.y.z-alpha` -## 版本号说明 - -版本号设置遵循 [SemVer 语义化版本控制规范 2.0.0](https://semver.org/lang/zh-CN/),一切以保证用户版本稳定性为前提,原则如下: - -- 当进行不兼容的 API 更改时,升级 MAJOR 版本 -- 当以向后兼容的方式添加功能时,升级 MINOR 版本 -- 当进行向后兼容的缺陷修复时,升级 PATCH 版本 - -目前我们还没有发布 1.0.0 版本,因此以 MINOR 作为 breaking change 时的迭代版本号 - -### 原因 - -用户项目的 package.json 文件中一般使用 `^` 或 `~` 来限制包版本: - -- `^`: 只会执行不更改最左边非零数字的更新,如果写入的是 ^0.13.0,可以更新到 0.13.1、0.13.2 等,但不能更新到 0.14.0 或更高版本。 如果写入的是 ^1.13.0,则当运行 npm update 时,可以更新到 1.13.1、1.14.0 等,但不能更新到 2.0.0 或更高版本 -- `~`: 如果写入的是 〜0.13.0,则当运行 npm update 时,会更新到补丁版本:即 0.13.1 可以,但 0.14.0 不可以。 - -参考 [使用 npm 的语义版本控制](http://nodejs.cn/learn/semantic-versioning-using-npm)、[npm/node-semver](https://github.com/npm/node-semver#caret-ranges-123-025-004) - -## 发布人职责 - -负责本次发布的同学应该 - -- review 这一迭代周期内的所有 MR 是否被正常合并,每个 MR 的描述是否准确,如果有关联的 issue,需要在 MR 评论中补充 issue 链接 -- 是否所有 issue 都得到了处理,如果已有 mr,请在 issue 中评论 mr 链接(目前工蜂还不能像 GitHub 一样在 issue 中自动显示关联 mr) -- 根据 MR 和 issue 整理 changelog (可以使用 [publish-cli](https://github.com/Tencent/tdesign-starter-cli/tree/main/packages/publish-cli) 帮助生成) -- 如果发布了 Breaking Change 版本,应该把上一个 MAJOR 版本的版本号更新至官网历史版本处,以支持历史版本官网供用户查看。 - ## 发布流程 -- 从 `develop` 新建 `docs/x.y.z-changelog` 分支,整理 changelog 并 push 分支到远端 -- changelog 分支链接发到群里召唤小伙伴们一起 review -- review 无误,`squash merge` 到 develop,保持只有一条更改 changelog 内容的 commit -- 本地删除 node_modules 目录后重新安装依赖后,执行 `npm run build` 通过 -- 推送 develop 分支到远端,触发部署体验环境,验证体验环境无误 -- 本地 `git tag x.y.z` 后 `git push origin x.y.z`,触发 [TAG_PUSH](https://github.com/Tencent/tdesign-vue/blob/develop/.github/workflows/tag-push.yml) GitAction 进行发包动作 -- 包发布成功后,merge develop 到 main 分支,推送远端后触发官网部署流水线 -- 官网部署完毕后,企微机器人通知群里用户更新 -- copy changelog 到 GitHub repo release(后面考虑改成自动触发更新 release) -- 内网 mk TDesign 发版 Topic 下,copy changelog 内容发布新的版本更新动态 +- 从 `develop` 新建 `release/x.y.z` 分支,并修改 `package.json` 中的版本号,推送分支至远程仓库,并提交一个合入`develop`的 Pull Request 到仓库 +- 仓库的 Github Action 会自动整理上个版本至今 commit 对应的 CHANGELOG,并将 CHANGELOG 的 draft 作为一个评论推送到该 Pull Request 上 +- 发布人检查 CHANGELOG,并优化内容逻辑结构,确认无误后删除对于评论首行提示,Github Action 会将优化后的内容写入 CHANGELOG.md 内 +- 确认无误后,合并分支入`develop` +- 合入 `develop` 后,仓库会触发 Github Action 合入`main`分支,并将版本号作为 `tag` 打在仓库上,并触发 Github Action 执行 npm 版本发布流程 +- 合入 `main` 分支后,站点的部署流水线 web hook 会监听到 `main` 分支的新增 commit,并触发流水线,官网更新站点 diff --git a/package.json b/package.json index 3612cc595..6bef239b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tdesign-vue", "purename": "tdesign", - "version": "1.8.0-naruto", + "version": "1.8.3-naruto", "description": "tdesign-vue", "title": "tdesign-vue", "keywords": [ diff --git a/src/_common b/src/_common index 3506c0dd4..77c26a5fe 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 3506c0dd4ec5ebd28c81ead1a84f26b0ab3a561e +Subproject commit 77c26a5fe917e1ce7f58258788328d56d54b8a85 diff --git a/src/auto-complete/_example/filter.vue b/src/auto-complete/_example/filter.vue index da5486b34..60cc97c40 100644 --- a/src/auto-complete/_example/filter.vue +++ b/src/auto-complete/_example/filter.vue @@ -23,6 +23,8 @@ + diff --git a/src/cascader/cascader.tsx b/src/cascader/cascader.tsx index 3aba8ba6b..69958d374 100644 --- a/src/cascader/cascader.tsx +++ b/src/cascader/cascader.tsx @@ -41,7 +41,9 @@ export default defineComponent({ const { global } = useConfig('cascader'); // 全局状态的上下文 - const { innerValue, cascaderContext, isFilterable } = useCascaderContext(props); + const { + innerValue, cascaderContext, isFilterable, getCascaderItems, + } = useCascaderContext(props); const displayValue = computed(() => props.multiple ? getMultipleContent(cascaderContext.value) : getSingleContent(cascaderContext.value)); @@ -59,6 +61,20 @@ export default defineComponent({ const { formDisabled } = useFormDisabled(); const isDisabled = computed(() => formDisabled.value || cascaderContext.value.disabled); + const valueDisplayParams = computed(() => { + const arrayValue = innerValue.value instanceof Array ? innerValue.value : [innerValue.value]; + const displayValue = props.multiple && props.minCollapsedNum ? arrayValue.slice(0, props.minCollapsedNum) : innerValue.value; + const options = getCascaderItems(arrayValue); + return { + value: innerValue.value, + selectedOptions: options, + onClose: (index: number) => { + handleRemoveTagEffect(cascaderContext.value, index, props.onRemove); + }, + displayValue, + }; + }); + return { COMPONENT_NAME, overlayClassName, @@ -72,10 +88,12 @@ export default defineComponent({ classPrefix, cascaderContext, emit, + valueDisplayParams, }; }, render() { const { + valueDisplayParams, COMPONENT_NAME, overlayClassName, panels, @@ -89,6 +107,10 @@ export default defineComponent({ emit, } = this; + const renderValueDisplay = () => renderTNodeJSX(this, 'valueDisplay', { + params: valueDisplayParams, + }); + const renderSuffixIcon = () => { const suffixIcon = renderTNodeJSX(this, 'suffixIcon'); if (suffixIcon) return suffixIcon; @@ -139,6 +161,7 @@ export default defineComponent({ minCollapsedNum: this.minCollapsedNum, collapsedItems: renderCollapsedItems, label: this.label, + valueDisplay: renderValueDisplay, suffix: this.suffix, // @ts-ignore tag: this.tag, diff --git a/src/cascader/hooks.ts b/src/cascader/hooks.ts index 7620857fa..dba954dea 100644 --- a/src/cascader/hooks.ts +++ b/src/cascader/hooks.ts @@ -19,6 +19,7 @@ import { TreeNodeModel, CascaderChangeSource, CascaderValue, + TreeOptionData, } from './interface'; // 全局状态 @@ -262,9 +263,19 @@ export const useCascaderContext = (props: TdCascaderProps) => { }, ); + const getCascaderItems = (arrValue: CascaderValue[]) => { + const options: TreeOptionData[] = []; + arrValue.forEach((value) => { + const nodes = statusContext.treeStore?.getNodes(value); + nodes && nodes[0] && options.push(nodes[0].data); + }); + return options; + }; + return { innerValue, cascaderContext, isFilterable, + getCascaderItems, }; }; diff --git a/src/checkbox/hooks/useKeyboardEvent.ts b/src/checkbox/hooks/useKeyboardEvent.ts index a4f9ebecb..a059bcb0a 100644 --- a/src/checkbox/hooks/useKeyboardEvent.ts +++ b/src/checkbox/hooks/useKeyboardEvent.ts @@ -1,8 +1,8 @@ -export const CHECKED_CODE = ['Enter', 'Space']; +import { CHECKED_CODE_REG } from '../../_common/js/common'; export function useKeyboardEvent(handleChange: (e: Event) => void) { const keyboardEventListener = (e: KeyboardEvent) => { - const isCheckedCode = CHECKED_CODE.includes(e.key) || CHECKED_CODE.includes(e.code); + const isCheckedCode = CHECKED_CODE_REG.test(e.key) || CHECKED_CODE_REG.test(e.code); if (isCheckedCode) { e.preventDefault(); const { disabled } = (e.currentTarget as HTMLElement).querySelector('input'); diff --git a/src/common.ts b/src/common.ts index 1e35eb7ba..2496ae479 100644 --- a/src/common.ts +++ b/src/common.ts @@ -113,7 +113,7 @@ export type InfinityScroll = TScroll; export interface ScrollToElementParams { /** 跳转元素下标 */ - index: number; + index?: number; /** 跳转元素距离顶部的距离 */ top?: number; /** 单个元素高度非固定场景下,即 isFixedRowHeight = false。延迟设置元素位置,一般用于依赖不同高度异步渲染等场景,单位:毫秒 */ @@ -122,5 +122,5 @@ export interface ScrollToElementParams { } export interface ComponentScrollToElementParams extends ScrollToElementParams { - key: string | number; + key?: string | number; } diff --git a/src/date-picker/DateRangePicker.tsx b/src/date-picker/DateRangePicker.tsx index b49bd4664..3e473d681 100644 --- a/src/date-picker/DateRangePicker.tsx +++ b/src/date-picker/DateRangePicker.tsx @@ -85,11 +85,11 @@ export default defineComponent({ } else if (value.value.length === 2 && !props.enableTimePicker) { // 确保右侧面板月份比左侧大 避免两侧面板月份一致 const nextMonth = value.value.map((v: string) => parseToDayjs(v, formatRef.value.format).month()); + year.value = value.value.map((v: string) => parseToDayjs(v, formatRef.value.format).year()); if (year.value[0] === year.value[1] && nextMonth[0] === nextMonth[1]) { nextMonth[0] === 11 ? (nextMonth[0] -= 1) : (nextMonth[1] += 1); } month.value = nextMonth; - year.value = value.value.map((v: string) => parseToDayjs(v, formatRef.value.format).year()); // 月份季度选择时需要确保右侧面板年份比左侧大 if ((props.mode === 'month' || props.mode === 'quarter') && year.value[0] === year.value[1]) { year.value = [year.value[0], year.value[0] + 1]; diff --git a/src/drawer/drawer.tsx b/src/drawer/drawer.tsx index a50afcf93..daf7a55be 100644 --- a/src/drawer/drawer.tsx +++ b/src/drawer/drawer.tsx @@ -122,7 +122,9 @@ export default mixins(ActionMixin, getConfigReceiverMixins('d visible: { handler(val) { if (val) { - (this.$refs.drawerContainer as HTMLDivElement)?.focus?.(); + this.$nextTick(() => { + (this.$refs.drawerContainer as HTMLDivElement)?.focus?.(); + }); } this.handleScrollThrough(val); diff --git a/src/dropdown/dropdown-menu.tsx b/src/dropdown/dropdown-menu.tsx index 6e4b47411..9b891ecdf 100644 --- a/src/dropdown/dropdown-menu.tsx +++ b/src/dropdown/dropdown-menu.tsx @@ -2,7 +2,7 @@ import { ScopedSlotReturnValue } from 'vue/types/vnode'; import { CreateElement, defineComponent, h, ref, onMounted, reactive, set, } from 'vue'; -import { ChevronRightIcon as TdChevronRightIcon, ChevronLeftIcon as TdChevronLeftIcon } from 'tdesign-icons-vue'; +import { ChevronRightIcon as TdChevronRightIcon } from 'tdesign-icons-vue'; import isFunction from 'lodash/isFunction'; import DropdownItem from './dropdown-item'; @@ -64,9 +64,8 @@ export default defineComponent({ }, // 处理options渲染的场景 renderOptions(data: Array, deep: number) { - const { ChevronRightIcon, ChevronLeftIcon } = useGlobalIcon({ + const { ChevronRightIcon } = useGlobalIcon({ ChevronRightIcon: TdChevronRightIcon, - ChevronLeftIcon: TdChevronLeftIcon, }); const arr: Array = []; let renderContent; @@ -94,21 +93,10 @@ export default defineComponent({ }, }} > - {this.direction === 'right' ? ( -
- - {this.renderOptionContent(optionItem.content)} - - -
- ) : ( -
- - - {this.renderOptionContent(optionItem.content)} - -
- )} +
+ {this.renderOptionContent(optionItem.content)} + +
, - placementSrc: [String, File] as PropType, + src: [String, Object] as PropType, + placementSrc: [String, Object] as PropType, }, setup(props) { diff --git a/src/image-viewer/image-viewer.tsx b/src/image-viewer/image-viewer.tsx index 8c0eefcbc..0dfaaed41 100644 --- a/src/image-viewer/image-viewer.tsx +++ b/src/image-viewer/image-viewer.tsx @@ -99,7 +99,6 @@ export default defineComponent({ setVisibleValue(false); unmountContent(); - window.removeEventListener('keydown', keydownHandler); props.onClose?.(ctx); emit('close', ctx); @@ -114,6 +113,8 @@ export default defineComponent({ }; const keydownHandler = (e: KeyboardEvent) => { + e.stopPropagation(); + switch (e.code) { case EVENT_CODE.left: prevImage(); @@ -148,26 +149,29 @@ export default defineComponent({ containerRef.value.unmountContent(); } }; - + const divRef = ref(); + const getFocus = () => { + if (divRef.value) { + // 只设置tabindex值无法自动获取到焦点,使用focus获取焦点 + divRef.value.focus(); + } + }; watch( () => visibleValue.value, (val) => { if (val) { onRest(); - window.addEventListener('keydown', keydownHandler); mountContent(); + getFocus(); } }, ); const onWheel = (e: WheelEvent) => { e.preventDefault(); - const { deltaY, ctrlKey } = e; - // mac触摸板双指缩放时ctrlKey=true,deltaY>0为缩小 <0为放大 - if (ctrlKey) { - return deltaY > 0 ? onZoomOut() : onZoomIn(); - } - deltaY > 0 ? onZoomIn() : onZoomOut(); + const { deltaY } = e; + + deltaY > 0 ? onZoomOut() : onZoomIn(); }; const transStyle = computed(() => setTransform(`translateX(calc(-${indexValue.value} * (40px / 9 * 16 + 4px)))`)); @@ -205,6 +209,8 @@ export default defineComponent({ scale, isMultipleImg, containerRef, + keydownHandler, + divRef, }; }, methods: { @@ -295,7 +301,14 @@ export default defineComponent({ }, renderViewer() { return ( -
+
{!!this.showOverlayValue && (
)} diff --git a/src/image/props.ts b/src/image/props.ts index 4ea995c29..0a8738946 100644 --- a/src/image/props.ts +++ b/src/image/props.ts @@ -89,7 +89,7 @@ export default { }, /** 用于显示图片的链接或原始图片文件对象 */ src: { - type: [String, File] as PropType, + type: [String, Object] as PropType, }, /** 图片链接集合,用于支持特殊格式的图片,如 `.avif` 和 `.webp`。会优先加载 `srcset` 中的图片格式,浏览器不支持的情况下,加载 `src` 设置的图片地址 */ srcset: { diff --git a/src/menu/menu-item.tsx b/src/menu/menu-item.tsx index ec061b709..9b607ee8c 100644 --- a/src/menu/menu-item.tsx +++ b/src/menu/menu-item.tsx @@ -62,6 +62,7 @@ export default defineComponent({ } }); } + submenu?.closeParentPopup?.(e); }; // lifetimes diff --git a/src/pagination/pagination.tsx b/src/pagination/pagination.tsx index cb6b65a02..87c3ab17a 100644 --- a/src/pagination/pagination.tsx +++ b/src/pagination/pagination.tsx @@ -251,7 +251,7 @@ export default mixins(getConfigReceiverMixins('pagination } const pageSize: number = parseInt(e, 10); let pageCount = 1; - if (pageSize > 0) { + if (pageSize > 0 && this.total > 0) { pageCount = Math.ceil(this.total / pageSize); } diff --git a/src/radio/group.tsx b/src/radio/group.tsx index 655c5152d..d9eb4ad5f 100644 --- a/src/radio/group.tsx +++ b/src/radio/group.tsx @@ -11,7 +11,7 @@ import { emitEvent } from '../utils/event'; import { getClassPrefixMixins } from '../config-provider/config-receiver'; import mixins from '../utils/mixins'; import { off, on } from '../utils/dom'; -import { CHECKED_CODE } from '../checkbox/hooks/useKeyboardEvent'; +import { CHECKED_CODE_REG } from '../_common/js/common'; const classPrefixMixins = getClassPrefixMixins('radio-group'); @@ -92,7 +92,12 @@ export default mixins(classPrefixMixins).extend({ mounted() { this.calcBarStyle(); const observer = new MutationObserver(this.calcBarStyle); - observer.observe(this.$el, { childList: true, attributes: true, subtree: true }); + observer.observe(this.$el, { + childList: true, + attributes: true, + subtree: true, + characterData: true, + }); this.observer = observer; this.addKeyboardListeners(); }, @@ -114,7 +119,7 @@ export default mixins(classPrefixMixins).extend({ // 注意:此处会还原区分 数字 和 数字字符串 checkRadioInGroup(e: KeyboardEvent) { - const isCheckedCode = CHECKED_CODE.includes(e.key) || CHECKED_CODE.includes(e.code); + const isCheckedCode = CHECKED_CODE_REG.test(e.key) || CHECKED_CODE_REG.test(e.code); if (isCheckedCode) { e.preventDefault(); const inputNode = (e.target as HTMLElement).querySelector('input'); @@ -137,7 +142,7 @@ export default mixins(classPrefixMixins).extend({ emitEvent>(this, 'change', value, context); }, - calcDefaultBarStyle(): void { + calcDefaultBarStyle() { const defaultNode = this.$el.cloneNode(true); const div = document.createElement('div'); div.setAttribute('style', 'position: absolute; visibility: hidden;'); @@ -146,8 +151,8 @@ export default mixins(classPrefixMixins).extend({ const defaultCheckedRadio: HTMLElement = div.querySelector(this.checkedClassName); const { offsetWidth, offsetLeft } = defaultCheckedRadio; - this.barStyle = { width: `${offsetWidth}px`, left: `${offsetLeft}px` }; document.body.removeChild(div); + return { offsetWidth, offsetLeft }; }, calcBarStyle(): void { if (this.variant === 'outline') return; @@ -155,13 +160,17 @@ export default mixins(classPrefixMixins).extend({ const checkedRadio: HTMLElement = this.$el.querySelector(this.checkedClassName); if (!checkedRadio) return; - const { offsetWidth, offsetLeft } = checkedRadio; + let { offsetWidth, offsetLeft } = checkedRadio; // current node is not rendered,fallback to default render if (!offsetWidth) { - this.calcDefaultBarStyle(); - } else { - this.barStyle = { width: `${offsetWidth}px`, left: `${offsetLeft}px` }; + const { offsetWidth: width, offsetLeft: left } = this.calcDefaultBarStyle(); + offsetWidth = width; + offsetLeft = left; } + const width = `${offsetWidth}px`; + const left = `${offsetLeft}px`; + if (this.barStyle.width === width && this.barStyle.left === left) return; + this.barStyle = { width, left }; }, }, }); diff --git a/src/statistic/statistic.tsx b/src/statistic/statistic.tsx index a894efea1..b65f91b2f 100644 --- a/src/statistic/statistic.tsx +++ b/src/statistic/statistic.tsx @@ -23,12 +23,18 @@ export default defineComponent({ setup(props) { const classPrefix = usePrefixClass('statistic'); + const { + value, decimalPlaces, separator, color, + } = toRefs(props); - const numberValue = computed(() => (isNumber(props.value) ? props.value : 0)); const innerValue = ref(props.animation?.valueFrom ?? props.value); - const tween = ref(null); - const { value } = toRefs(props); + + const numberValue = computed(() => (isNumber(props.value) ? props.value : 0)); + const valueStyle = computed(() => ({ color: COLOR_MAP[color.value] || color.value })); + const innerDecimalPlaces = computed( + () => decimalPlaces.value ?? numberValue.value.toString().split('.')[1]?.length ?? 0, + ); const start = (from: number = props.animation?.valueFrom ?? 0, to: number = numberValue.value) => { if (from !== to) { @@ -41,7 +47,7 @@ export default defineComponent({ }, duration: props.animation.duration, onUpdate: (keys) => { - innerValue.value = keys.value; + innerValue.value = Number(keys.value.toFixed(innerDecimalPlaces.value)); }, onFinish: () => { innerValue.value = to; @@ -53,29 +59,21 @@ export default defineComponent({ const formatValue = computed(() => { let _value: number | undefined | string = innerValue.value; - const { decimalPlaces, separator } = props; if (isFunction(props.format)) { return props.format(_value); } const options = { - minimumFractionDigits: decimalPlaces || 0, - maximumFractionDigits: decimalPlaces || 20, + minimumFractionDigits: decimalPlaces.value || 0, + maximumFractionDigits: decimalPlaces.value || 20, useGrouping: !!separator, }; // replace的替换的方案仅能应对大部分地区 - _value = _value.toLocaleString(undefined, options).replace(/,|,/g, separator); + _value = _value.toLocaleString(undefined, options).replace(/,|,/g, separator.value); return _value; }); - const valueStyle = computed(() => { - const { color } = props; - return { - color: COLOR_MAP[color] || color, - }; - }); - onMounted(() => props.animation && props.animationStart && start()); watch( diff --git a/src/table/__tests__/column.test.jsx b/src/table/__tests__/column.test.jsx index ce559976a..5bcc3865b 100644 --- a/src/table/__tests__/column.test.jsx +++ b/src/table/__tests__/column.test.jsx @@ -21,7 +21,7 @@ TABLES.forEach((TTable) => { const columns = [ { title: 'Index', colKey: 'index', align: 'center' }, { title: 'Instance', colKey: 'instance', align: 'left' }, - { title: 'description', colKey: 'instance' }, + { title: 'description', colKey: 'description' }, { title: 'Owner', colKey: 'owner', align: 'right' }, ]; const wrapper = mount({ @@ -41,7 +41,7 @@ TABLES.forEach((TTable) => { const columns = [ { title: 'Index', colKey: 'index' }, { title: 'Instance', colKey: 'instance', attrs: { 'col-key': 'instance' } }, - { title: 'description', colKey: 'instance' }, + { title: 'description', colKey: 'description' }, { title: 'Owner', colKey: 'owner' }, ]; const wrapper = mount({ @@ -58,7 +58,7 @@ TABLES.forEach((TTable) => { const columns = [ { title: 'Index', colKey: 'index', className: () => ['tdesign-class'] }, { title: 'Instance', colKey: 'instance', className: 'tdesign-class' }, - { title: 'description', colKey: 'instance', className: [{ 'tdesign-class': true }] }, + { title: 'description', colKey: 'description', className: [{ 'tdesign-class': true }] }, { title: 'Owner', colKey: 'owner', className: { 'tdesign-class': true, 'tdesign-class1': false } }, ]; const wrapper = mount({ diff --git a/src/table/__tests__/pagination/controlled.test.jsx b/src/table/__tests__/pagination/controlled.test.jsx new file mode 100644 index 000000000..cccd71698 --- /dev/null +++ b/src/table/__tests__/pagination/controlled.test.jsx @@ -0,0 +1,173 @@ +import { mockDelay } from '@test/utils'; +import { afterEach } from 'vitest'; +import { + Table, BaseTable, PrimaryTable, EnhancedTable, +} from '@/src/table/index.ts'; +import { getAjaxDataTableMount, getLocalDataTableMount, getSwitchPaginationTableMount } from './mount'; + +// 4 类表格组件同时测试 +const TABLES = [Table, BaseTable, PrimaryTable, EnhancedTable]; + +TABLES.forEach((TTable) => { + describe(TTable.name, () => { + describe('ajax data pagination: data.length = pageSize', () => { + afterEach(() => { + document.querySelector('.t-popup')?.remove(); + document.querySelector('.t-table')?.remove(); + }); + + it('toggle pagination show', async () => { + const wrapper = getSwitchPaginationTableMount(TTable); + expect(wrapper.find('.t-table__pagination').exists()).toBeFalsy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(38); + await wrapper.find('.toggle-pagination input[type=checkbox]').setChecked(true); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.t-table__pagination').exists()).toBeTruthy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(5); + await wrapper.find('.toggle-pagination input[type=checkbox]').setChecked(false); + expect(wrapper.find('.t-table__pagination').exists()).toBeFalsy(); + }); + + it('pagination props change', async () => { + const wrapper = getAjaxDataTableMount(TTable); + + const firstSerialNumberClass = '.t-table tbody tr td:first-child'; + const firstInstanceColClass = '.t-table tbody tr td.custom-instance-class-name'; + expect(wrapper.find('.t-table__pagination').exists()).toBeTruthy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(5); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('1'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance10_5'); + + await wrapper.find('.next-page').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20_5'); + + await wrapper.find('.next-page').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance30_5'); + + await wrapper.find('.prev-page').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20_5'); + + await wrapper.find('.change-page-size').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20_10'); + }); + + it('pagination inner change', async () => { + const wrapper = getAjaxDataTableMount(TTable); + + const firstSerialNumberClass = '.t-table tbody tr td:first-child'; + const firstInstanceColClass = '.t-table tbody tr td.custom-instance-class-name'; + expect(wrapper.find('.t-table__pagination').exists()).toBeTruthy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(5); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('1'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance10_5'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(2)').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20_5'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(3)').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance30_5'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(2)').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20_5'); + + await wrapper.find('.t-pagination__select .t-select-input > .t-input__wrap').trigger('click'); + document.querySelector('.t-select__list li.t-select-option:nth-child(2)').click(); + await wrapper.vm.$nextTick(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(10); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20_10'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(3)').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('21'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance30_10'); + }); + }); + + describe('local data pagination: data.length > pageSize ', () => { + afterEach(() => { + document.querySelector('.t-popup')?.remove(); + document.querySelector('.t-table')?.remove(); + }); + + it('pagination props change', async () => { + const wrapper = getLocalDataTableMount(TTable); + + const firstSerialNumberClass = '.t-table tbody tr td:first-child'; + const firstInstanceColClass = '.t-table tbody tr td.custom-instance-class-name'; + expect(wrapper.find('.t-table__pagination').exists()).toBeTruthy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(5); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('1'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance0'); + + await wrapper.find('.next-page').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance5'); + + await wrapper.find('.next-page').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance10'); + + await wrapper.find('.prev-page').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance5'); + + await wrapper.find('.change-page-size').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance10'); + }); + + it('pagination inner change', async () => { + const wrapper = getLocalDataTableMount(TTable); + + const firstSerialNumberClass = '.t-table tbody tr td:first-child'; + const firstInstanceColClass = '.t-table tbody tr td.custom-instance-class-name'; + expect(wrapper.find('.t-table__pagination').exists()).toBeTruthy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(5); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('1'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance0'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(2)').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance5'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(3)').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance10'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(2)').trigger('click'); + await wrapper.vm.$nextTick(); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance5'); + + await wrapper.find('.t-pagination__select .t-select-input > .t-input__wrap').trigger('click'); + await wrapper.vm.$nextTick(); + document.querySelector('.t-select__list li.t-select-option:nth-child(2)').click(); + await mockDelay(60); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(10); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance10'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(3)').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('21'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20'); + }); + }); + }); +}); diff --git a/src/table/__tests__/pagination/mount.jsx b/src/table/__tests__/pagination/mount.jsx new file mode 100644 index 000000000..272e1d849 --- /dev/null +++ b/src/table/__tests__/pagination/mount.jsx @@ -0,0 +1,323 @@ +import { mount } from '@vue/test-utils'; +import { Checkbox } from '@/src/checkbox/index.ts'; + +function getAjaxTableData(total, currentPage = 1, pageSize = 5) { + return new Array(total).fill(null).map((item, index) => ({ + id: Math.random() + index + 100, + instance: `Instance${index + currentPage * 10}_${pageSize}`, + status: index % 2, + owner: 'jenny;peter', + description: 'test', + })); +} + +const SIMPLE_COLUMNS = [ + { title: 'SerialNumber', colKey: 'serial-number' }, + { title: 'ID', colKey: 'id' }, + { title: 'Instance', colKey: 'instance', className: 'custom-instance-class-name' }, + { title: 'Status', colKey: 'status' }, +]; + +/** 远程数据分页;分页属性为受控属性 */ +export function getAjaxDataTableMount(TTable) { + const DATA_TOTAL = 5; + return mount({ + data() { + return { + data: getAjaxTableData(DATA_TOTAL), + pagination: { + current: 1, + pageSize: 5, + total: 38, + }, + }; + }, + methods: { + // props change + goToPrevPage() { + this.pagination.current = Math.max(this.pagination.current - 1, 1); + this.data = getAjaxTableData(DATA_TOTAL, this.pagination.current); + }, + // props change + goToNextPage() { + const { current, total, pageSize } = this.pagination; + this.pagination.current = Math.min(current + 1, Math.ceil(total / pageSize)); + this.data = getAjaxTableData(DATA_TOTAL, this.pagination.current); + }, + // props change + changePageSize() { + const newPageSize = 10; + this.pagination.pageSize = newPageSize; + this.data = getAjaxTableData(DATA_TOTAL, this.pagination.current, newPageSize); + }, + // inner change + onPaginationChange(pageInfo) { + this.pagination.current = pageInfo.current; + this.pagination.pageSize = pageInfo.pageSize; + this.data = getAjaxTableData(pageInfo.pageSize, pageInfo.current, pageInfo.pageSize); + }, + }, + render() { + return ( +
+ + + + +
+ ); + }, + }); +} + +export function getSwitchPaginationTableMount(TTable) { + const DATA_TOTAL = 38; + return mount({ + data() { + return { + showPagination: false, + data: getLocalTableData(DATA_TOTAL), + }; + }, + computed: { + pagination() { + return this.showPagination + ? { + current: 1, + pageSize: 5, + total: DATA_TOTAL, + } + : undefined; + }, + }, + methods: { + // inner change + onPaginationChange(pageInfo) { + this.pagination.current = pageInfo.current; + this.pagination.pageSize = pageInfo.pageSize; + }, + onCheckboxChange(val) { + this.showPagination = val; + }, + }, + render() { + return ( +
+ + Toggle Pagination + + +
+ ); + }, + }); +} + +/** 远程数据分页;分页属性为非受控属性 */ +export function getDefaultPaginationAjaxDataTableMount(TTable) { + const DATA_TOTAL = 5; + return mount({ + data() { + return { + data: getAjaxTableData(DATA_TOTAL), + pagination: { + defaultCurrent: 1, + defaultPageSize: 5, + total: 38, + }, + }; + }, + methods: { + // inner change + onPaginationChange(pageInfo) { + this.pagination.current = pageInfo.current; + this.pagination.pageSize = pageInfo.pageSize; + this.data = getAjaxTableData(pageInfo.pageSize, pageInfo.current, pageInfo.pageSize); + }, + }, + render() { + return ( +
+ +
+ ); + }, + }); +} + +function getLocalTableData(total) { + return new Array(total).fill(null).map((item, index) => ({ + id: Math.random() + index + 100, + instance: `Instance${index}`, + status: index % 2, + owner: 'jenny;peter', + description: 'test', + })); +} + +export function getLocalDataTableMount(TTable) { + const DATA_TOTAL = 38; + return mount({ + data() { + return { + data: getLocalTableData(DATA_TOTAL), + pagination: { + current: 1, + pageSize: 5, + total: DATA_TOTAL, + }, + }; + }, + methods: { + // props change + goToPrevPage() { + this.pagination.current = Math.max(this.pagination.current - 1, 1); + }, + // props change + goToNextPage() { + const { current, total, pageSize } = this.pagination; + this.pagination.current = Math.min(current + 1, Math.ceil(total / pageSize)); + }, + // props change + changePageSize() { + const newPageSize = 10; + this.pagination.pageSize = newPageSize; + }, + // inner change + onPaginationChange(pageInfo) { + this.pagination.current = pageInfo.current; + this.pagination.pageSize = pageInfo.pageSize; + }, + }, + render() { + return ( +
+ + + + +
+ ); + }, + }); +} + +export function getDefaultPaginationLocalDataTableMount(TTable) { + const DATA_TOTAL = 38; + return mount({ + data() { + return { + data: getLocalTableData(DATA_TOTAL), + pagination: { + defaultCurrent: 1, + defaultPageSize: 5, + total: DATA_TOTAL, + }, + }; + }, + methods: { + // inner change + onPaginationChange(pageInfo) { + this.pagination.current = pageInfo.current; + this.pagination.pageSize = pageInfo.pageSize; + }, + }, + render() { + return ( +
+ +
+ ); + }, + }); +} + +export function getSwitchDefaultPaginationTableMount(TTable) { + const DATA_TOTAL = 36; + return mount({ + data() { + return { + showPagination: false, + data: getLocalTableData(DATA_TOTAL), + }; + }, + computed: { + pagination() { + return this.showPagination + ? { + defaultCurrent: 1, + defaultPageSize: 5, + total: DATA_TOTAL, + } + : undefined; + }, + }, + methods: { + onCheckboxChange(val) { + this.showPagination = val; + }, + }, + render() { + return ( +
+ + Toggle Pagination + + +
+ ); + }, + }); +} diff --git a/src/table/__tests__/pagination/uncontrolled.test.jsx b/src/table/__tests__/pagination/uncontrolled.test.jsx new file mode 100644 index 000000000..b0c3ca6b0 --- /dev/null +++ b/src/table/__tests__/pagination/uncontrolled.test.jsx @@ -0,0 +1,114 @@ +import { mockDelay } from '@test/utils'; +import { afterEach } from 'vitest'; +import { + Table, BaseTable, PrimaryTable, EnhancedTable, +} from '@/src/table/index.ts'; +import { + getDefaultPaginationAjaxDataTableMount, + getDefaultPaginationLocalDataTableMount, + getSwitchDefaultPaginationTableMount, +} from './mount'; + +// 4 类表格组件同时测试 +const TABLES = [Table, BaseTable, PrimaryTable, EnhancedTable]; + +TABLES.forEach((TTable) => { + describe(TTable.name, () => { + afterEach(() => { + document.querySelector('.t-popup')?.remove(); + document.querySelector('.t-table')?.remove(); + }); + describe('ajax data pagination: data.length = pageSize', () => { + afterEach(() => { + document.querySelector('.t-popup')?.remove(); + document.querySelector('.t-table')?.remove(); + }); + + it('toggle pagination show', async () => { + const wrapper = getSwitchDefaultPaginationTableMount(TTable); + expect(wrapper.find('.t-table__pagination').exists()).toBeFalsy(); + await wrapper.find('.toggle-pagination input[type=checkbox]').setChecked(true); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.t-table__pagination').exists()).toBeTruthy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(5); + await wrapper.find('.toggle-pagination input[type=checkbox]').setChecked(false); + expect(wrapper.find('.t-table__pagination').exists()).toBeFalsy(); + }); + + it('pagination inner change', async () => { + const wrapper = getDefaultPaginationAjaxDataTableMount(TTable); + + const firstSerialNumberClass = '.t-table tbody tr td:first-child'; + const firstInstanceColClass = '.t-table tbody tr td.custom-instance-class-name'; + expect(wrapper.find('.t-table__pagination').exists()).toBeTruthy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(5); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('1'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance10_5'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(2)').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20_5'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(3)').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance30_5'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(2)').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20_5'); + + await wrapper.find('.t-pagination__select .t-select-input > .t-input__wrap').trigger('click'); + document.querySelector('.t-select__list li.t-select-option:nth-child(2)').click(); + await mockDelay(60); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(10); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20_10'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(3)').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('21'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance30_10'); + }); + }); + + describe('local data pagination: data.length > pageSize ', () => { + afterEach(() => { + document.querySelector('.t-popup')?.remove(); + document.querySelector('.t-table')?.remove(); + }); + + it('pagination inner change', async () => { + const wrapper = getDefaultPaginationLocalDataTableMount(TTable); + + const firstSerialNumberClass = '.t-table tbody tr td:first-child'; + const firstInstanceColClass = '.t-table tbody tr td.custom-instance-class-name'; + expect(wrapper.find('.t-table__pagination').exists()).toBeTruthy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(5); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('1'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance0'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(2)').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance5'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(3)').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance10'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(2)').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance5'); + + await wrapper.find('.t-pagination__select .t-select-input > .t-input__wrap').trigger('click'); + document.querySelector('.t-select__list li.t-select-option:nth-child(2)').click(); + await mockDelay(60); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(10); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('11'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance10'); + + await wrapper.find('.t-pagination__pager .t-pagination__number:nth-child(3)').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('21'); + expect(wrapper.find(firstInstanceColClass).element.innerHTML).toBe('Instance20'); + }); + }); + }); +}); diff --git a/src/table/__tests__/serial-number/controlled.test.jsx b/src/table/__tests__/serial-number/controlled.test.jsx new file mode 100644 index 000000000..f55379754 --- /dev/null +++ b/src/table/__tests__/serial-number/controlled.test.jsx @@ -0,0 +1,106 @@ +import { mount } from '@vue/test-utils'; +import { mockDelay } from '@test/utils'; +import { afterEach } from 'vitest'; +import { + Table, BaseTable, PrimaryTable, EnhancedTable, +} from '@/src/table/index.ts'; + +// 4 类表格组件同时测试 +const TABLES = [Table, BaseTable, PrimaryTable, EnhancedTable]; + +function getTableData(total) { + return new Array(total).fill(null).map((item, index) => ({ + id: Math.random() + index + 100, + instance: `JQTest${index + 1}`, + status: index % 2, + owner: 'jenny;peter', + description: 'test', + })); +} + +const SIMPLE_COLUMNS = [ + { title: 'SerialNumber', colKey: 'serial-number' }, + { title: 'Status', colKey: 'status' }, + { title: 'Instance', colKey: 'instance' }, +]; + +function getTableMount(TTable) { + return mount({ + data() { + return { + data: getTableData(5), + pagination: { + current: 1, + pageSize: 5, + total: 102, + }, + }; + }, + methods: { + goToPrevPage() { + this.pagination.current = Math.max(this.pagination.current - 1, 1); + }, + goToNextPage() { + const { current, total, pageSize } = this.pagination; + this.pagination.current = Math.min(current + 1, Math.ceil(total / pageSize)); + }, + onPaginationChange(pageInfo) { + this.pagination.current = pageInfo.current; + this.pagination.pageSize = pageInfo.pageSize; + this.data = getTableData(pageInfo.pageSize); + }, + }, + render() { + return ( +
+ + + +
+ ); + }, + }); +} + +TABLES.forEach((TTable) => { + describe(TTable.name, () => { + describe('serial-number', () => { + afterEach(() => { + document.querySelector('.t-popup')?.remove(); + document.querySelector('.t-table')?.remove(); + }); + + it('controlled mode', async () => { + const wrapper = getTableMount(TTable); + + const firstSerialNumberClass = '.t-table tbody tr td:first-child'; + expect(wrapper.find('.t-table__pagination').exists()).toBeTruthy(); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(5); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('1'); + + await wrapper.find('.next-page').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('6'); + + await wrapper.find('.t-pagination__select .t-select-input > .t-input__wrap').trigger('click'); + document.querySelector('.t-select__list li.t-select-option:nth-child(2)').click(); + await mockDelay(60); + expect(wrapper.findAll('.t-table tbody tr').length).toBe(10); + + await wrapper.find('.next-page').trigger('click'); + expect(wrapper.find(firstSerialNumberClass).element.innerHTML).toBe('21'); + }); + }); + }); +}); diff --git a/src/table/_example/drag-sort.vue b/src/table/_example/drag-sort.vue index c4067f1a2..7a78692b1 100644 --- a/src/table/_example/drag-sort.vue +++ b/src/table/_example/drag-sort.vue @@ -3,7 +3,15 @@
- +