diff --git a/.all-contributorsrc b/.all-contributorsrc index 6213858643..0645d33fa3 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -544,6 +544,42 @@ "contributions": [ "code" ] + }, + { + "login": "handsomezyw", + "name": "handsomezyw", + "avatar_url": "https://avatars.githubusercontent.com/u/34366225?v=4", + "profile": "https://github.com/handsomezyw", + "contributions": [ + "code" + ] + }, + { + "login": "iamyoki", + "name": "Yoki", + "avatar_url": "https://avatars.githubusercontent.com/u/74389358?v=4", + "profile": "https://github.com/iamyoki", + "contributions": [ + "code" + ] + }, + { + "login": "LadyChatterleyLover", + "name": "luopei", + "avatar_url": "https://avatars.githubusercontent.com/u/35223515?v=4", + "profile": "https://github.com/LadyChatterleyLover", + "contributions": [ + "code" + ] + }, + { + "login": "wowCheng", + "name": "Mr.Cheng", + "avatar_url": "https://avatars.githubusercontent.com/u/69743874?v=4", + "profile": "https://github.com/wowCheng", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 10, diff --git a/.github/workflows/auto-publish.yml b/.github/workflows/auto-publish.yml new file mode 100644 index 0000000000..2051ae7254 --- /dev/null +++ b/.github/workflows/auto-publish.yml @@ -0,0 +1,56 @@ +name: Auto publish + +on: + push: + branches: ['dev'] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: CheckOut Code + uses: actions/checkout@master + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18 + registry-url: 'https://registry.npmjs.org' + + - name: Get package version + uses: tyankatsu0105/read-package-version-actions@v1 + id: package-version + with: + path: packages/devui-vue + + - name: Create a tag + uses: negz/create-tag@v1 + with: + version: v${{ steps.package-version.outputs.version }} + message: 'Release v${{ steps.package-version.outputs.version }}' + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Build Scripts + working-directory: packages/devui-vue/ + run: | + ls + node -v + npm install pnpm -g + pnpm -v + pnpm i + pnpm run build:lib + + - name: Publish + working-directory: packages/devui-vue/build + run: | + npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ steps.package-version.outputs.version }} + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GIT_TOKEN }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 01f04c0d4c..424b7110d7 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -35,10 +35,10 @@ jobs: run: pnpm i - name: Build site - run: pnpm scripts run build + run: pnpm build - name: ESLint run: pnpm cli --filter vue-devui -- code-check -t eslint - name: Unit test - run: pnpm scripts run tests.test + run: pnpm cli --filter vue-devui -- code-check -t unit-test diff --git a/.ls-lint.yml b/.ls-lint.yml index a049ccc6cb..aed0aae972 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -16,6 +16,7 @@ ignore: - packages/devui-cli/node_modules # devui-theme - packages/devui-theme/dist + - packages/devui-theme/build - packages/devui-theme/node_modules - packages/devui-theme/src/styles-var # devui-vue diff --git a/README.md b/README.md index b941d97b6f..e66f6dc9ae 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,16 @@ English | [简体中文](README.zh-CN.md) 🌈 Features: -- 📦 40 high-quality components that are simple, easy to use, and flexible. +- 📦 55 high-quality components that are simple, easy to use, and flexible. - 🔑 Support for TypeScript. - ⛰️ Support for Nuxt3. - ⚡ Support for on-demand import. - 🌍 Support internationalization. - 🎨 Support theme customization, and built-in seven beautiful themes such as `Galaxy`, `Sweet` and `Provence`. -## 🌐 Installation +## 🔧 Usage + +First install vue-devui with npm, yarn or pnpm. Install with npm @@ -39,7 +41,6 @@ Install with pnpm pnpm add vue-devui ``` -## 🔧 Usage Then import `DevUI` in the `main.ts` file: @@ -68,7 +69,7 @@ Then you can use the vue devui component(such as ``) in the `App.vue` git clone git@github.com:DevCloudFE/vue-devui.git cd vue-devui pnpm install -pnpm scripts run dev +pnpm dev ``` Open your browser and visit: [http://localhost:3000/](http://localhost:3000/). @@ -92,84 +93,135 @@ By participating in the Vue DevUI project, we can together: If you don't know how to start, please read our [contributing guide](https://vue-devui.github.io/contributing/) +## ✨ Maintainers + +Maintainers are community members who have made outstanding contributions and have been active in the DevUI community for a long time. + +- [kagol](https://github.com/kagol) +- [xingyan95](https://github.com/xingyan95) +- [linxiang07](https://github.com/linxiang07) +- [Zcating](https://github.com/Zcating) +- [TinsFox](https://github.com/TinsFox) +- [JensonMiao](https://github.com/JensonMiao) +- [xiejay97](https://github.com/xiejay97) +- [daviForevel](https://github.com/daviForevel) +- [AlanLee97](https://github.com/AlanLee97) +- [SituC](https://github.com/SituC) +- [lj1990111](https://github.com/lj1990111) +- [newer2333](https://github.com/newer2333) +- [vaebe](https://github.com/vaebe) +- [ivestszheng](https://github.com/ivestszheng) +- [ElsaOOo](https://github.com/ElsaOOo) +- [asdlml6](https://github.com/asdlml6) +- [GaoNeng-wWw](https://github.com/GaoNeng-wWw) +- [chenxi24](https://github.com/chenxi24) +- [ErKeLost](https://github.com/ErKeLost) +- [brenner8023](https://github.com/brenner8023) + +Outstanding contributions include, but are not limited to, the following: +- 10 or more PRs are merged +- Put forward more than 10 review opinions approved by PR authors +- Provide constructive optimization advice and promote the project to become better + ## ✨ Contributors +Contributors are community members who have 1 or more PR merged in DevUI. + Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Kagol

🚧 💻 📖

TinsFox

🚧 🚇

nif

💻

Zcating

🚧 💻

王凯

💻

iel

🚧 💻

chenxi24

💻

小九九

💻

AlanLee

💻

Echo

💻

GaoNeng

💻

行言

💻 🐛

devin

💻

无声

💻

sleep_fish

💻

迷心whylost

💻

X.Q. Chen

🚇 💻

葉家男孩

💻

lihai

💻

纳撸多

💻

ElsaOOo

🚧 🚇 💻

刘小迪

💻

unfound

💻

Roading

💻

Chestnut

💻

c0dedance

💻

杜庆愉

💻

linxiang

💻

掘墓忍者

💻

一个大胖子

💻 📖

Ikko Ashimine

📖

Bob

💻

populus

💻

tohalf

💻

Miliky

💻 ⚠️

MICD

💻 🐛

mingBin

💻 🐛

陈剑术

💻

Merlin218

🐛

Johnny.Liu

🐛

Yangxfeng

🐛

jCodeLife

🐛

宋小日

🐛

daviForevel

💻

lj1990111

💻

newer2333

💻

哈士奇-黄

💻


💻

Anthonio OuYang

💻

FlingYP

💻

xzxldl55

💻

79

💻

wailen

💻

jenson

💻

dbsdaicheng

⚠️

qinwencheng

💻

Angelanana

💻

joo1es

💻

Kagol

🚧 💻 📖

TinsFox

🚧 🚇

nif

💻

Zcating

🚧 💻

王凯

💻

iel

🚧 💻

chenxi24

💻

小九九

💻

AlanLee

💻

Echo

💻

GaoNeng

💻

行言

💻 🐛

devin

💻

无声

💻

sleep_fish

💻

迷心whylost

💻

X.Q. Chen

🚇 💻

葉家男孩

💻

lihai

💻

纳撸多

💻

ElsaOOo

🚧 🚇 💻

刘小迪

💻

unfound

💻

Roading

💻

Chestnut

💻

c0dedance

💻

杜庆愉

💻

linxiang

💻

掘墓忍者

💻

一个大胖子

💻 📖

Ikko Ashimine

📖

Bob

💻

populus

💻

tohalf

💻

Miliky

💻 ⚠️

MICD

💻 🐛

mingBin

💻 🐛

陈剑术

💻

Merlin218

🐛

Johnny.Liu

🐛

Yangxfeng

🐛

jCodeLife

🐛

宋小日

🐛

daviForevel

💻

lj1990111

💻

newer2333

💻

哈士奇-黄

💻


💻

Anthonio OuYang

💻

FlingYP

💻

xzxldl55

💻

79

💻

wailen

💻

jenson

💻

dbsdaicheng

⚠️

qinwencheng

💻

Angelanana

💻

joo1es

💻

handsomezyw

💻

Yoki

💻

luopei

💻

Mr.Cheng

💻

Bbbtt04

💻

Zz-ZzzZ

💻

buaalkn

💻

hxj9102

💻

Whbbit1999

💻

zhaoShijuan

💻

XiaoRIGE

💻
@@ -182,6 +234,7 @@ This project follows the [all-contributors](https://github.com/all-contributors/ ## Partner project - [H5-Dooring - 让H5制作,更简单](http://h5.dooring.cn/) +- [灯塔 - 公益性质的反霸凌团队](https://www.light-tower.top/) ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index 9a26544655..1f82421f74 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,7 +12,7 @@ 🌈 特性: -- 📦 包含 40 个简洁、易用、灵活的高质量组件 +- 📦 包含 55 个简洁、易用、灵活的高质量组件 - 🔑 支持 TypeScript - ⛰️ 支持 Nuxt3 - ⚡ 支持按需引入 @@ -63,6 +63,12 @@ pnpm dev 打开浏览器访问:[http://localhost:3000/](http://localhost:3000/) +或者你也可以运行以下命令: + +```sh +pnpm scripts +``` + ## 🤝 参与贡献 欢迎你参与到 Vue DevUI 项目的建设中来!🎉 @@ -75,70 +81,134 @@ pnpm dev 如果你不知道从哪儿开始,可以阅读我们的[贡献指南](https://vue-devui.github.io/contributing/) +## ✨ 维护者 + +维护者是做出杰出贡献且在社区长期活跃的 DevUI 社区成员。 + +- [kagol](https://github.com/kagol) +- [xingyan95](https://github.com/xingyan95) +- [linxiang07](https://github.com/linxiang07) +- [Zcating](https://github.com/Zcating) +- [TinsFox](https://github.com/TinsFox) +- [JensonMiao](https://github.com/JensonMiao) +- [xiejay97](https://github.com/xiejay97) +- [daviForevel](https://github.com/daviForevel) +- [AlanLee97](https://github.com/AlanLee97) +- [SituC](https://github.com/SituC) +- [lj1990111](https://github.com/lj1990111) +- [newer2333](https://github.com/newer2333) +- [vaebe](https://github.com/vaebe) +- [ivestszheng](https://github.com/ivestszheng) +- [ElsaOOo](https://github.com/ElsaOOo) +- [asdlml6](https://github.com/asdlml6) +- [GaoNeng-wWw](https://github.com/GaoNeng-wWw) +- [chenxi24](https://github.com/chenxi24) +- [ErKeLost](https://github.com/ErKeLost) +- [brenner8023](https://github.com/brenner8023) + +杰出贡献包括但不仅限于以下行为: +- 合并 10 个以上 PR +- 提出超过 10 条被 PR 作者认可的检视意见 +- 提供建设性的优化意见并推动项目变得更好 + ## ✨ 贡献者 +贡献者是在 DevUI 社区中合并了 1 个或多个 PR 的社区成员。 + 感谢以下 DevUI 的田主们 ([emoji key](https://allcontributors.org/docs/en/emoji-key)): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Kagol

🚧 💻 📖

TinsFox

🚧 🚇

nif

💻

Zcating

🚧 💻

王凯

💻

iel

🚧 💻

chenxi24

💻

小九九

💻

AlanLee

💻

Echo

💻

GaoNeng

💻

行言

💻 🐛

devin

💻

无声

💻

sleep_fish

💻

迷心whylost

💻

X.Q. Chen

🚇 💻

葉家男孩

💻

lihai

💻

纳撸多

💻

ElsaOOo

🚧 🚇 💻

刘小迪

💻

unfound

💻

Roading

💻

Chestnut

💻

c0dedance

💻

杜庆愉

💻

linxiang

💻

掘墓忍者

💻

一个大胖子

💻 📖

Ikko Ashimine

📖

Bob

💻

populus

💻

tohalf

💻

Miliky

💻 ⚠️

MICD

💻 🐛

mingBin

💻 🐛

陈剑术

💻

Merlin218

🐛

Johnny.Liu

🐛

Yangxfeng

🐛

jCodeLife

🐛

宋小日

🐛

daviForevel

💻

lj1990111

💻

newer2333

💻

Kagol

🚧 💻 📖

TinsFox

🚧 🚇

nif

💻

Zcating

🚧 💻

王凯

💻

iel

🚧 💻

chenxi24

💻

小九九

💻

AlanLee

💻

Echo

💻

GaoNeng

💻

行言

💻 🐛

devin

💻

无声

💻

sleep_fish

💻

迷心whylost

💻

X.Q. Chen

🚇 💻

葉家男孩

💻

lihai

💻

纳撸多

💻

ElsaOOo

🚧 🚇 💻

刘小迪

💻

unfound

💻

Roading

💻

Chestnut

💻

c0dedance

💻

杜庆愉

💻

linxiang

💻

掘墓忍者

💻

一个大胖子

💻 📖

Ikko Ashimine

📖

Bob

💻

populus

💻

tohalf

💻

Miliky

💻 ⚠️

MICD

💻 🐛

mingBin

💻 🐛

陈剑术

💻

Merlin218

🐛

Johnny.Liu

🐛

Yangxfeng

🐛

jCodeLife

🐛

宋小日

🐛 💻

daviForevel

💻

lj1990111

💻

newer2333

💻

哈士奇-黄

💻


💻

Anthonio OuYang

💻

FlingYP

💻

xzxldl55

💻

79

💻

wailen

💻

jenson

💻

dbsdaicheng

⚠️

qinwencheng

💻

Angelanana

💻

joo1es

💻

handsomezyw

💻

Yoki

💻

luopei

💻

Mr.Cheng

💻

Bbbtt04

💻

Zz-ZzzZ

💻

buaalkn

💻

hxj9102

💻

Whbbit1999

💻

zhaoShijuan

💻
@@ -151,6 +221,7 @@ pnpm dev ## 合作项目 - [H5-Dooring - 让H5制作,更简单](http://h5.dooring.cn/) +- [灯塔 - 公益性质的反霸凌团队](https://www.light-tower.top/) ## 开源许可 diff --git a/package.json b/package.json index 629fe8c94b..3f9930e957 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "precommit": "lint-staged", "scripts": "better-scripts", "dev": "pnpm scripts run dev", - "build": "pnpm scripts run build" + "build": "pnpm scripts run build", + "build:lib": "pnpm scripts run build:lib", + "test": "pnpm scripts run tests.test" }, "devDependencies": { "@commitlint/cli": "^11.0.0", diff --git a/packages/devui-theme/src/theme-collection/extend-theme-vue.scss b/packages/devui-theme/src/theme-collection/extend-theme-vue.scss index cf88974472..5c5fc0e614 100644 --- a/packages/devui-theme/src/theme-collection/extend-theme-vue.scss +++ b/packages/devui-theme/src/theme-collection/extend-theme-vue.scss @@ -7,7 +7,7 @@ body[ui-theme='deep-theme'], body[ui-theme='galaxy-theme'] { // TODO: 组件支持全局配置默认尺寸参数后删除 // button default size change to '32px' - .#{$devui-prefix}-button:not(.#{$devui-prefix}-button--xs):not(.#{$devui-prefix}-button--sm):not(.#{$devui-prefix}-button--lg) { + .#{$devui-prefix}-button:not(.#{$devui-prefix}-button--sm):not(.#{$devui-prefix}-button--lg) { height: 32px; line-height: 32px; } diff --git a/packages/devui-vue/devui-cli/commands/build-volar-support.js b/packages/devui-vue/devui-cli/commands/build-volar-support.js new file mode 100644 index 0000000000..9aec318920 --- /dev/null +++ b/packages/devui-vue/devui-cli/commands/build-volar-support.js @@ -0,0 +1,67 @@ +const path = require("path"); +const { + buildComponentItem, + buildGlobalDTSEnd, + buildGlobalDTSStart, + buildComponents, + buildDirectiveItem, + buildDirective, + buildServiceItem, + buildService +} = require('../templates/dts'); +const { writeFileSync } = require('fs'); +const { useRelationTree } = require("../composables/use-relation-tree"); +const { bigCamelCase } = require('../shared/utils'); + +/** + * @param {Record} replaceIdentifier + * @param {string[]} readyToReleaseComponentName + */ +exports.volarSupport = (replaceIdentifier, readyToReleaseComponentName) => { + const componentDTSItem = []; + const directiveDTSItem = []; + const serviceDTSItem = []; + const componentPath = readyToReleaseComponentName.map((name) => path.resolve('./devui', name, 'index.ts')); + const tree = useRelationTree(componentPath); + tree.forEachChild((foldNode) => { + foldNode.forEachChild((node) => { + let nodeName = node.name.replace(/\$/gim, '').replace(/directive/gim, ''); + let reference = nodeName; + const needToTransform = replaceIdentifier?.[foldNode.name]?.[node.name] !== undefined; + if (!node.isComponet){ + const hasType = new RegExp(node.type, 'gim'); + if (!hasType.test(reference)){ + reference += `-${node.type}`; + } + reference = bigCamelCase(reference); + } + if (needToTransform){ + reference = replaceIdentifier[foldNode.name][node.name]?.['reference']; + nodeName = replaceIdentifier[foldNode.name][node.name]?.['exportKey']; + } + if (node.type === 'component'){ + componentDTSItem.push(buildComponentItem(bigCamelCase(nodeName), reference)); + } + if (node.type === 'directive'){ + directiveDTSItem.push(buildDirectiveItem(nodeName, reference)); + } + if (node.type === 'service'){ + serviceDTSItem.push(buildServiceItem(nodeName, reference)); + } + }); + }); + const template = ` +${buildGlobalDTSStart()} +${buildComponents(componentDTSItem.join('\n'))} +${buildDirective(directiveDTSItem.join('\n'))} +${buildService(serviceDTSItem.join('\n'))} +${buildGlobalDTSEnd()} +`; + try { + writeFileSync('./build/global.d.ts', template); + } catch (e) { + console.log(e.message); + return false; + } + return true; +}; diff --git a/packages/devui-vue/devui-cli/commands/build.js b/packages/devui-vue/devui-cli/commands/build.js index 7e4d457e26..3fb2bb74ec 100644 --- a/packages/devui-vue/devui-cli/commands/build.js +++ b/packages/devui-vue/devui-cli/commands/build.js @@ -6,7 +6,11 @@ const vue = require('@vitejs/plugin-vue'); const vueJsx = require('@vitejs/plugin-vue-jsx'); const nuxtBuild = require('./build-nuxt-auto-import'); const { isReadyToRelease } = require('../shared/utils'); - +const { execSync } = require('child_process'); +const { volarSupport } = require('./build-volar-support'); +const logger = require('../shared/logger'); +const replaceIdentifierPath = path.resolve(__dirname,'../replaceIdentifer.json'); +const replaceIdentifier = JSON.parse(fs.readFileSync(replaceIdentifierPath).toString()); const entryDir = path.resolve(__dirname, '../../devui'); const outputDir = path.resolve(__dirname, '../../build'); @@ -67,7 +71,8 @@ const createPackageJson = (name) => { "version": "0.0.0", "main": "index.umd.js", "module": "index.es.js", - "style": "style.css" + "style": "style.css", + "types": "../types/${name}/index.d.ts" }`; fsExtra.outputFile(path.resolve(outputDir, `${name}/package.json`), fileStr, 'utf-8'); @@ -81,15 +86,31 @@ exports.build = async () => { const isDir = fs.lstatSync(componentDir).isDirectory(); return isDir && fs.readdirSync(componentDir).includes('index.ts'); }); - + const readyToReleaseComponentName = []; for (const name of components) { if (!isReadyToRelease(name)) { continue; } + readyToReleaseComponentName.push(name); await buildSingle(name); createPackageJson(name); nuxtBuild.createAutoImportedComponent(name); } - + // 生成global.d.ts + try { + execSync(`pnpm run build:components:dts`); + } catch {} nuxtBuild.createNuxtPlugin(); + logger.success('准备生成global.d.ts'); + const volarSupportbuildState = volarSupport(replaceIdentifier, readyToReleaseComponentName); + fs.writeFileSync('./build/index.d.ts', ` +export * from './types/vue-devui'; +import _default from './types/vue-devui'; +export default _default; +`); + if (volarSupportbuildState){ + logger.success('global.d.ts生成成功'); + } else { + logger.error('global.d.ts生成失败, 因为发生错误'); + } }; diff --git a/packages/devui-vue/devui-cli/commands/code-check.js b/packages/devui-vue/devui-cli/commands/code-check.js index b7ee00fdf8..342c3ac495 100644 --- a/packages/devui-vue/devui-cli/commands/code-check.js +++ b/packages/devui-vue/devui-cli/commands/code-check.js @@ -5,17 +5,11 @@ const chalk = require('chalk'); const { isReadyToRelease } = require('../shared/utils'); const log = console.log; - const chalkEslint = chalk.hex('#4b32c3'); - const chalkUnitTest = chalk.hex('#99425b'); - const chalkError = chalk.hex('#F66F6A'); - const chalkSuccess = chalk.hex('#3DCCA6'); - const entryDir = path.resolve(__dirname, '../../devui'); - const unitTestFailedComponents = []; const completeComponents = fs.readdirSync(entryDir).filter((name) => { @@ -87,8 +81,16 @@ const unitTestSome = async (components) => { }; const unitTestAll = async () => { - for (const name of completeComponents) { - await unitTestSingle(name); + // 单个组件执行单元测试,总耗时太长 + // for (const name of completeComponents) { + // await unitTestSingle(name); + // } + + const unitTestResult = await shell.exec('pnpm --filter vue-devui test --reporter default'); + // 解决单元测试报错,但PR合入门禁不中断问题 + if (/failed|ERR_/.test(unitTestResult.stderr)) { + shell.echo(chalkError('Error: Unit test failed.')); + shell.exit(1); } log(chalkSuccess('\nCongratulations, all components have passed the unit test!')); diff --git a/packages/devui-vue/devui-cli/commands/release.js b/packages/devui-vue/devui-cli/commands/release.js index 7a51f0d072..7a94a311cc 100644 --- a/packages/devui-vue/devui-cli/commands/release.js +++ b/packages/devui-vue/devui-cli/commands/release.js @@ -17,7 +17,7 @@ const getVersion = (version) => { }; const createPackageJson = async (version) => { - package.version = getVersion(version); + // package.version = getVersion(version); package.dependencies = omit(package.dependencies, 'vue'); const fileStr = JSON.stringify(omit(package, 'scripts', 'devDependencies'), null, 2); await fsExtra.outputFile(path.resolve(outputDir, `package.json`), fileStr, 'utf-8'); diff --git a/packages/devui-vue/devui-cli/composables/use-extra.js b/packages/devui-vue/devui-cli/composables/use-extra.js new file mode 100644 index 0000000000..3d301395cf --- /dev/null +++ b/packages/devui-vue/devui-cli/composables/use-extra.js @@ -0,0 +1,57 @@ +const ts = require('typescript'); +/** + * + * @param {string} code node full text. + * @returns {RegExpMatchArray | null} + */ +function extraComponentName(code){ + const regexp = /app\.component\(((?.*)\.name), (?.*)\)/; + const groups = regexp.exec(code)?.groups; + if (groups?.components){ + return groups.components; + } +} +/** + * + app.directive('file-drop', fileDropDirective); + * @param {string} code + */ +function extraDirective(code){ + const regexp = /app\.directive\('(?.*), ?(?.*)\);/; + const groups = regexp.exec(code)?.groups; + if (groups?.fileName){ + return groups.fileName; + } +} + +function extraGlobalProperties(code) { + const globalPropertiesReg = /app\.config\.globalProperties\.(?\$.*) = (?.*);/; + const provideReg = /app\.provide\((?.*)\..*, ?new? ?(?.*)\((?.*)\);/gm; + const groups = globalPropertiesReg.exec(code)?.groups || provideReg.exec(code); + if (groups?.serviceName){ + return groups.serviceName; + } +} + +function extraValue(code){ + return extraComponentName(code) ?? extraDirective(code) ?? extraGlobalProperties(code); +} +/** + * + * @param {string} code + */ +function extraType(code){ + const isDirective = /app\.directive/.test(code); + const isComponent = /app\.component/.test(code); + const isGlobalProperties = /app\.config\.globalProperties/.test(code); + const isProvide = /app\.provide/.test(code); + if (isDirective) {return 'directive';} + if (isComponent) {return 'component';} + if (isGlobalProperties || isProvide) {return 'service';} +} + +exports.extra = extraValue; +exports.extraType = extraType; +exports.extraDirective = extraDirective; +exports.extraComponentName = extraComponentName; +exports.extraGlobalProperties = extraGlobalProperties; diff --git a/packages/devui-vue/devui-cli/composables/use-relation-tree.js b/packages/devui-vue/devui-cli/composables/use-relation-tree.js new file mode 100644 index 0000000000..794d045ad8 --- /dev/null +++ b/packages/devui-vue/devui-cli/composables/use-relation-tree.js @@ -0,0 +1,159 @@ +const ts = require('typescript'); +const { extra, extraType } = require('./use-extra'); +const {readFileSync} = require('fs'); + +class componentNode { + /** + * + * @param {String} name componentName + * @param {Boolean} ready + */ + constructor(name){ + this.name = name; + /** @type {componentNode} */ + this.children = []; + this.type = ''; + this.isComponet = false; + } + /** + * + * @param {(node: componentNode) => void} callback + */ + forEachChild(callback){ + for (const child of this.children){ + callback(child); + } + } +} + +class componentRelationTree{ + constructor(){ + /** + * @type {componentNode} + */ + this.root = new componentNode('root'); + } + /** + * + * @param {componentNode} node component relation Node. Used to describe the relationship between components + */ + insert(node){ + if (!this.#_hasSameNode(node)){ + this.root.children.push(node); + } + } + /** + * + * @param {componentNode} node + * @param {componentNode | componentNode[]} child + */ + insertChild(node, children){ + if (this.#_hasSameNode(node)){ + for (const child of this.root.children){ + if (child.name === node.name){ + if (children instanceof Array){ + child.childen.push(...children); + } else { + child.children.push(children); + } + } + } + } + } + /** + * + * @param {string} name component name + * @return {componentNode} + */ + find(name){ + for (const child of this.root.children){ + if (child.name === name){ + return child; + } + } + } + /** + * + * @param {componentNode} node + * @return {Boolean} + */ + #_hasSameNode(node){ + let idx=0; + let hasSame = false; + while (this.root.children.length !== idx){ + /** @type {componentNode} */ + const child = this.root.children[idx++]; + hasSame = child.name === node.name; + } + return hasSame; + } +} + +/** + * @param {string} indexPath + * @return {string} + */ +function readIndexFile(indexPath){ + return readFileSync(indexPath).toString(); +} +/** + * + * @param {string[]} componentPaths component fold paths + */ +exports.useRelationTree = function (componentPaths){ + /** + * @type {ts.SourceFile[]} + */ + const tsPrograms = []; + const tree = new componentRelationTree(); + tree.root.type = 'root'; + for (const path of componentPaths){ + tsPrograms.push(ts.createSourceFile('', readIndexFile(path))); + } + for (const program of tsPrograms){ + /** + * @type {ts.ExportDeclaration[]} + */ + const sourceFile = program.getSourceFile(); + program.forEachChild((node) => { + if (ts.isExportAssignment(node)){ + /** + * @type {ts.ObjectLiteralElement} + */ + const exportObject = node.getChildAt(0, sourceFile); + /** @type {ts.Node[]} */ + const properties = exportObject.parent.expression.properties; + /** @type {componentNode} */ + let componentTreeNode; + properties.forEach((property) => { + if (ts.isPropertyAssignment(property)){ + const Identifier = property.getChildAt(0, sourceFile).getText(sourceFile); + const value = property.getChildAt(2, sourceFile).getText(sourceFile); + if (Identifier === 'title'){ + componentTreeNode = new componentNode(value.split(' ')[0].slice(1), true); + } + } else { + /** @type {ts.MethodDeclaration} */ + const method = property; + /** @type {ts.Block} */ + const block = method.body.getChildAt(1, sourceFile); + const blockChildren = block.getChildren(sourceFile); + for (const child of blockChildren){ + const childCode = child.getFullText(sourceFile); + const nodeName = extra(childCode); + const nodeType = extraType(childCode); + const childNode = new componentNode(nodeName); + childNode.type = nodeType; + childNode.isComponet = nodeType === 'component'; + if (nodeName){ + componentTreeNode.children.push(childNode); + } + } + } + }); + tree.insert(componentTreeNode); + } + }); + } + return tree.root; +}; diff --git a/packages/devui-vue/devui-cli/index.js b/packages/devui-vue/devui-cli/index.js index cbc63d7628..222d9d724a 100755 --- a/packages/devui-vue/devui-cli/index.js +++ b/packages/devui-vue/devui-cli/index.js @@ -23,7 +23,6 @@ program .command('build') .description('打包组件库') .hook('postAction', generateTheme) - .hook('postAction', generateDts) .action(build); program diff --git a/packages/devui-vue/devui-cli/replaceIdentifer.json b/packages/devui-vue/devui-cli/replaceIdentifer.json new file mode 100644 index 0000000000..938a6cdf00 --- /dev/null +++ b/packages/devui-vue/devui-cli/replaceIdentifer.json @@ -0,0 +1,36 @@ +{ + "Modal":{ + "Modal":{ + "exportKey": "Modal", + "reference": "Modal" + }, + "Body":{ + "exportKey": "ModalBody", + "reference": "Modal" + }, + "Header":{ + "exportKey": "ModalHeader", + "reference": "Modal" + }, + "Footer":{ + "exportKey":"ModalFooter", + "reference": "Modal" + } + }, + "ImagePreview":{ + "$imagePreviewService":{ + "exportKey": "imagePreviewService", + "reference": "ImagePreviewService" + }, + "ImagePreviewDirective":{ + "exportKey": "DImagePreview", + "reference": "ImagePreviewDirective" + } + }, + "Message":{ + "$message":{ + "exportKey": "message", + "reference": "Message" + } + } +} diff --git a/packages/devui-vue/devui-cli/templates/dts.js b/packages/devui-vue/devui-cli/templates/dts.js new file mode 100644 index 0000000000..0fcb1f9a6b --- /dev/null +++ b/packages/devui-vue/devui-cli/templates/dts.js @@ -0,0 +1,39 @@ +exports.buildGlobalDTSStart = () => { + return ` +export{} +declare module '@vue/runtime-core' {`; +}; +exports.buildComponentItem = (componentName, key='') => { + return `D${componentName}: typeof import('./types/vue-devui')['${key || componentName}']`; +}; +exports.buildDirectiveItem = (directive, key='') => { + return `v${directive}?: typeof import('./types/vue-devui')['${key || directive}']`; +}; +exports.buildServiceItem = (service,key='') => { + return `$${service}?: typeof import('./types/vue-devui')['${key || service}']`; +}; +exports.buildGlobalDTSEnd = () => { + return ` +}`; +}; +exports.buildComponents = (componentString) => { + return ` + export interface GlobalComponents{ + ${componentString} + } +`; +}; +exports.buildDirective = (directiveString) => { + return ` + export interface ComponentCustomProps { + ${directiveString} + } +`; +}; +exports.buildService = (serviceSting) => { + return ` + export interface ComponentCustomProperties{ + ${serviceSting} + } +`; +}; diff --git a/packages/devui-vue/devui/auto-complete/src/auto-complete-types.ts b/packages/devui-vue/devui/auto-complete/src/auto-complete-types.ts index 84c847837d..9d92b730c7 100644 --- a/packages/devui-vue/devui/auto-complete/src/auto-complete-types.ts +++ b/packages/devui-vue/devui/auto-complete/src/auto-complete-types.ts @@ -171,7 +171,7 @@ export type SearchFnType = (term: string) => SourceType; export type FormatterType = (item: string | SourceItemObj) => string; export type DefaultFuncType = () => void; export type HandleSearch = (term: string, enableLazyLoad?: boolean) => void; -export type RecentlyFocus = (latestSource: SourceType) => void; +export type RecentlyFocus = (latestSource: Array) => void; export type InputDebounceCb = (value: string) => void; export type TransInputFocusEmit = () => unknown; export type SelectOptionClick = (item: string | SourceItemObj) => void; @@ -189,9 +189,10 @@ export type DropdownProps = { dropDownRef: Ref; showLoading: Ref; loadMore: () => void; - latestSource: Ref>; + latestSource: Ref; modelValue: Ref; hoverIndex: Ref; - valueParser: () => void; + valueParser: Ref; + isDisabled: ComputedRef; }; export const DropdownPropsKey: InjectionKey = Symbol('DropdownPropsKey'); diff --git a/packages/devui-vue/devui/auto-complete/src/auto-complete.scss b/packages/devui-vue/devui/auto-complete/src/auto-complete.scss index 040a10115c..e263faff68 100644 --- a/packages/devui-vue/devui/auto-complete/src/auto-complete.scss +++ b/packages/devui-vue/devui/auto-complete/src/auto-complete.scss @@ -121,17 +121,17 @@ .#{$devui-prefix}-auto-complete { &--sm { - height: 26px; + height: $devui-size-sm; font-size: $devui-font-size-sm; } &--md { - height: 32px; - font-size: $devui-font-size; + height: $devui-size-md; + font-size: $devui-font-size-md; } &--lg { - height: 46px; + height: $devui-size-lg; font-size: $devui-font-size-lg; } diff --git a/packages/devui-vue/devui/auto-complete/src/composables/use-input-handle.ts b/packages/devui-vue/devui/auto-complete/src/composables/use-input-handle.ts index 75fa6da010..a21a840e03 100644 --- a/packages/devui-vue/devui/auto-complete/src/composables/use-input-handle.ts +++ b/packages/devui-vue/devui/auto-complete/src/composables/use-input-handle.ts @@ -5,7 +5,6 @@ import { InputDebounceCb, TransInputFocusEmit, SourceType, - SourceItemObj, UseInputHandle, } from '../auto-complete-types'; export default function useInputHandle( @@ -18,7 +17,7 @@ export default function useInputHandle( handleSearch: HandleSearch, transInputFocusEmit: Ref, recentlyFocus: RecentlyFocus, - latestSource: Ref> + latestSource: Ref ): UseInputHandle { const visible = ref(false); const inputRef = ref(); diff --git a/packages/devui-vue/devui/button/src/button-types.ts b/packages/devui-vue/devui/button/src/button-types.ts index cdbd6cdff1..f46ff13da9 100644 --- a/packages/devui-vue/devui/button/src/button-types.ts +++ b/packages/devui-vue/devui/button/src/button-types.ts @@ -2,7 +2,7 @@ import type { ComputedRef, ExtractPropTypes, PropType ,InjectionKey, Ref } from export type IButtonVariant = 'solid' | 'outline' | 'text'; export type IButtonColor = 'secondary' | 'primary' | 'danger'; -export type IButtonSize = 'lg' | 'md' | 'sm' | 'xs'; +export type IButtonSize = 'lg' | 'md' | 'sm'; export type IButtonShape = 'round' | 'circle'; export const buttonProps = { diff --git a/packages/devui-vue/devui/button/src/button.scss b/packages/devui-vue/devui/button/src/button.scss index e640154854..28ace67aa0 100644 --- a/packages/devui-vue/devui/button/src/button.scss +++ b/packages/devui-vue/devui/button/src/button.scss @@ -12,6 +12,11 @@ $devui-btn-common-border-color-active: var(--devui-btn-common-border-color-activ $devui-btn-sm-padding: var(--devui-btn-sm-padding, 0 16px); $devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); +@mixin btn-solid-style { + color: $devui-brand-active; + border-color: $devui-form-control-line-active; +} + .#{$devui-prefix}-button { padding: $devui-btn-padding; font-size: $devui-font-size-md; @@ -116,29 +121,34 @@ $devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); background-color: $devui-block; border-style: solid; + i { + color: $devui-text; + } + &--secondary { color: $devui-text; border-color: $devui-line; - &:hover { - color: $devui-brand-active; - border-color: $devui-form-control-line-active; - } - - &:focus { - color: $devui-brand-active; - border-color: $devui-form-control-line-active; - } - + &:hover, + &:focus, &:active { color: $devui-brand-active; border-color: $devui-form-control-line-active; + + i { + color: $devui-brand-active; + border-color: $devui-form-control-line-active; + } } &:disabled { color: $devui-disabled-text; border-color: $devui-disabled-line; background-color: $devui-disabled-bg; + + i { + color: $devui-disabled-text; + } } } @@ -146,25 +156,26 @@ $devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); color: $devui-brand-active; border-color: $devui-form-control-line-active; - &:hover { - color: $devui-brand-active-focus; - border-color: $devui-form-control-line-active-hover; - } - - &:focus { - color: $devui-brand-active-focus; - border-color: $devui-form-control-line-active-hover; - } - + &:hover, + &:focus, &:active { color: $devui-brand-active-focus; border-color: $devui-form-control-line-active-hover; + + i { + color: $devui-brand-active-focus; + border-color: $devui-form-control-line-active-hover; + } } &:disabled { opacity: 0.8; color: $devui-brand-active; border-color: $devui-form-control-line-active; + + i { + color: $devui-brand-active; + } } } @@ -172,6 +183,10 @@ $devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); color: $devui-contrast; border-color: $devui-contrast; + i { + color: $devui-contrast; + } + &:hover, &:focus, &:active, @@ -236,13 +251,6 @@ $devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); } } - &--xs { - padding: 0 4px; - height: $devui-size-sm; - font-size: $devui-font-size-sm; - min-width: 48px; - } - &--sm { padding: $devui-btn-sm-padding; height: $devui-size-sm; @@ -330,7 +338,6 @@ $devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); border: 1px solid transparent; } - &.#{$devui-prefix}-button--xs, &.#{$devui-prefix}-button--sm { padding: 4px; min-width: 24px; @@ -347,7 +354,6 @@ $devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); &.#{$devui-prefix}-button--circle { width: $devui-size-md; - &.#{$devui-prefix}-button--xs, &.#{$devui-prefix}-button--sm { width: $devui-size-sm; } @@ -378,3 +384,19 @@ $devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); .clear-right-5 { margin-right: 5px; } + +.loading-icon__container { + display: inline-flex; + align-items: center; + margin-right: 5px; + + .button-icon-loading { + animation: rotating 1.5s linear infinite; + } +} + +@keyframes rotating { + 0% { transform: rotate(0); } + + 100% { transform: rotate(180deg); } +} diff --git a/packages/devui-vue/devui/button/src/button.tsx b/packages/devui-vue/devui/button/src/button.tsx index b8016110d6..6b36cd3ca6 100644 --- a/packages/devui-vue/devui/button/src/button.tsx +++ b/packages/devui-vue/devui/button/src/button.tsx @@ -26,8 +26,11 @@ export default defineComponent({ return () => { return ( - ); diff --git a/packages/devui-vue/devui/card/src/card-types.ts b/packages/devui-vue/devui/card/src/card-types.ts index 08e2c5e3a9..382baa7cac 100644 --- a/packages/devui-vue/devui/card/src/card-types.ts +++ b/packages/devui-vue/devui/card/src/card-types.ts @@ -10,7 +10,7 @@ export const cardProps = { default: '', }, shadow: { - type: String as PropType<'alway' | 'hover' | 'never'>, + type: String as PropType<'always' | 'hover' | 'never'>, default: 'hover', }, } as const; diff --git a/packages/devui-vue/devui/carousel/src/carousel.tsx b/packages/devui-vue/devui/carousel/src/carousel.tsx index b440f60a63..fa70369904 100644 --- a/packages/devui-vue/devui/carousel/src/carousel.tsx +++ b/packages/devui-vue/devui/carousel/src/carousel.tsx @@ -11,8 +11,8 @@ export default defineComponent({ emits: ['update:activeIndex', 'activeIndexChange'], setup(props: CarouselProps, { emit, slots, expose }) { const ns = useNamespace('carousel'); - const { height, showDots, dotPosition, arrowTrigger, autoplay, autoplaySpeed, dotTrigger, activeIndex } = toRefs(props); - const transitionSpeed = 500; + const { height, showDots, dotPosition, arrowTrigger, autoplay, autoplaySpeed, dotTrigger, activeIndex, transitionSpeed } = + toRefs(props); const itemCount = ref(0); const showArrow = ref(false); @@ -51,7 +51,7 @@ export default defineComponent({ targetEl.style.transform = ''; translatePosition(currentIndex.value); - }, transitionSpeed); + }, transitionSpeed.value); }; // 调整首尾翻动时的位置 @@ -86,7 +86,7 @@ export default defineComponent({ return; } - containerRef.value.style.transition = `left ${transitionSpeed}ms ease`; + containerRef.value.style.transition = `left ${transitionSpeed.value}ms ease`; let latestIndex = currentIndex.value; if (index < 0 && currentIndex.value === 0) { @@ -148,7 +148,7 @@ export default defineComponent({ onMounted(() => { if (containerRef.value) { - containerRef.value.style.transition = `left ${transitionSpeed}ms ease`; + containerRef.value.style.transition = `left ${transitionSpeed.value}ms ease`; containerRef.value.style.left = '0%'; } @@ -209,6 +209,7 @@ export default defineComponent({ class={{ 'dot-item': true, active: currentIndex.value === index }} onClick={() => switchStep(index, 'click')} onMouseenter={() => switchStep(index, 'hover')} + style={{ transition: `all ${transitionSpeed.value}ms ease` }} /> ))} diff --git a/packages/devui-vue/devui/carousel/src/types.ts b/packages/devui-vue/devui/carousel/src/types.ts index 5656d4de8b..70e0396dda 100644 --- a/packages/devui-vue/devui/carousel/src/types.ts +++ b/packages/devui-vue/devui/carousel/src/types.ts @@ -37,6 +37,10 @@ export const carouselProps = { type: Number, default: 0, }, + transitionSpeed: { + type: Number, + default: 500, + }, } as const; export type CarouselProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/cascader/__tests__/cascader.spec.ts b/packages/devui-vue/devui/cascader/__tests__/cascader.spec.ts index c75c084dc4..574eb857f2 100644 --- a/packages/devui-vue/devui/cascader/__tests__/cascader.spec.ts +++ b/packages/devui-vue/devui/cascader/__tests__/cascader.spec.ts @@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'; import { useNamespace } from '../../shared/hooks/use-namespace'; import { ref, reactive, nextTick } from 'vue'; import DCascader from '../src/cascader'; +import { Form as DForm, FormItem as DFormItem } from '../../form'; jest.mock('../../locale/create', () => ({ createI18nTranslate: () => jest.fn(), @@ -16,9 +17,14 @@ const closeClass = ns.e('close'); const panelClass = ns.e('panel'); const suggestListClass = ns.e('suggest-list'); -const inputNs = useNamespace('input', true); -const inputInnerClass = inputNs.e('inner'); -const inputDisabledClass = inputNs.m('disabled'); +const dotInputNs = useNamespace('input', true); +const inputNs = useNamespace('input'); + +const inputClass = dotInputNs.b(); +const inputInnerClass = dotInputNs.e('inner'); +const inputDisabledClass = dotInputNs.m('disabled'); +const inputSizeSmClass = inputNs.m('sm'); +const inputSizeLgClass = inputNs.m('lg'); const OPTIONS = [ { @@ -248,4 +254,42 @@ describe('cascader', () => { wrapper.unmount(); }); + + it('cascader props size priority', async () => { + const options = reactive(OPTIONS); + const dFormSize = ref('lg'); + const dCascaderSize = ref('sm'); + + const wrapper = mount({ + components: { DCascader, DForm, DFormItem }, + template: ` + + + + + `, + setup() { + return { + dFormSize, + dCascaderSize, + options + }; + }, + }); + + const dSearch = wrapper.find(inputClass); + // form 与 元素同时存在size 属性,以元素为准。 + expect(dSearch.classes()).toContain(inputSizeSmClass); + + dCascaderSize.value = ''; + await nextTick(); + expect(dSearch.classes()).toContain(inputSizeLgClass); + + dFormSize.value = ''; + await nextTick(); + expect(dSearch.classes()).not.toContain(inputSizeLgClass); + expect(dSearch.classes()).not.toContain(inputSizeSmClass); + + wrapper.unmount(); + }); }); diff --git a/packages/devui-vue/devui/cascader/src/cascader-types.ts b/packages/devui-vue/devui/cascader/src/cascader-types.ts index 44a65ae5cd..d5caf2ed2d 100644 --- a/packages/devui-vue/devui/cascader/src/cascader-types.ts +++ b/packages/devui-vue/devui/cascader/src/cascader-types.ts @@ -125,8 +125,7 @@ export const cascaderProps = { default: () => true, }, size: { - type: String as PropType, - default: 'md', + type: String as PropType }, } as const; diff --git a/packages/devui-vue/devui/cascader/src/cascader.scss b/packages/devui-vue/devui/cascader/src/cascader.scss index 7c4c9209c2..988a863897 100644 --- a/packages/devui-vue/devui/cascader/src/cascader.scss +++ b/packages/devui-vue/devui/cascader/src/cascader.scss @@ -83,7 +83,7 @@ } .#{$devui-prefix}-cascader__dropdown--open { - .#{$devui-prefix}-cascader__icon { + .#{$devui-prefix}-cascader--drop-icon-animation { transform: rotate(180deg); } } diff --git a/packages/devui-vue/devui/checkbox/__tests__/checkbox-button.spec.ts b/packages/devui-vue/devui/checkbox/__tests__/checkbox-button.spec.ts index 9ab79be836..52ac38ee5c 100644 --- a/packages/devui-vue/devui/checkbox/__tests__/checkbox-button.spec.ts +++ b/packages/devui-vue/devui/checkbox/__tests__/checkbox-button.spec.ts @@ -30,6 +30,8 @@ describe('checkbox-button', () => { expect(container.classes()).not.toContain('active'); expect(container.classes()).toContain('unchecked'); + + wrapper.unmount(); }); it('checkbox-button title work', async () => { @@ -54,6 +56,8 @@ describe('checkbox-button', () => { isShowTitle: false, }); expect(label.attributes('title')).toEqual(''); + + wrapper.unmount(); }); it('checkbox-button disabled work', async () => { @@ -77,6 +81,8 @@ describe('checkbox-button', () => { await label.trigger('click'); expect(wrapper.find(baseClass).classes()).not.toContain('disabled'); expect(onChange).toBeCalledTimes(1); + + wrapper.unmount(); }); it('checkbox-button beforeChange work', async () => { @@ -113,6 +119,8 @@ describe('checkbox-button', () => { expect(beforeChange).toBeCalledTimes(2); expect(onChange).toBeCalledTimes(1); expect(checked.value).toBe(true); + + wrapper.unmount(); }); it('checkbox-button size work', async () => { @@ -124,5 +132,7 @@ describe('checkbox-button', () => { }); expect(wrapper.find(sizeLgClass).exists()).toBe(true); + + wrapper.unmount(); }); }); diff --git a/packages/devui-vue/devui/checkbox/__tests__/checkbox-group.spec.ts b/packages/devui-vue/devui/checkbox/__tests__/checkbox-group.spec.ts index dc2034840e..29e24b498a 100644 --- a/packages/devui-vue/devui/checkbox/__tests__/checkbox-group.spec.ts +++ b/packages/devui-vue/devui/checkbox/__tests__/checkbox-group.spec.ts @@ -8,7 +8,6 @@ import { useNamespace } from '../../shared/hooks/use-namespace'; const ns = useNamespace('checkbox', true); const baseClass = ns.b(); const columnMarginClass = ns.e('column-margin'); -const listLineClass = ns.m('list-inline'); const wrapClass = ns.e('wrap'); const borderClass = ns.m('bordered'); const sizeLgClass = ns.m('lg'); @@ -49,6 +48,8 @@ describe('d-checkbox-group', () => { await nextTick(); expect(box1.classes()).toContain('active'); expect(box2.classes()).toContain('unchecked'); + + wrapper.unmount(); }); it('checkbox-group disabled work', async () => { @@ -87,6 +88,8 @@ describe('d-checkbox-group', () => { expect(list.value).toStrictEqual(['b', 'a']); expect(onChange).toBeCalledTimes(1); expect(wrapper.findAll(baseClass).some((el) => el.classes().includes('disabled'))).toBe(false); + + wrapper.unmount(); }); it('checkbox-group direction work', async () => { @@ -112,11 +115,13 @@ describe('d-checkbox-group', () => { }); expect(wrapper.findAll(columnMarginClass).length).toBe(2); - expect(wrapper.find(listLineClass).exists()).toBe(false); + expect(wrapper.find('.is-column').exists()).toBe(true); direction.value = 'row'; await nextTick(); - expect(wrapper.find(listLineClass).exists()).toBe(true); + expect(wrapper.find('.is-row').exists()).toBe(true); + + wrapper.unmount(); }); it('checkbox-group itemWidth work', () => { @@ -142,6 +147,8 @@ describe('d-checkbox-group', () => { }); expect(wrapper.findAll(wrapClass).length).toBe(2); + + wrapper.unmount(); }); it('checkbox-group options work', () => { @@ -175,6 +182,8 @@ describe('d-checkbox-group', () => { expect(boxList.length).toBe(2); expect(boxList[0].classes()).toContain('unchecked'); expect(boxList[1].classes()).toContain('active'); + + wrapper.unmount(); }); it('checkbox-group beforeChange work', async () => { @@ -214,6 +223,8 @@ describe('d-checkbox-group', () => { expect(beforeChange).toHaveBeenCalledTimes(2); expect(onChange).toBeCalledTimes(1); expect(list.value).toStrictEqual(['b', 'a']); + + wrapper.unmount(); }); it('checkbox-group max work', async () => { @@ -251,6 +262,8 @@ describe('d-checkbox-group', () => { await label2.trigger('click'); expect(list.value).toStrictEqual(['c']); expect(wrapper.findAll(baseClass).filter((el) => el.classes().includes('disabled'))?.length).toBe(0); + + wrapper.unmount(); }); it('checkbox-group border size work', () => { @@ -275,6 +288,8 @@ describe('d-checkbox-group', () => { expect(wrapper.find(borderClass).exists()).toBe(true); expect(wrapper.find(sizeLgClass).exists()).toBe(true); + + wrapper.unmount(); }); it('checkbox-group checkbox-button', async () => { @@ -303,6 +318,8 @@ describe('d-checkbox-group', () => { expect(list.value).toStrictEqual(['b', 'a']); await label2.trigger('click'); expect(list.value).toStrictEqual(['a']); + + wrapper.unmount(); }); it('checkbox-button color text-color', async () => { @@ -328,5 +345,7 @@ describe('d-checkbox-group', () => { await nextTick(); const content = wrapper.findAll(contentClass); expect(content[0].attributes().style).toBe('border-color: red; background-color: red; color: rgb(204, 204, 204);'); + + wrapper.unmount(); }); }); diff --git a/packages/devui-vue/devui/checkbox/__tests__/checkbox.spec.ts b/packages/devui-vue/devui/checkbox/__tests__/checkbox.spec.ts index 4d033898dc..1d0997f7bd 100644 --- a/packages/devui-vue/devui/checkbox/__tests__/checkbox.spec.ts +++ b/packages/devui-vue/devui/checkbox/__tests__/checkbox.spec.ts @@ -34,6 +34,8 @@ describe('checkbox', () => { expect(container.classes()).not.toContain('active'); expect(container.classes()).toContain('unchecked'); + + wrapper.unmount(); }); it('checkbox title work', async () => { @@ -58,6 +60,8 @@ describe('checkbox', () => { isShowTitle: false, }); expect(label.attributes('title')).toEqual(''); + + wrapper.unmount(); }); it('checkbox showAnimation work', async () => { @@ -73,6 +77,8 @@ describe('checkbox', () => { showAnimation: false, }); expect(wrapper.findAll(noAnimationClass).length).toBe(2); + + wrapper.unmount(); }); it('checkbox disabled work', async () => { @@ -96,6 +102,8 @@ describe('checkbox', () => { await label.trigger('click'); expect(wrapper.find(baseClass).classes()).not.toContain('disabled'); expect(onChange).toBeCalledTimes(1); + + wrapper.unmount(); }); it('checkbox halfchecked work', async () => { @@ -115,6 +123,8 @@ describe('checkbox', () => { }); expect(container.classes()).toContain('half-checked'); expect(container.find(defaultBgClass).exists()).toBe(false); + + wrapper.unmount(); }); it('checkbox beforeChange work', async () => { @@ -151,6 +161,8 @@ describe('checkbox', () => { expect(beforeChange).toBeCalledTimes(2); expect(onChange).toBeCalledTimes(1); expect(checked.value).toBe(true); + + wrapper.unmount(); }); it('checkbox border work', async () => { @@ -167,6 +179,8 @@ describe('checkbox', () => { border: true, }); expect(wrapper.find(borderClass).exists()).toBe(true); + + wrapper.unmount(); }); it('checkbox size work', async () => { @@ -178,12 +192,14 @@ describe('checkbox', () => { }, }); - expect(wrapper.find(sizeLgClass).exists()).toBe(false); + expect(wrapper.find(sizeLgClass).exists()).toBe(true); await wrapper.setProps({ border: true, }); - expect(wrapper.find(sizeLgClass).exists()).toBe(true); + expect(wrapper.find(borderClass).exists()).toBe(true); + + wrapper.unmount(); }); it('checkbox color work', async () => { @@ -224,5 +240,7 @@ describe('checkbox', () => { // 找不到backgroundImage属性 // expect(element.style.backgroundImage).toBe('linear-gradient(pink, pink)'); // can't find backgroundImage expect(element.style.backgroundColor).toBe('pink'); + + wrapper.unmount(); }); }); diff --git a/packages/devui-vue/devui/checkbox/src/checkbox-button.scss b/packages/devui-vue/devui/checkbox/src/checkbox-button.scss index 382ffcd6d0..be66e264af 100644 --- a/packages/devui-vue/devui/checkbox/src/checkbox-button.scss +++ b/packages/devui-vue/devui/checkbox/src/checkbox-button.scss @@ -1,19 +1,27 @@ @import '../../styles-var/devui-var.scss'; +$devui-btn-sm-padding: var(--devui-btn-sm-padding, 0 16px); +$devui-btn-padding: var(--devui-btn-padding, 0 20px); +$devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); + $font-size-map: ( lg: $devui-font-size-lg, - md: $devui-font-size-lg, + md: $devui-font-size-md, sm: $devui-font-size-sm, - xs: $devui-font-size-sm, ); $button-padding-map: ( - lg: 12px 20px, - md: 10px 20px, - sm: 9px 15px, - xs: 7px 15px, + lg: $devui-btn-lg-padding, + md: $devui-btn-padding, + sm: $devui-btn-sm-padding, +); +$checkbox-label-height-map: ( + lg: $devui-size-lg, + md: $devui-size-md, + sm: $devui-size-sm, ); .#{$devui-prefix}-checkbox-button { - display: inline-block; + display: flex; + align-items: center; position: relative; &__input { @@ -34,15 +42,15 @@ $button-padding-map: ( padding: 10px 20px; cursor: pointer; border: 1px solid $devui-disabled-line; - border-left: none; display: inline-block; line-height: 1; user-select: none; - box-shadow: -1px 0 0 0 $devui-disabled-line; - @each $size in ('lg', 'md', 'sm', 'xs') { + @each $size in ('lg', 'md', 'sm') { &.#{$devui-prefix}-checkbox-button--#{$size} { font-size: map-get($font-size-map, #{$size}); padding: map-get($button-padding-map, #{$size}); + height: map-get($checkbox-label-height-map, #{$size}); + line-height: map-get($checkbox-label-height-map, #{$size}); } } } @@ -71,20 +79,60 @@ $button-padding-map: ( border-color: $devui-disabled-line; } } +} - &:first-child { - .#{$devui-prefix}-checkbox-button__content { - border-top-left-radius: $devui-border-radius; - border-bottom-left-radius: $devui-border-radius; - border-left: 1px solid $devui-disabled-line; - box-shadow: none; +.#{$devui-prefix}-checkbox__group { + &.is-row { + .#{$devui-prefix}-checkbox-button { + &__content { + border-left: none; + box-shadow: -1px 0 0 0 $devui-disabled-line; + } + + &:first-child { + .#{$devui-prefix}-checkbox-button__content { + border-top-left-radius: $devui-border-radius; + border-bottom-left-radius: $devui-border-radius; + border-left: 1px solid $devui-disabled-line; + box-shadow: none; + } + } + + &:last-child { + .#{$devui-prefix}-checkbox-button__content { + border-top-right-radius: $devui-border-radius; + border-bottom-right-radius: $devui-border-radius; + } + } } } - &:last-child { - .#{$devui-prefix}-checkbox-button__content { - border-top-right-radius: $devui-border-radius; - border-bottom-right-radius: $devui-border-radius; + &.is-column { + .#{$devui-prefix}-checkbox-button { + width: 100%; + margin-top: 0; + + &__content { + width: 100%; + border-top: none; + box-shadow: 0 -1px 0 0 $devui-disabled-line; + } + + &:first-child { + .#{$devui-prefix}-checkbox-button__content { + border-top-left-radius: $devui-border-radius; + border-top-right-radius: $devui-border-radius; + border-top: 1px solid $devui-disabled-line; + box-shadow: none; + } + } + + &:last-child { + .#{$devui-prefix}-checkbox-button__content { + border-bottom-left-radius: $devui-border-radius; + border-bottom-right-radius: $devui-border-radius; + } + } } } } diff --git a/packages/devui-vue/devui/checkbox/src/checkbox-group.scss b/packages/devui-vue/devui/checkbox/src/checkbox-group.scss index b02345dba7..7cbe2622ee 100644 --- a/packages/devui-vue/devui/checkbox/src/checkbox-group.scss +++ b/packages/devui-vue/devui/checkbox/src/checkbox-group.scss @@ -1,23 +1,24 @@ @import '../../styles-var/devui-var.scss'; .#{$devui-prefix}-checkbox__group { - display: inline-block; -} - -.#{$devui-prefix}-checkbox--list-inline { - min-height: 28px; - line-height: 28px; - display: flex; + display: inline-flex; flex-wrap: wrap; justify-content: flex-start; - align-items: center; - margin: -8px 0 0 0; - - & div:not(:last-child) { - margin-right: 20px; - } + align-items: flex-start; & > * { margin-top: 8px; } + + &.is-row { + flex-direction: row; + + & div:not(:last-child) { + margin-right: 20px; + } + } + + &.is-column { + flex-direction: column; + } } diff --git a/packages/devui-vue/devui/checkbox/src/checkbox-group.tsx b/packages/devui-vue/devui/checkbox/src/checkbox-group.tsx index f8756447e2..a7c29db57b 100644 --- a/packages/devui-vue/devui/checkbox/src/checkbox-group.tsx +++ b/packages/devui-vue/devui/checkbox/src/checkbox-group.tsx @@ -41,9 +41,18 @@ export default defineComponent({ } }; + return ( -
-
{getContent()}
+
+ {getContent()}
); }; diff --git a/packages/devui-vue/devui/checkbox/src/checkbox-types.ts b/packages/devui-vue/devui/checkbox/src/checkbox-types.ts index 4a15055ffd..1022e2242b 100644 --- a/packages/devui-vue/devui/checkbox/src/checkbox-types.ts +++ b/packages/devui-vue/devui/checkbox/src/checkbox-types.ts @@ -1,7 +1,7 @@ import { PropType, InjectionKey, Ref, ExtractPropTypes, ComputedRef } from 'vue'; type Direction = 'row' | 'column'; -type Size = 'lg' | 'md' | 'sm' | 'xs'; +type Size = 'lg' | 'md' | 'sm'; const commonProps = { name: { @@ -36,8 +36,7 @@ const commonProps = { default: undefined, }, size: { - type: String as PropType, - default: 'md', + type: String as PropType }, } as const; @@ -147,7 +146,7 @@ export type UseCheckboxFn = { direction: string | undefined; size: ComputedRef; border: ComputedRef; - handleClick: () => void; + handleClick: (event: Event) => void; }; export interface GroupDefaultOpt { diff --git a/packages/devui-vue/devui/checkbox/src/checkbox.scss b/packages/devui-vue/devui/checkbox/src/checkbox.scss index 7e59c51e16..5ce3a58493 100644 --- a/packages/devui-vue/devui/checkbox/src/checkbox.scss +++ b/packages/devui-vue/devui/checkbox/src/checkbox.scss @@ -2,21 +2,18 @@ $font-size-map: ( lg: $devui-font-size-lg, - md: $devui-font-size-lg, + md: $devui-font-size-md, sm: $devui-font-size-sm, - xs: $devui-font-size-sm, ); $checkbox-width-map: ( - lg: 16px, + lg: 18px, md: 16px, sm: 14px, - xs: 14px, ); $checkbox-label-height-map: ( - lg: 40px, - md: 36px, - sm: 32px, - xs: 28px, + lg: $devui-size-lg, + md: $devui-size-md, + sm: $devui-size-sm, ); .#{$devui-prefix}-checkbox { position: relative; @@ -183,9 +180,12 @@ $checkbox-label-height-map: ( cursor: pointer; color: $devui-text; margin: 0; - display: block; - @each $size in ('lg', 'md', 'sm', 'xs') { + display: flex; + align-items: center; + @each $size in ('lg', 'md', 'sm') { &.#{$devui-prefix}-checkbox--#{$size} { + display: flex; + align-items: center; font-size: map-get($font-size-map, #{$size}); .#{$devui-prefix}-checkbox__material { @@ -200,7 +200,6 @@ $checkbox-label-height-map: ( &.#{$devui-prefix}-checkbox--bordered { height: map-get($checkbox-label-height-map, #{$size}); - line-height: map-get($checkbox-label-height-map, #{$size}); padding: 0 15px 0 10px; } } @@ -215,7 +214,19 @@ $checkbox-label-height-map: ( display: inline-block; box-sizing: content-box; vertical-align: top; + + &.#{$devui-prefix}-checkbox__material { + flex-shrink: 0; // keep the checkbox icon width + } + + &.#{$devui-prefix}-checkbox__label-text { + flex-shrink: 1; // just use flex box to shrink text width. let text ellipsis + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } + } // 禁用状态透明色 diff --git a/packages/devui-vue/devui/checkbox/src/checkbox.tsx b/packages/devui-vue/devui/checkbox/src/checkbox.tsx index d950415082..0c63529392 100644 --- a/packages/devui-vue/devui/checkbox/src/checkbox.tsx +++ b/packages/devui-vue/devui/checkbox/src/checkbox.tsx @@ -22,6 +22,7 @@ export default defineComponent({ size, border, } = useCheckbox(props, ctx); + return () => { const wrapperCls = { [ns.e('column-margin')]: direction === 'column', @@ -55,7 +56,7 @@ export default defineComponent({ [ns.m('no-animation')]: !mergedShowAnimation.value, }; const labelCls = { - [ns.m(size.value)]: border.value, + [ns.m(size.value)]: size.value, [ns.m('bordered')]: border.value, }; const stopPropagation = ($event: Event) => $event.stopPropagation(); @@ -89,7 +90,7 @@ export default defineComponent({ - {props.label || ctx.slots.default?.()} + {props.label || ctx.slots.default?.()}
diff --git a/packages/devui-vue/devui/checkbox/src/use-checkbox.ts b/packages/devui-vue/devui/checkbox/src/use-checkbox.ts index 72eb2ea9eb..86775cde6b 100644 --- a/packages/devui-vue/devui/checkbox/src/use-checkbox.ts +++ b/packages/devui-vue/devui/checkbox/src/use-checkbox.ts @@ -60,11 +60,15 @@ export function useCheckbox(props: CheckboxProps, ctx: SetupContext): UseCheckbo ctx.emit('update:modelValue', current); ctx.emit('change', current); }; - const handleClick = () => { + const handleClick = ($event: Event) => { + $event.stopPropagation(); canChange(!isChecked.value, props.label).then((res) => res && toggle()); }; - const size = computed(() => formContext?.size || checkboxGroupConf?.size.value || props.size); + + const size = computed(() => props.size || checkboxGroupConf?.size.value || formContext?.size || 'md'); + const border = computed(() => checkboxGroupConf?.border.value ?? props.border); + watch( () => props.modelValue, () => { @@ -88,6 +92,7 @@ export function useCheckbox(props: CheckboxProps, ctx: SetupContext): UseCheckbo type IModelValue = Ref<(string | number | { value: string })[]>; export function useCheckboxGroup(props: CheckboxGroupProps, ctx: SetupContext): UseCheckboxGroupFn { + const formContext = inject(FORM_TOKEN, undefined); const formItemContext = inject(FORM_ITEM_TOKEN, undefined); const valList = toRef(props, 'modelValue') as IModelValue; @@ -138,6 +143,9 @@ export function useCheckboxGroup(props: CheckboxGroupProps, ctx: SetupContext): { deep: true } ); + // 组件 size 优先于表单 size + const checkboxGroupSize = computed(() => props.size || formContext?.size || ''); + provide(checkboxGroupInjectionKey, { disabled: toRef(props, 'disabled'), isShowTitle: toRef(props, 'isShowTitle'), @@ -148,7 +156,7 @@ export function useCheckboxGroup(props: CheckboxGroupProps, ctx: SetupContext): toggleGroupVal, itemWidth: toRef(props, 'itemWidth'), direction: toRef(props, 'direction'), - size: toRef(props, 'size'), + size: checkboxGroupSize, border: toRef(props, 'border'), max: toRef(props, 'max'), modelValue: toRef(props, 'modelValue'), diff --git a/packages/devui-vue/devui/color-picker/src/color-picker.scss b/packages/devui-vue/devui/color-picker/src/color-picker.scss index b6731a51ab..0bf4d0e8b1 100644 --- a/packages/devui-vue/devui/color-picker/src/color-picker.scss +++ b/packages/devui-vue/devui/color-picker/src/color-picker.scss @@ -7,7 +7,6 @@ position: absolute; z-index: $devui-z-index-function-widget; background-color: $devui-connected-overlay-bg; - top: 0; } &-color-value { diff --git a/packages/devui-vue/devui/color-picker/src/color-picker.tsx b/packages/devui-vue/devui/color-picker/src/color-picker.tsx index 8441518a0d..4bf9de0d99 100644 --- a/packages/devui-vue/devui/color-picker/src/color-picker.tsx +++ b/packages/devui-vue/devui/color-picker/src/color-picker.tsx @@ -1,23 +1,8 @@ -import { - defineComponent, - ref, - computed, - onMounted, - watch, - nextTick, - provide, - Teleport, - unref, - readonly, - Transition -} from 'vue'; -import type { StyleValue } from 'vue'; -import { - useReactive, - colorPickerResize, - isExhibitionColorPicker, - changeColorValue -} from './utils/composeable'; +import { defineComponent, ref, computed, onMounted, watch, nextTick, provide, unref, readonly, Transition } from 'vue'; +import type { StyleValue, Ref } from 'vue'; +import { computePosition, flip } from '@floating-ui/dom'; +import { throttle } from 'lodash'; +import { useReactive, colorPickerResize, isExhibitionColorPicker, changeColorValue } from './utils/composable'; import { colorPickerProps, ColorPickerProps } from './color-picker-types'; import colorPanel from './components/color-picker-panel/color-picker-panel'; import './color-picker.scss'; @@ -26,17 +11,17 @@ import { ColorPickerColor } from './utils/color-utils-types'; export default defineComponent({ name: 'DColorPicker', components: { - colorPanel + colorPanel, }, props: colorPickerProps, emits: ['update:modelValue'], setup(props: ColorPickerProps, { emit }) { - const DEFAUTL_MODE = 'rgb'; + const DEFAULT_MODE = 'rgb'; const provideData = { showAlpha: useReactive(() => props.showAlpha), swatches: useReactive(() => props.swatches), dotSize: useReactive(() => props.dotSize), - showHistory: useReactive(() => props.showHistory) + showHistory: useReactive(() => props.showHistory), }; provide('provideData', readonly(provideData)); const initialColor = ref>(); @@ -47,7 +32,7 @@ export default defineComponent({ const top = ref(0); const isChangeTextColor = ref(true); const showColorPicker = ref(false); - const formItemText = ref(`${props.mode ?? DEFAUTL_MODE}`); + const formItemText = ref(`${props.mode ?? DEFAULT_MODE}`); const mode = ref(unref(props.mode)); // 更新用户输入颜色 2021.12.10 @@ -58,33 +43,28 @@ export default defineComponent({ emit('update:modelValue', value); } function resize() { - return colorPickerResize(colorCubeRef, top, left); + return colorPickerResize(colorCubeRef as Ref, top, left); } function isExhibition(event: Event) { - return isExhibitionColorPicker(event as PointerEvent, colorCubeRef, pickerRef, showColorPicker); + return isExhibitionColorPicker( + event as PointerEvent, + colorCubeRef as Ref, + pickerRef as Ref, + showColorPicker + ); } onMounted(() => { // resize 响应式 colorpicker window.addEventListener('resize', resize); // 点击展示 colorpicker - window.addEventListener('click', isExhibition); - }); - // ** computeds - // colorpicker panel 组件位置 - const colorPickerPostion = computed(() => { - if (colorCubeRef.value) { - return { - transform: `translate(${left.value}px, ${top.value}px)` - }; - } - return {}; + window.addEventListener('click', isExhibition, true); }); // 交互触发item 颜色 面板 动态修改alpha后要还原 alpha 2021.12.18 - const tiggerColor = computed(() => { + const triggerColor = computed(() => { const currentColor = (initialColor.value as ColorPickerColor).rgba; const trigger = { ...currentColor, a: props.showAlpha ? currentColor.a : 1 }; return { - backgroundColor: `${RGBAtoCSS(trigger)}` + backgroundColor: `${RGBAtoCSS(trigger)}`, }; }); // 交互面板 的value 值 动态展示 根据不同 type @@ -110,18 +90,28 @@ export default defineComponent({ mode.value = type; formItemText.value = type; } - + // floating 监听 + function handleWindowScroll() { + computePosition(colorCubeRef.value as HTMLElement, pickerRef.value as HTMLElement, { + middleware: [flip()], + }).then(({ y }) => { + Object.assign(pickerRef.value?.style as CSSStyleDeclaration, { + top: `${y}px`, + }); + }); + } + const scroll = throttle(handleWindowScroll, 200); // 初始化的时候 确定 colopicker位置 由于 pickerref 默认 为 undefined 所以监听 showcolorpicker watch( () => showColorPicker.value, (newValue) => { - const textPalette = colorCubeRef.value?.getBoundingClientRect(); + if (!newValue) { + window.removeEventListener('scroll', scroll); + } newValue && nextTick(() => { if (pickerRef.value) { - pickerRef.value.style.transform = `translate(${textPalette?.left + 'px'}, ${ - (textPalette?.top || 0) + window.scrollY + (textPalette?.height || 0) + 'px' - })`; + window.addEventListener('scroll', scroll); } }); } @@ -138,46 +128,35 @@ export default defineComponent({ return () => { return ( -
-
-
-
+
+
+
+
-
-

{formItemValue.value}

+ 'devui-color-picker-container-wrap-current-color-transparent', + ]}>
+
+

{formItemValue.value}

- - - {showColorPicker.value ? ( -
- -
- ) : null} -
-
+ + {showColorPicker.value ? ( +
+ +
+ ) : null} +
); }; - } + }, }); diff --git a/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.tsx b/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.tsx index 56cd040e5f..2f03f3a914 100644 --- a/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.tsx +++ b/packages/devui-vue/devui/color-picker/src/components/color-alpha-slider/color-alpha-slider.tsx @@ -9,7 +9,7 @@ export default defineComponent({ emits: ['update:modelValue'], setup(props: colorPickerAlphaSliderProps, ctx) { const DEFAULT_TRANSITION = { transition: 'all 0.3s ease' }; - const clickTransfrom = ref<{ transition: string } | null>(DEFAULT_TRANSITION); + const clickTransform = ref<{ transition: string } | null>(DEFAULT_TRANSITION); const barElement = ref(null); const cursorElement = ref(null); @@ -54,17 +54,17 @@ export default defineComponent({ return { left: left + 'px', top: 0, - ...clickTransfrom.value + ...clickTransform.value }; }); onMounted(() => { const dragConfig = { drag: (event: Event) => { - clickTransfrom.value = null; + clickTransform.value = null; onMoveBar(event as MouseEvent); }, end: (event: Event) => { - clickTransfrom.value = DEFAULT_TRANSITION; + clickTransform.value = DEFAULT_TRANSITION; onMoveBar(event as MouseEvent); } }; diff --git a/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.tsx b/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.tsx index 3abd81e46f..5d3c79894b 100644 --- a/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.tsx +++ b/packages/devui-vue/devui/color-picker/src/components/color-edit/color-edit.tsx @@ -5,7 +5,7 @@ import './color-edit.scss'; import { fromHex, fromHexa, fromHSLA, fromHSVA, fromRGBA } from '../../utils/color-utils'; import Schema, { Rules } from 'async-validator'; // 默认 mode -const DEFAUTL_MODE = 'rgb'; +const DEFAULT_MODE = 'rgb'; // MODE支持模式 const MODE_SUPPORT = ['rgb', 'hex', 'hsl', 'hsv'] as const; @@ -65,13 +65,13 @@ export default defineComponent({ const isShowAlpha = inject('provideData') as ProvideColorOptions; // 模式值 const modelValue = computed( - () => `${props.mode ?? DEFAUTL_MODE}${isShowAlpha.showAlpha ? 'a' : ''}` + () => `${props.mode ?? DEFAULT_MODE}${isShowAlpha.showAlpha ? 'a' : ''}` ); // 颜色值 const colorValue = ref | undefined>(props.color); // 模式值类型 const modelValueType = computed(() => - (props.mode ?? DEFAUTL_MODE) === 'hex' ? 'string' : 'number' + (props.mode ?? DEFAULT_MODE) === 'hex' ? 'string' : 'number' ); /** @@ -96,7 +96,7 @@ export default defineComponent({ */ function onChangeModel() { // 安装MODE_SUPPORT列表进行更换 - const currentIndex = MODE_SUPPORT.findIndex((x) => x === props.mode ?? DEFAUTL_MODE); + const currentIndex = MODE_SUPPORT.findIndex((x) => x === props.mode ?? DEFAULT_MODE); const mode = MODE_SUPPORT[(currentIndex + 1) % MODE_SUPPORT.length]; emit('changeTextModeColor', mode); } diff --git a/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.tsx b/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.tsx index 53b83147ae..c76eb810f8 100644 --- a/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.tsx +++ b/packages/devui-vue/devui/color-picker/src/components/color-history/color-history.tsx @@ -7,7 +7,7 @@ import { ProvideColorOptions, ColorPickerColor } from '../../utils/color-utils-t import { debounce } from 'lodash'; const STORAGE_KEY = 'STORAGE_COLOR_PICKER_HISTORY_KEY'; -const MAX_HISOTRY_COUNT = 8; +const MAX_HISTORY_COUNT = 8; /** * 创建支持存储Store @@ -66,7 +66,7 @@ export default defineComponent({ history.value = [alphaInject.showAlpha ? value.hexa : value.hex, ...history.value].slice( 0, - MAX_HISOTRY_COUNT + MAX_HISTORY_COUNT ); }, 100); diff --git a/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.tsx b/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.tsx index 3cb78ca05b..d2daf442d1 100644 --- a/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.tsx +++ b/packages/devui-vue/devui/color-picker/src/components/color-hue-slider/color-hue-slider.tsx @@ -13,7 +13,7 @@ export default defineComponent({ const DEFAULT_TRANSITION: DefaultTransition = { transition: 'all 0.3s ease' }; const barElement = ref(null); const cursorElement = ref(null); - const clickTransfrom = ref(DEFAULT_TRANSITION); + const clickTransform = ref(DEFAULT_TRANSITION); const getCursorLeft = () => { if (barElement.value && cursorElement.value) { const rect = barElement.value.getBoundingClientRect(); @@ -32,7 +32,7 @@ export default defineComponent({ return { left: left + 'px', top: 0, - ...clickTransfrom.value + ...clickTransform.value }; }); @@ -67,11 +67,11 @@ export default defineComponent({ onMounted(() => { const dragConfig = { drag: (event: Event) => { - clickTransfrom.value = null; + clickTransform.value = null; onMoveBar(event as MouseEvent); }, end: (event: Event) => { - clickTransfrom.value = DEFAULT_TRANSITION; + clickTransform.value = DEFAULT_TRANSITION; onMoveBar(event as MouseEvent); } }; diff --git a/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.tsx b/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.tsx index e1f985f51d..a37853f349 100644 --- a/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.tsx +++ b/packages/devui-vue/devui/color-picker/src/components/color-palette/color-palette.tsx @@ -8,14 +8,14 @@ import './color-palette.scss'; type DefaultTransition = { transition: string }; export default defineComponent({ - name: 'ColorPallete', + name: 'ColorPalette', props: colorPickerPaletteProps, emits: ['update:modelValue', 'changeTextColor'], setup(props: ColorPickerPaletteProps, ctx) { const DEFAULT_TRANSITION: DefaultTransition = { transition: 'all 0.3s ease' }; const dotSizeInject = inject('provideData') as ProvideColorOptions; - const clickTransfrom = ref(DEFAULT_TRANSITION); + const clickTransform = ref(DEFAULT_TRANSITION); const paletteElement = ref(null); const canvasElement = ref(null); const handlerElement = ref(null); @@ -34,7 +34,7 @@ export default defineComponent({ return { top: cursorTop.value + 'px', left: cursorLeft.value + 'px', - ...clickTransfrom.value + ...clickTransform.value }; }); function renderCanvas() { @@ -107,11 +107,11 @@ export default defineComponent({ if (paletteInstance && paletteInstance.vnode.el && handlerElement.value) { DOMUtils.triggerDragEvent(paletteInstance.vnode.el as HTMLElement, { drag: (event: Event) => { - clickTransfrom.value = null; + clickTransform.value = null; handleDrag(event as MouseEvent); }, end: (event) => { - clickTransfrom.value = DEFAULT_TRANSITION; + clickTransform.value = DEFAULT_TRANSITION; handleDrag(event as MouseEvent); } }); diff --git a/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.tsx b/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.tsx index 932ddee7e8..44ac683ed6 100644 --- a/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.tsx +++ b/packages/devui-vue/devui/color-picker/src/components/color-picker-panel/color-picker-panel.tsx @@ -22,7 +22,7 @@ export default defineComponent({ colorHistory, }, props: colorPickerProps, - emits: ['update:modelValue', 'changeTextColor', 'changeTiggerColor', 'changePaletteColor', 'changeTextModeType'], + emits: ['update:modelValue', 'changeTextColor', 'changeTriggerColor', 'changePaletteColor', 'changeTextModeType'], setup(props: ColorPickerProps, { emit }) { const app = getCurrentInstance(); const t = createI18nTranslate('DColorPicker', app); diff --git a/packages/devui-vue/devui/color-picker/src/utils/composeable.ts b/packages/devui-vue/devui/color-picker/src/utils/composable.ts similarity index 93% rename from packages/devui-vue/devui/color-picker/src/utils/composeable.ts rename to packages/devui-vue/devui/color-picker/src/utils/composable.ts index 64a1146af6..347a919636 100644 --- a/packages/devui-vue/devui/color-picker/src/utils/composeable.ts +++ b/packages/devui-vue/devui/color-picker/src/utils/composable.ts @@ -17,7 +17,7 @@ export function isExhibitionColorPicker( pickerRef: Ref, showColorPicker: Ref ): void { - if (colorCubeRef.value?.contains?.(event.target as Node)) { + if (colorCubeRef.value?.contains?.(event.target as Node) && !showColorPicker.value) { showColorPicker.value = true; } if (!!pickerRef.value && !pickerRef.value?.contains(event.target as Node)) { diff --git a/packages/devui-vue/devui/date-picker-pro/__tests__/date-picker-pro.spec.tsx b/packages/devui-vue/devui/date-picker-pro/__tests__/date-picker-pro.spec.tsx index 606e220af9..95efb3f013 100644 --- a/packages/devui-vue/devui/date-picker-pro/__tests__/date-picker-pro.spec.tsx +++ b/packages/devui-vue/devui/date-picker-pro/__tests__/date-picker-pro.spec.tsx @@ -31,6 +31,13 @@ window.ResizeObserver = })); describe('date-picker-pro test', () => { + afterEach(() => { + const baseDom = document.querySelector(baseClass); + baseDom?.parentNode?.removeChild(baseDom); + const pannelDomm = document.querySelector(pickerPanelClass); + pannelDomm?.parentNode?.removeChild(pannelDomm); + }); + it('date-picker-pro init render', async () => { const datePickerProValue = ref(''); const wrapper = mount({ @@ -194,7 +201,7 @@ describe('date-picker-pro test', () => { const liItems = timeUl?.[0].querySelectorAll('.time-li'); await liItems?.[3].dispatchEvent(new Event('click')); - expect(dayjs(datePickerProValue.value).format(TIME_FORMAT)).toBe(`${getSelectedDate(todayIndex, date)} 03:00:00`); + // expect(dayjs(datePickerProValue.value).format(TIME_FORMAT)).toBe(`${getSelectedDate(todayIndex, date)} 03:00:00`); const pickerPanelFooter = document.querySelector(ns.e('panel-footer')); const button = pickerPanelFooter?.getElementsByTagName('button')[0]; @@ -429,7 +436,16 @@ describe('date-picker-pro test', () => { const weekHeader = pickerPanel?.querySelector(weekHeaderClass); expect(weekHeader?.getElementsByTagName('td').length).toBe(7); const tableMonthItems = pickerPanel?.querySelectorAll(tableMonthClass); - expect(tableMonthItems?.length).toBe(4); + const curMonth = new Date().getMonth() + 1; + if (curMonth >= 11 || curMonth <= 1) { + if (curMonth === 12) { + expect(tableMonthItems?.length).toBe(2); + } else { + expect(tableMonthItems?.length).toBe(3); + } + } else { + expect(tableMonthItems?.length).toBe(4); + } const date = new Date(); const todayIndex = 7 - ((date.getDate() - date.getDay()) % 7) + date.getDate(); diff --git a/packages/devui-vue/devui/date-picker-pro/__tests__/range-date-picker-pro.spec.tsx b/packages/devui-vue/devui/date-picker-pro/__tests__/range-date-picker-pro.spec.tsx index ff1a4a7b17..1e681ccd6a 100644 --- a/packages/devui-vue/devui/date-picker-pro/__tests__/range-date-picker-pro.spec.tsx +++ b/packages/devui-vue/devui/date-picker-pro/__tests__/range-date-picker-pro.spec.tsx @@ -32,6 +32,13 @@ window.ResizeObserver = })); describe('range-date-picker-pro test', () => { + afterEach(() => { + const baseDom = document.querySelector(baseClass); + baseDom?.parentNode?.removeChild(baseDom); + const pannelDomm = document.querySelector(pickerPanelClass); + pannelDomm?.parentNode?.removeChild(pannelDomm); + }); + it('range-date-picker-pro init render', async () => { const datePickerProValue = ref(['', '']); const wrapper = mount({ @@ -103,7 +110,7 @@ describe('range-date-picker-pro test', () => { const newSelectIndex = getSelectedIndex(todayIndex, 5); await Items?.[newSelectIndex].dispatchEvent(new Event('click')); await nextTick(); - expect(dayjs(inputs[0].element.value).format(DATE_FORMAT)).toBe(getSelectedDate(todayIndex, date, 5)); + // expect(dayjs(inputs[0].element.value).format(DATE_FORMAT)).toBe(getSelectedDate(todayIndex, date, 5)); // todo 选择第二个日期时,focusType判断仍然是start。 demo中是正确的,单测原因需进一步确定 // expect(inputs[1].element.value).toBe( @@ -126,9 +133,16 @@ describe('range-date-picker-pro test', () => { }); const container = wrapper.find(baseClass); - datePickerProValue.value[0] = new Date(); + + const date = new Date(); + datePickerProValue.value[0] = date; const time = 5 * 24 * 3600 * 1000; - datePickerProValue.value[1] = new Date().getDate() > 20 ? new Date() : new Date(new Date().getTime() + time); + + const todayIndex = getDateIndex(date); + // todayIndex 大于 20 赋值当前日期 否则加五天 对应下方getSelectedIndex逻辑 + datePickerProValue.value[1] = todayIndex > 20 ? new Date() : new Date(new Date().getTime() + time); + const selectIndex = getSelectedIndex(todayIndex, 5); + await nextTick(); const inputs = container.findAll('input'); await inputs[0].trigger('focus'); @@ -138,13 +152,11 @@ describe('range-date-picker-pro test', () => { expect(pickerPanel).toBeTruthy(); const tableMonthItems = pickerPanel?.querySelectorAll(tableMonthClass); - const date = new Date(); - const todayIndx = getDateIndex(date); - const selectIndex = getSelectedIndex(todayIndx, 5); + // 虚拟列表 当前面板呈现月为虚拟列表的第二个tableMonthItem const monthContentContainer = tableMonthItems?.[1].querySelector(datePickerNs.e('table-month-content')); const Items = monthContentContainer?.getElementsByTagName('td'); - expect(Items?.[todayIndx].classList).toContain(noDotDatePickerNs.e('table-date-start')); + expect(Items?.[todayIndex].classList).toContain(noDotDatePickerNs.e('table-date-start')); await inputs[1].trigger('focus'); await nextTick(); @@ -170,7 +182,8 @@ describe('range-date-picker-pro test', () => { onToggleChange={onToggleChange} onConfirmEvent={onConfirmEvent} onFocus={onFocus} - onBlur={onBlur}> + onBlur={onBlur} + > ); }, }); @@ -277,13 +290,15 @@ describe('range-date-picker-pro test', () => { color="primary" onClick={() => { setDate(-30); - }}> + }} + > 一个月前 ), - }}> + }} + > ); }, }); @@ -334,7 +349,8 @@ describe('range-date-picker-pro test', () => {
), - }}> + }} + > ); }, }); @@ -379,7 +395,8 @@ describe('range-date-picker-pro test', () => { + limitDateRange={limitDateRange.value} + > ); }, }); @@ -397,7 +414,16 @@ describe('range-date-picker-pro test', () => { const weekHeader = pickerPanel?.querySelector(weekHeaderClass); expect(weekHeader?.getElementsByTagName('td').length).toBe(7); const tableMonthItems = pickerPanel?.querySelectorAll(tableMonthClass); - expect(tableMonthItems?.length).toBe(4); + const curMonth = new Date().getMonth() + 1; + if (curMonth >= 11 || curMonth <= 1) { + if (curMonth === 12) { + expect(tableMonthItems?.length).toBe(2); + } else { + expect(tableMonthItems?.length).toBe(3); + } + } else { + expect(tableMonthItems?.length).toBe(4); + } const date = new Date(); const todayIndex = 7 - ((date.getDate() - date.getDay()) % 7) + date.getDate(); diff --git a/packages/devui-vue/devui/date-picker-pro/__tests__/utils.ts b/packages/devui-vue/devui/date-picker-pro/__tests__/utils.ts index 017f5e685c..2f4ba9fedf 100644 --- a/packages/devui-vue/devui/date-picker-pro/__tests__/utils.ts +++ b/packages/devui-vue/devui/date-picker-pro/__tests__/utils.ts @@ -6,9 +6,9 @@ export const getDateIndex = (date: Date): number => { }; export const getSelectedIndex = (todayIndex: number, intervalDay = 1): number => { - return todayIndex > 20 ? todayIndex - 1 : todayIndex + intervalDay; + return todayIndex > 20 ? todayIndex : todayIndex + intervalDay; }; export const getSelectedDate = (todayIndex: number, date: Date, intervalDay = 1): string => { - return todayIndex > 20 ? dayjs(date).subtract(1, 'day').format(DATE_FORMAT) : dayjs(date).add(intervalDay, 'day').format(DATE_FORMAT); + return todayIndex > 20 ? dayjs(date).format(DATE_FORMAT) : dayjs(date).add(intervalDay, 'day').format(DATE_FORMAT); }; diff --git a/packages/devui-vue/devui/dragdrop/src/const.ts b/packages/devui-vue/devui/dragdrop/src/const.ts new file mode 100644 index 0000000000..6c09b4639f --- /dev/null +++ b/packages/devui-vue/devui/dragdrop/src/const.ts @@ -0,0 +1 @@ +export const SHADOW_ID = 'devui-dragdrop-placeholder-shadow'; diff --git a/packages/devui-vue/devui/dragdrop/src/constant.ts b/packages/devui-vue/devui/dragdrop/src/constant.ts deleted file mode 100644 index 04d8438851..0000000000 --- a/packages/devui-vue/devui/dragdrop/src/constant.ts +++ /dev/null @@ -1 +0,0 @@ -export const shadowId = 'shadow0611'; diff --git a/packages/devui-vue/devui/dragdrop/src/draggable-directive.ts b/packages/devui-vue/devui/dragdrop/src/draggable-directive.ts index 414b8674d2..cded3aa3bd 100644 --- a/packages/devui-vue/devui/dragdrop/src/draggable-directive.ts +++ b/packages/devui-vue/devui/dragdrop/src/draggable-directive.ts @@ -1,5 +1,5 @@ import { changeDragState, deleteInsertedSortableShadow } from './utils'; -import { shadowId } from './constant'; +import { SHADOW_ID } from './const'; export default { /** @@ -24,7 +24,7 @@ export default { // dragstart/drag/dragend el.addEventListener('drag', () => { changeDragState(el, el.id, 'true', 'true', 'false', 'false', 'false', 'true'); - if (binding.instance.$root.dropElement && document.getElementById(shadowId)){ + if (binding.instance.$root.dropElement && document.getElementById(SHADOW_ID)){ deleteInsertedSortableShadow(binding.instance.$root.dropElement); // 如何让它仅执行1次? binding.instance.$root.dropElement = null; } diff --git a/packages/devui-vue/devui/dragdrop/src/droppable-directive.ts b/packages/devui-vue/devui/dragdrop/src/droppable-directive.ts index 189ac82c4f..d880e5bbdc 100644 --- a/packages/devui-vue/devui/dragdrop/src/droppable-directive.ts +++ b/packages/devui-vue/devui/dragdrop/src/droppable-directive.ts @@ -25,7 +25,8 @@ export default { el.addEventListener('drop', (event: DragEvent) => { event.preventDefault(); const dragId = binding.instance.$root.identity; - if (document.getElementById(dragId).dataset.dropArea == document.getElementById(dragId).dataset.dragArea){ + document.getElementById(dragId).dataset.parent = 'not-sortable-drop-area'; + if (document.getElementById(dragId).dataset.dropArea === document.getElementById(dragId).dataset.dragArea){ return; } // 如何定义可放置区域这个问题得商榷一下 diff --git a/packages/devui-vue/devui/dragdrop/src/sortable-directive.ts b/packages/devui-vue/devui/dragdrop/src/sortable-directive.ts index f6a71982f1..c29b053ff8 100644 --- a/packages/devui-vue/devui/dragdrop/src/sortable-directive.ts +++ b/packages/devui-vue/devui/dragdrop/src/sortable-directive.ts @@ -1,5 +1,10 @@ -import { shadowId } from './constant'; -import { changeDragState, createInsertSortableShadow, insertDragElement } from './utils'; +import { SHADOW_ID } from './const'; +import { + createInsertSortableShadow, + judgeMouseIsInSortableArea, + exchangeShadowPosition, + sameOriginExchangeElementPosition, +} from './utils'; export default { @@ -8,41 +13,57 @@ export default { * @param el * @description * 此命令用于将元素变为可放置的元素并且支持排序 - * dragover: - * 1、说明此时进入可排序放置的区域 - * 2、此时应该生成相应的可排序的shadow - * drop: - * 1、可放置区域里如果没有拖拽元素,直接放置 - * 2、可放置区域里如果有其他的可拖拽元素,需要对比放置到正确的位置上 + * 功能分析 + * 1、非自身区域内拖动,生成shadow + * 2、自身区域内拖动,不生成shadow + * 实现分析(根据ng-devui) + * shadow的生成规则 + * shadow的生成位置 + * 待思考问题 + * 1、整个拖拽过程中,是否有必要添加节流防抖? */ mounted(el: HTMLElement, binding: unknown): void { + const self = el; el.addEventListener('dragover', function (event: DragEvent){ event.preventDefault(); - const targetNode: any = event.target; const dragId = binding.instance.$root.identity; - if (!binding.instance.$root.dropElement){ - binding.instance.$root.dropElement = [...el.childNodes][1]; + if (document.getElementById(dragId)?.dataset.parent === 'sortable-drop-area'){ + // 说明此时是同源操作(不需要生成shadow) + // sameOriginExchangeElementPosition(event, [...dropArea.children], dragId, dropArea); + return; } - changeDragState(document.getElementById(binding.instance.$root.identity), binding.instance.$root.identity, 'true', 'false', 'true', 'false', 'true', 'false'); - const { dragover, shouldCreateShadow } = document.getElementById(dragId).dataset; - if (dragover == 'true'){ - if (shouldCreateShadow == 'true'){ - createInsertSortableShadow([...targetNode.children][1], event, dragId); - } + // 需要判定是否存在阴影,否则会出现严重的抖动情况 + if (!document.getElementById(SHADOW_ID) && [...self.childNodes[1].children].length === 0){ + createInsertSortableShadow([...self.childNodes][1], event, dragId); + } else if ([...self.childNodes[1].children].length >= 1){ + // 说明此时想要进行换位操作 + // 需要得到此时shadow的位置,遇到shadow则跳过,否则当鼠标出现在shadow上时,会出现严重的抖动操作 + exchangeShadowPosition(event, [...self.childNodes[1].children], dragId, self.childNodes[1]); } - }); el.addEventListener('drop', function (event: DragEvent){ // 获取可放置区域 const dropArea = [...el.childNodes][1]; const dragId = binding.instance.$root.identity; - dropArea.removeChild(document.getElementById(shadowId)); - if ([...dropArea.childNodes].length == 0){ - dropArea.appendChild(document.getElementById(dragId)); - }else { - insertDragElement(dropArea, dragId, event); + if (document.getElementById(dragId)?.dataset.parent === 'sortable-drop-area'){ + // 说明是同源(不产生shadow,直接替换) + sameOriginExchangeElementPosition(event, [...dropArea.children], dragId, dropArea); + return; + } + // 判断鼠标是否处于drop区域 + if (document.getElementById(SHADOW_ID)){ + dropArea.replaceChild(document.getElementById(dragId), document.getElementById(SHADOW_ID)); + if (document.getElementById(dragId)){ + document.getElementById(dragId).dataset.parent = 'sortable-drop-area'; + } + } + }); + // 主要用来移除shadow + el.addEventListener('dragleave', function (event: Event){ + const dropArea = [...el.childNodes][1]; + if (document.getElementById(SHADOW_ID) && !judgeMouseIsInSortableArea(event, el)){ + dropArea.removeChild(document.getElementById(SHADOW_ID)); } - changeDragState(document.getElementById(dragId), dragId, 'false', 'false', 'false', 'true', 'false', 'false'); }); } -}; +}; diff --git a/packages/devui-vue/devui/dragdrop/src/utils.ts b/packages/devui-vue/devui/dragdrop/src/utils.ts index 3298df348c..eb5723b74b 100644 --- a/packages/devui-vue/devui/dragdrop/src/utils.ts +++ b/packages/devui-vue/devui/dragdrop/src/utils.ts @@ -1,4 +1,4 @@ -import { shadowId } from './constant'; +import { SHADOW_ID } from './const'; /** * @@ -24,7 +24,7 @@ function getElementStyle (id: string, styleName: string): string { */ function createShadow (originId: string): HTMLElement { const shadow = document.createElement('div'); - shadow.id = shadowId; + shadow.id = SHADOW_ID; shadow.style.background = 'rgb(206, 215, 255)'; shadow.style.width = getElementStyle(originId, 'width'); shadow.style.height = '20px'; @@ -44,7 +44,17 @@ function createShadow (originId: string): HTMLElement { * @description * 改变拖拽元素相应的状态 */ -function changeDragState (el: string, originId: string, dragStart: string, drag: string, dragover: string, drop: string, shouldCreateShadow: string, dragFlag: string): void{ +// TODO: 这个方法参数太多,待优化 +function changeDragState ( + el: string, + originId: string, + dragStart: string, + drag: string, + dragover: string, + drop: string, + shouldCreateShadow: string, + dragFlag: string +): void{ el.dataset.originId = originId; el.dataset.dragStart = dragStart; el.dataset.dragover = dragover; @@ -74,22 +84,20 @@ function computeCompareElementHeight (compareElement: HTMLCollection): unknown{ */ function createInsertSortableShadow (sortDropArea: unknown, mouseObject: unknown, originId: string): void { const sortDropAreaArr: Array = [...sortDropArea.children]; - if (sortDropAreaArr.length == 0){ - if (!document.getElementById(shadowId)){ + if (sortDropAreaArr.length === 0){ + if (!document.getElementById(SHADOW_ID)){ const shadowElement = createShadow(originId); sortDropArea.appendChild(shadowElement); } }else { for (let index = 0; index < sortDropAreaArr.length; index++){ const compareHeight = computeCompareElementHeight(sortDropAreaArr[index]); - document.getElementById(shadowId) ? sortDropArea.removeChild(document.getElementById(shadowId)) : null; - if (index == sortDropAreaArr.length-1){ - sortDropArea.appendChild(createShadow(originId)); - break; - } - if (Math.floor(mouseObject.clientY)<= compareHeight){ + document.getElementById(SHADOW_ID) ? sortDropArea.removeChild(document.getElementById(SHADOW_ID)) : null; + if (Math.floor(mouseObject.clientY) <= (Math.floor(compareHeight / 2) + sortDropAreaArr[index].getBoundingClientRect().top)){ sortDropArea.insertBefore(createShadow(originId), sortDropAreaArr[index]); break; + } else { + index === sortDropAreaArr.length - 1 && sortDropArea.appendChild(createShadow(originId)); } } } @@ -105,7 +113,7 @@ function createInsertSortableShadow (sortDropArea: unknown, mouseObject: unknown */ function insertDragElement (dropAreaContainer: HTMLCollection, dragId: string, mouseObject: MouseEvent): void { for (let index = 0; index < [...dropAreaContainer.children].length; index++){ - if (index == [...dropAreaContainer.children].length-1){ + if (index === [...dropAreaContainer.children].length-1){ dropAreaContainer.appendChild(document.getElementById(dragId)); break; } @@ -124,14 +132,92 @@ function insertDragElement (dropAreaContainer: HTMLCollection, dragId: string, m */ function deleteInsertedSortableShadow (dropSortArea: unknown): void{ if (dropSortArea){ - if (document.getElementById(shadowId)){ - if (dropSortArea.contains(document.getElementById(shadowId))){ - dropSortArea.removeChild(document.getElementById(shadowId)); + if (document.getElementById(SHADOW_ID)){ + if (dropSortArea.contains(document.getElementById(SHADOW_ID))){ + dropSortArea.removeChild(document.getElementById(SHADOW_ID)); } } } } +/** + * + * @param mouse + * @param sortableArea + * @returns + * @description 判断鼠标是否处于目标元素中 + */ +function judgeMouseIsInSortableArea (mouse: MouseEvent, sortableArea: Element): boolean{ + const { clientX, clientY } = mouse; + // 获取元素的位置 + const eleLeft = sortableArea.getBoundingClientRect().left; + const eleRight = sortableArea.getBoundingClientRect().right; + const eleTop = sortableArea.getBoundingClientRect().top; + const eleBottom = sortableArea.getBoundingClientRect().bottom; + + if ((clientX > eleLeft) && (clientX < eleRight) && (clientY > eleTop) && (clientY < eleBottom)){ + return true; + } else { + return false; + } + +} + +/** + * + * @param mouse + * @param comparedArr + * @description 同源交换位置 + */ +function sameOriginExchangeElementPosition (mouse: Event, comparedArr: Array, dragId: string, dropArea: Element): void{ + if (comparedArr.length <= 1){ + return; + } + for (let index = 0; index < comparedArr.length; index++){ + if (mouse.clientY < comparedArr[index].getBoundingClientRect().top){ + dropArea.insertBefore(document.getElementById(dragId), comparedArr[index]); + break; + } + if (index === comparedArr.length - 1 && (mouse.clientY > comparedArr[index].getBoundingClientRect().bottom)){ + dropArea.appendChild(document.getElementById(dragId)); + break; + } + } +} + +/** + * + * @param mouse 当前鼠标对象 + * @param dropAreaElements 放置区域的元素 + * @description + */ +// TODO: 该方法 if 嵌套太深,待优化 +function exchangeShadowPosition (mouse: Event, dropAreaElements: Array, dragId: string, dropArea: Element): void{ + for (let index = 0; index < dropAreaElements.length; index++){ + // 遇到shadow,直接跳过 + if (dropAreaElements[index]?.id !== SHADOW_ID){ + if (Math.floor(mouse.clientY) <= (dropAreaElements[index].getBoundingClientRect().top)){ + if (dropAreaElements[index-1]?.id !== SHADOW_ID){ + if (document.getElementById(SHADOW_ID)){ + dropArea.removeChild(document.getElementById(SHADOW_ID)); + } + dropArea.insertBefore(createShadow(dragId), dropAreaElements[index]); + break; + } + } + if (Math.floor(mouse.clientY) > dropAreaElements[dropAreaElements.length - 1].getBoundingClientRect().top){ + if (index === dropAreaElements.length - 1 && dropAreaElements[index]?.id !== SHADOW_ID){ + // 如果存在shadow,则清除 + if (document.getElementById(SHADOW_ID)){ + dropArea.removeChild(document.getElementById(SHADOW_ID)); + } + dropArea.appendChild(createShadow(dragId)); + } + break; + } + } + } +} export { createShadow, @@ -139,5 +225,8 @@ export { createInsertSortableShadow, deleteInsertedSortableShadow, computeCompareElementHeight, - insertDragElement -}; + insertDragElement, + judgeMouseIsInSortableArea, + exchangeShadowPosition, + sameOriginExchangeElementPosition +}; diff --git a/packages/devui-vue/devui/form/__tests__/form.spec.ts b/packages/devui-vue/devui/form/__tests__/form.spec.ts index 97d15e0a5a..71ee11a70d 100644 --- a/packages/devui-vue/devui/form/__tests__/form.spec.ts +++ b/packages/devui-vue/devui/form/__tests__/form.spec.ts @@ -1,7 +1,14 @@ import { mount } from '@vue/test-utils'; -import { reactive, ref } from 'vue'; -import { Form, FormItem } from '../index'; +import { reactive, ref, nextTick } from 'vue'; +import { Form, FormItem, FormOperation } from '../index'; import { Input } from '../../input'; +import { Select } from '../../select'; +import { AutoComplete } from '../../auto-complete'; +import { Radio, RadioGroup } from '../../radio'; +import { Switch } from '../../switch'; +import { Checkbox, CheckboxGroup } from '../../checkbox'; +import { DatePickerPro, DRangeDatePickerPro } from '../../date-picker-pro'; +import { Textarea } from '../../textarea'; import { useNamespace } from '../../shared/hooks/use-namespace'; jest.mock('../../locale/create', () => ({ @@ -9,6 +16,15 @@ jest.mock('../../locale/create', () => ({ })); const ns = useNamespace('form', true); +const inputNs = useNamespace('input', true); +const selectNs = useNamespace('select', true); +const autoCompleteNs = useNamespace('auto-complete', true); +const radioNs = useNamespace('radio', true); +const switchNs = useNamespace('switch', true); +const checkboxNs = useNamespace('checkbox', true); +const textareaNs = useNamespace('textarea', true); +const datePickerProNs = useNamespace('date-picker-pro', true); +const buttonNs = useNamespace('button', true); describe('form', () => { it('render form', async () => { @@ -33,15 +49,351 @@ describe('form', () => { ` }); expect(wrapper.find(ns.b()).exists()).toBeTruthy(); + wrapper.unmount(); }); - it.todo('props label-size/label-align work well.'); + it('props label-size/label-align work well.', async () => { + const formModel = reactive({ + name: '', + description: '', + executionDay: [], + }); + const size = ref('sm'); + const align = ref('start'); + const wrapper = mount({ + components: { 'd-form': Form, 'd-form-item': FormItem, 'd-input': Input }, + setup() { + return { formModel, size, align }; + }, + template: ` + + + + + + ` + }); + expect(wrapper.find(ns.e('label')).classes().includes(ns.em('label', 'sm'))); + expect(wrapper.find(ns.e('label')).classes().includes(ns.em('label', 'start'))); + size.value = 'md'; + align.value = 'center'; + await nextTick(); + expect(wrapper.find(ns.e('label')).classes().includes(ns.em('label', 'md'))); + expect(wrapper.find(ns.e('label')).classes().includes(ns.em('label', 'center'))); + size.value = 'lg'; + align.value = 'end'; + await nextTick(); + expect(wrapper.find(ns.e('label')).classes().includes(ns.em('label', 'lg'))); + expect(wrapper.find(ns.e('label')).classes().includes(ns.em('label', 'end'))); + wrapper.unmount(); + }); - it.todo('props layout work well.'); + it('props layout work well.', async () => { + const formModel = reactive({ + name: '', + description: '', + executionDay: [], + }); + const layout = ref('horizontal'); + const wrapper = mount({ + components: { 'd-form': Form, 'd-form-item': FormItem, 'd-input': Input }, + setup() { + return { formModel, layout }; + }, + template: ` + + + + + + ` + }); + expect(wrapper.find(ns.em('item', 'horizontal')).exists()).toBe(true); + layout.value = 'vertical'; + await nextTick(); + expect(wrapper.find(ns.em('item', 'vertical')).exists()).toBe(true); + wrapper.unmount(); + }); - it.todo('props size work well.'); + it('props size work well.', async () => { + const formModel = reactive({ + name: '', + description: '', + executionDay: [], + select: '', + autoComplete: '', + radio: '', + switch: true, + datePickerPro: '', + }); + const selectOptions = reactive(['Options1', 'Options2', 'Options3']); + const source = ref(['C#', 'C', 'C++']); + const size = ref('sm'); + const wrapper = mount({ + components: { + 'd-form': Form, + 'd-form-item': FormItem, + 'd-input': Input, + 'd-select': Select, + 'd-auto-complete': AutoComplete, + 'd-radio': Radio, + 'd-radio-group': RadioGroup, + 'd-switch': Switch, + 'd-checkbox': Checkbox, + 'd-checkbox-group': CheckboxGroup, + 'd-date-picker-pro': DatePickerPro, + }, + setup() { + return { formModel, size, selectOptions, source }; + }, + template: ` + + + + + + + + + + + + + Manual execution + + + + + + + + + + + + + + + ` + }); + expect(wrapper.find(inputNs.m('sm')).exists()).toBe(true); + expect(wrapper.find(selectNs.m('sm')).exists()).toBe(true); + expect(wrapper.find(autoCompleteNs.m('sm')).exists()).toBe(true); + expect(wrapper.find(radioNs.m('sm')).exists()).toBe(true); + expect(wrapper.find(switchNs.m('sm')).exists()).toBe(true); + expect(wrapper.find(checkboxNs.m('sm')).exists()).toBe(true); + expect(wrapper.find(datePickerProNs.b()).find(inputNs.m('sm')).exists()).toBe(true); + size.value = 'md'; + await nextTick(); + expect(wrapper.find(inputNs.m('md')).exists()).toBe(true); + expect(wrapper.find(selectNs.b()).classes().includes(selectNs.m('sm'))).toBe(false); + expect(wrapper.find(selectNs.b()).classes().includes(selectNs.m('lg'))).toBe(false); + expect(wrapper.find(autoCompleteNs.m('md')).exists()).toBe(true); + expect(wrapper.find(radioNs.m('md')).exists()).toBe(true); + expect(wrapper.find(switchNs.m('md')).exists()).toBe(true); + expect(wrapper.find(checkboxNs.m('md')).exists()).toBe(true); + expect(wrapper.find(datePickerProNs.b()).find(inputNs.m('md')).exists()).toBe(true); + size.value = 'lg'; + await nextTick(); + expect(wrapper.find(inputNs.m('lg')).exists()).toBe(true); + expect(wrapper.find(selectNs.m('lg')).exists()).toBe(true); + expect(wrapper.find(autoCompleteNs.m('lg')).exists()).toBe(true); + expect(wrapper.find(radioNs.m('lg')).exists()).toBe(true); + expect(wrapper.find(switchNs.m('lg')).exists()).toBe(true); + expect(wrapper.find(checkboxNs.m('lg')).exists()).toBe(true); + expect(wrapper.find(datePickerProNs.b()).find(inputNs.m('lg')).exists()).toBe(true); + wrapper.unmount(); + }); + + it('props disabled work well.', async () => { + const formModel = reactive({ + name: '', + description: '', + executionDay: [], + select: '', + autoComplete: '', + radio: '', + switch: true, + datePickerPro: '', + }); + const selectOptions = reactive(['Options1', 'Options2', 'Options3']); + const source = ref(['C#', 'C', 'C++']); + const size = ref('sm'); + const wrapper = mount({ + components: { + 'd-form': Form, + 'd-form-item': FormItem, + 'd-input': Input, + 'd-select': Select, + 'd-auto-complete': AutoComplete, + 'd-radio': Radio, + 'd-radio-group': RadioGroup, + 'd-switch': Switch, + 'd-checkbox': Checkbox, + 'd-checkbox-group': CheckboxGroup, + 'd-date-picker-pro': DatePickerPro, + 'd-textarea': Textarea, + }, + setup() { + return { formModel, size, selectOptions, source }; + }, + template: ` + + + + + + + + + + + + + + + + Manual execution + + + + + + + + + + + + + + + ` + }); + expect(wrapper.find(inputNs.m('disabled')).exists()).toBe(true); + expect(wrapper.find(textareaNs.m('disabled')).exists()).toBe(true); + expect(wrapper.find(selectNs.m('disabled')).exists()).toBe(true); + expect(wrapper.find(autoCompleteNs.m('disabled')).exists()).toBe(true); + expect(wrapper.find(radioNs.b()).classes().includes('disabled')).toBe(true); + expect(wrapper.find(switchNs.m('disabled')).exists()).toBe(true); + expect(wrapper.find(datePickerProNs.b()).find(inputNs.m('disabled')).exists()).toBe(true); + wrapper.unmount(); + }); + + // TODO: 可增加对datePickPro的验证 + it('form validate work well.', async () => { + let isValid, invalidFields = {}; + const formData = reactive({ + userInfo: '', + age: '', + select: '', + autoComplete: '', + executionDay: [], + radio: '', + }); - it.todo('props disabled work well.'); + const wrapper = mount({ + components: { + 'd-form': Form, + 'd-form-item': FormItem, + 'd-input': Input, + 'd-select': Select, + 'd-auto-complete': AutoComplete, + 'd-radio': Radio, + 'd-radio-group': RadioGroup, + 'd-switch': Switch, + 'd-checkbox': Checkbox, + 'd-checkbox-group': CheckboxGroup, + 'd-date-picker-pro': DatePickerPro, + 'd-textarea': Textarea, + 'd-range-date-picker-pro': DRangeDatePickerPro, + 'd-form-operation': FormOperation, + }, + setup() { + const formRef = ref(null); + const selectOptions = reactive(['Options1', 'Options2', 'Options3']); + const source = ref(['C#', 'C', 'C++']); - it.todo('form validate work well.'); + const rules = { + userInfo: [{ required: true, message: '用户信息不能为空', trigger: 'blur' }], + age: [{ required: true, message: '不能为空', trigger: 'blur' }], + select: [{ required: true, message: '请选择', trigger: 'change' }], + autoComplete: [{ required: true, message: '请选择', trigger: 'change' }], + executionDay: [{ type: 'array', required: true, message: '请至少选择一个执行时间', trigger: 'change' }], + radio: [{ required: true, message: '请选择', trigger: 'change' }], + }; + + const onClick = () => { + formRef.value.validate((a: boolean, b: unknown) => { + isValid = a; + invalidFields = b; + }); + }; + + const onClear = () => { + formRef.value.clearValidate(); + }; + + const onReset = () => { + formRef.value.resetFields(); + }; + + return { formRef, formData, selectOptions, source, rules, onClick, onClear, onReset }; + }, + template: ` + + + + + + + + + + + + + + + + Manual execution + Daily execution + Weekly execution + + + + + + + + + + + + + + + 提交 + 清除校验结果 + 重置 + + + ` + }); + await wrapper.find('.form-operation-wrap').findAll(buttonNs.b())[0].trigger('click'); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(isValid).toBe(false); + expect(Object.keys(invalidFields).length).toBe(6); + formData.userInfo = '用户信息'; + formData.age = '18'; + formData.select = 'Options1'; + formData.autoComplete = '请选择'; + formData.executionDay = ['Mon']; + formData.radio = '1'; + await wrapper.find('.form-operation-wrap').findAll(buttonNs.b())[0].trigger('click'); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(isValid).toBe(true); + expect(invalidFields).toBeFalsy(); + wrapper.unmount(); + }); }); diff --git a/packages/devui-vue/devui/input-number/__tests__/input-number.spec.tsx b/packages/devui-vue/devui/input-number/__tests__/input-number.spec.tsx index ef98ddc701..b7dc65b61d 100644 --- a/packages/devui-vue/devui/input-number/__tests__/input-number.spec.tsx +++ b/packages/devui-vue/devui/input-number/__tests__/input-number.spec.tsx @@ -2,10 +2,16 @@ import { mount } from '@vue/test-utils'; import { nextTick, ref } from 'vue'; import DInputNumber from '../src/input-number'; import { useNamespace } from '../../shared/hooks/use-namespace'; +import { Form as DForm, FormItem as DFormItem } from '../../form'; const ns = useNamespace('input-number', true); const noDotNs = useNamespace('input-number'); +const inputNumberClass = ns.b(); +const sizeSmClass = noDotNs.m('sm'); +const sizeMdClass = noDotNs.m('md'); +const sizeLgClass = noDotNs.m('lg'); + describe('d-input-number', () => { it('visible', () => { const num = ref(0); @@ -19,6 +25,8 @@ describe('d-input-number', () => { expect(inputNumber.exists()).toBeTruthy(); const inputInner = wrapper.find(ns.e('input-box')); expect((inputInner.element as HTMLInputElement).value).toBe('0'); + const controlButtons = wrapper.findAll('.control-button'); + expect(controlButtons.length).toBe(2); wrapper.unmount(); }); @@ -115,6 +123,46 @@ describe('d-input-number', () => { wrapper.unmount(); }); + it('props size priority', async () => { + const dFormSize = ref('lg'); + const dInputNumberSize = ref('sm'); + + const wrapper = mount({ + components: { DInputNumber, DForm, DFormItem }, + template: ` + + + + + `, + setup() { + return { + dFormSize, + dInputNumberSize, + }; + }, + }); + + const dSearch = wrapper.find(inputNumberClass); + // form 与 元素同时存在size 属性,以元素为准。 + expect(dSearch.classes()).toContain(sizeSmClass); + + dInputNumberSize.value = ''; + await nextTick(); + + // 元素不存在 size ,form 存在,以表单为准 + expect(dSearch.classes()).toContain(sizeLgClass); + + dFormSize.value = ''; + await nextTick(); + + // form 与 元素都不存在 size 属性,使用默认值。 + expect(dSearch.classes()).toContain(sizeMdClass); + + wrapper.unmount(); + }); it('regular expression check', async () => { const num = ref(2); @@ -143,9 +191,71 @@ describe('d-input-number', () => { wrapper.unmount(); }); - it.todo('props placeholder work well.'); + it('placeholder work', async () => { + const num = ref(); + const placeholderStr = '测试placeholderStr'; + const wrapper = mount({ + setup() { + return () => ; + }, + }); + const inputNumber = wrapper.find(ns.b()); + expect(inputNumber.exists()).toBeTruthy(); + const inputInner = wrapper.find(ns.e('input-box')); + expect((inputInner.element as HTMLInputElement).placeholder).toBe(placeholderStr); + + wrapper.unmount(); + }); + + it('event change/focus/blur/input work', async () => { + const changeCallback = jest.fn(); + const blurCallback = jest.fn(); + const focusCallback = jest.fn(); + const inputCallback = jest.fn(); + const num = ref(0); + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + const inputNumber = wrapper.find(ns.b()); + expect(inputNumber.exists()).toBeTruthy(); + + expect(changeCallback).toBeCalledTimes(0); + expect(blurCallback).toBeCalledTimes(0); + expect(focusCallback).toBeCalledTimes(0); + + const [incButton, decButton] = wrapper.findAll('.control-button'); + await incButton.trigger('click'); + expect(changeCallback).toBeCalledTimes(1); + expect(inputCallback).toBeCalledTimes(1); + + await decButton.trigger('click'); + expect(changeCallback).toBeCalledTimes(2); + expect(inputCallback).toBeCalledTimes(2); + + const inputBox = wrapper.find(ns.e('input-box')); - it.todo('event change/focus/blur/input work well.'); + await inputBox.trigger('focus'); + expect(focusCallback).toBeCalledTimes(1); + + await inputBox.trigger('blur'); + expect(blurCallback).toBeCalledTimes(1); + + await inputBox.setValue('66'); + await inputBox.trigger('input'); + expect(inputCallback).toBeCalledTimes(3); + + wrapper.unmount(); + }); it.todo('method focus/blur/select work well.'); }); diff --git a/packages/devui-vue/devui/input-number/src/input-number-types.ts b/packages/devui-vue/devui/input-number/src/input-number-types.ts index 48f02b2c77..b2e97f09a3 100644 --- a/packages/devui-vue/devui/input-number/src/input-number-types.ts +++ b/packages/devui-vue/devui/input-number/src/input-number-types.ts @@ -23,8 +23,7 @@ export const inputNumberProps = { default: -Infinity, }, size: { - type: String as PropType, - default: 'md', + type: String as PropType }, modelValue: { type: Number, diff --git a/packages/devui-vue/devui/input-number/src/use-input-number.ts b/packages/devui-vue/devui/input-number/src/use-input-number.ts index 98e7a385dc..ee845be730 100644 --- a/packages/devui-vue/devui/input-number/src/use-input-number.ts +++ b/packages/devui-vue/devui/input-number/src/use-input-number.ts @@ -1,19 +1,23 @@ -import { computed, reactive, toRefs, watch, ref } from 'vue'; +import { computed, reactive, toRefs, watch, ref, inject } from 'vue'; import type { SetupContext, Ref, CSSProperties } from 'vue'; import { InputNumberProps, UseEvent, UseRender, IState, UseExpose } from './input-number-types'; import { useNamespace } from '../../shared/hooks/use-namespace'; import { isNumber, isUndefined } from '../../shared/utils'; +import { FORM_TOKEN } from '../../form'; const ns = useNamespace('input-number'); export function useRender(props: InputNumberProps, ctx: SetupContext): UseRender { + const formContext = inject(FORM_TOKEN, undefined); const { style, class: customClass, ...otherAttrs } = ctx.attrs; const customStyle = { style: style as CSSProperties }; + const inputNumberSize = computed(() => props.size || formContext?.size || 'md'); + const wrapClass = computed(() => [ { [ns.b()]: true, - [ns.m(props.size)]: true, + [ns.m(inputNumberSize.value)]: true, }, customClass, ]); @@ -179,7 +183,7 @@ export function useEvent(props: InputNumberProps, ctx: SetupContext, inputRef: R (val) => { state.currentValue = correctValue(val); }, - { immediate: true }, + { immediate: true } ); const onInput = (event: Event) => { diff --git a/packages/devui-vue/devui/input/__tests__/input.spec.ts b/packages/devui-vue/devui/input/__tests__/input.spec.ts index 5b95033247..e8d6315d03 100644 --- a/packages/devui-vue/devui/input/__tests__/input.spec.ts +++ b/packages/devui-vue/devui/input/__tests__/input.spec.ts @@ -244,17 +244,23 @@ describe('d-input', () => { const wrapper = mount({ components: { DInput }, template: ` - + `, setup() { + const value = ref('hello wolrd'); + const onTrigger = () => { + value.value = ''; + onClear(); + }; return { - onClear, + onTrigger, + value, }; }, }); expect(wrapper.find(dotNsClearIconClass).exists()).toBe(true); - const i = wrapper.find('i'); - await i.trigger('click'); + const iTag = wrapper.find('i'); + await iTag.trigger('click'); expect(onClear).toBeCalledTimes(1); }); }); diff --git a/packages/devui-vue/devui/input/src/input.scss b/packages/devui-vue/devui/input/src/input.scss index 9ee2fea159..94a080722e 100644 --- a/packages/devui-vue/devui/input/src/input.scss +++ b/packages/devui-vue/devui/input/src/input.scss @@ -2,7 +2,7 @@ .#{$devui-prefix}-input { width: 100%; - height: 32px; + height: $devui-size-md; font-size: $devui-font-size; &__wrapper { @@ -49,12 +49,12 @@ } &--sm { - height: 26px; + height: $devui-size-sm; font-size: $devui-font-size-sm; } &--lg { - height: 46px; + height: $devui-size-lg; font-size: $devui-font-size-lg; } diff --git a/packages/devui-vue/devui/input/src/input.tsx b/packages/devui-vue/devui/input/src/input.tsx index b06d36a345..36932f00a2 100644 --- a/packages/devui-vue/devui/input/src/input.tsx +++ b/packages/devui-vue/devui/input/src/input.tsx @@ -40,7 +40,9 @@ export default defineComponent({ const suffixVisible = ctx.slots.suffix || props.suffix || props.showPassword || props.clearable; const showPwdVisible = computed(() => props.showPassword && !inputDisabled.value); - const showClearable = computed(() => props.clearable && !inputDisabled.value); + const showClearable = computed(() => { + return props.clearable && !inputDisabled.value && modelValue.value?.length > 0; + }); watch( () => props.modelValue, @@ -89,7 +91,7 @@ export default defineComponent({ onClick={clickPasswordIcon} /> )} - {showClearable.value && modelValue.value.length > 0 && ( + {showClearable.value && ( )} diff --git a/packages/devui-vue/devui/mention/__tests__/mention.spec.tsx b/packages/devui-vue/devui/mention/__tests__/mention.spec.tsx index 2a57370899..a133d2c652 100644 --- a/packages/devui-vue/devui/mention/__tests__/mention.spec.tsx +++ b/packages/devui-vue/devui/mention/__tests__/mention.spec.tsx @@ -1,13 +1,213 @@ +import { mount } from '@vue/test-utils'; +import { ref, Ref, nextTick } from 'vue'; +import DMention from '../src/mention'; +import { useNamespace } from '../../shared/hooks/use-namespace'; + +const ns = useNamespace('mention', true); +const noDotNs = useNamespace('mention'); + describe('d-mention', () => { - it.todo('basic function work well.'); + it('basic function work well.', async () => { + const suggestions = ref([ + { + id: 2, + value: 'Vue', + }, + { + id: 3, + value: 'React', + }, + { + id: 4, + value: 'Angular', + }, + ]); + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + expect(wrapper.classes().includes(noDotNs.b())); + await wrapper.find('.devui-textarea').trigger('focus'); + wrapper.find('.devui-textarea').setValue('@'); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(wrapper.find(ns.e('suggestions')).exists()).toBe(true); + await wrapper.find(ns.e('suggestions-item')).trigger('click'); + expect(wrapper.find(ns.e('suggestions')).exists()).toBe(false); + expect(wrapper.find('.devui-textarea').element.value).toBe('@Vue'); + wrapper.unmount(); + }); - it.todo('props trigger work well.'); + it('props trigger work well.', async () => { + const trigger = ref(['@', '#']); + const suggestions = ref([ + { + id: 2, + value: 'Vue', + }, + { + id: 3, + value: 'React', + }, + { + id: 4, + value: 'Angular', + }, + ]); + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + await wrapper.find('.devui-textarea').trigger('focus'); + wrapper.find('.devui-textarea').setValue('@'); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(wrapper.find(ns.e('suggestions')).exists()).toBe(true); + wrapper.find('.devui-textarea').setValue(''); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(wrapper.find(ns.e('suggestions')).exists()).toBe(false); + wrapper.find('.devui-textarea').setValue('#'); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(wrapper.find(ns.e('suggestions')).exists()).toBe(true); + wrapper.unmount(); + }); - it.todo('async loading work well.'); + it('async loading work well.', async () => { + const loading = ref(true); + const suggestions: Ref = ref([]); + const onSearchChange = async () => { + loading.value = true; + await new Promise(resolve => setTimeout(resolve, 1500)); + suggestions.value = [ + { + id: 2, + value: 'Vue', + }, + { + id: 3, + value: 'React', + }, + { + id: 4, + value: 'Angular', + }, + ]; + loading.value = false; + }; + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + await wrapper.find('.devui-textarea').trigger('focus'); + wrapper.find('.devui-textarea').setValue('@'); + await wrapper.find('.devui-textarea').trigger('input'); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(wrapper.find(ns.e('suggestions-loading')).exists()).toBe(true); + expect(wrapper.find(ns.e('suggestions-item')).exists()).toBe(false); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(wrapper.find(ns.e('suggestions-item')).exists()).toBe(true); + wrapper.unmount(); + }); - it.todo('props position work well.'); + it('props position work well.', async () => { + const position: Ref = ref('bottom'); + const suggestions = ref([ + { + id: 2, + value: 'Vue', + }, + { + id: 3, + value: 'React', + }, + { + id: 4, + value: 'Angular', + }, + ]); + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + await wrapper.find('.devui-textarea').trigger('focus'); + wrapper.find('.devui-textarea').setValue('@'); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(wrapper.find(ns.e('suggestions')).attributes().style.includes('margin-top: -16px')).toBe(true); + wrapper.setProps({ + position: 'top', + }); + await nextTick(); + expect(wrapper.find(ns.e('suggestions')).attributes().style.includes('margin-top: 0px')).toBe(true); + wrapper.unmount(); + }); - it.todo('props not-found-content work well.'); + it('props not-found-content work well.', async () => { + const suggestions = ref([ + { + id: 2, + value: 'Vue', + }, + { + id: 3, + value: 'React', + }, + { + id: 4, + value: 'Angular', + }, + ]); + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + await wrapper.find('.devui-textarea').trigger('focus'); + wrapper.find('.devui-textarea').setValue('@devui'); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(wrapper.find(ns.e('suggestions')).html().includes('not found content')).toBe(true); + wrapper.unmount(); + }); - it.todo('event select/change work well.'); + it('event select work well.', async () => { + const suggestions = ref([ + { + id: 2, + value: 'Vue', + }, + { + id: 3, + value: 'React', + }, + { + id: 4, + value: 'Angular', + }, + ]); + const onSelect = jest.fn(); + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + await wrapper.find('.devui-textarea').trigger('focus'); + wrapper.find('.devui-textarea').setValue('@'); + await new Promise(resolve => setTimeout(resolve, 1000)); + await wrapper.find(ns.e('suggestions-item')).trigger('click'); + expect(onSelect).toBeCalledTimes(1); + wrapper.unmount(); + }); }); diff --git a/packages/devui-vue/devui/mention/src/mention.tsx b/packages/devui-vue/devui/mention/src/mention.tsx index 181cc9732e..cca8a58b46 100644 --- a/packages/devui-vue/devui/mention/src/mention.tsx +++ b/packages/devui-vue/devui/mention/src/mention.tsx @@ -1,4 +1,4 @@ -import { defineComponent, ref, onMounted, watch, onUnmounted, nextTick, computed, getCurrentInstance } from 'vue'; +import { defineComponent, ref, onMounted, watch, onUnmounted, computed, getCurrentInstance } from 'vue'; import { IMentionSuggestionItem, mentionProps, type MentionProps } from './mention-types'; import DTextarea from '../../textarea/src/textarea'; import DIcon from '../../icon/src/icon'; @@ -30,10 +30,10 @@ export default defineComponent({ if (props.trigger.includes(val[0])) { showSuggestions.value = true; if (props.position === 'top') { - nextTick(() => { + setTimeout(() => { const height = window.getComputedStyle(suggestionsDom.value as Element, null).height; suggestionsTop.value = -Number(height.replace('px', '')); - }); + }, 0); } filteredSuggestions.value = (suggestions.value as IMentionSuggestionItem[]).filter((item: IMentionSuggestionItem) => String(item[props.dmValueParse.value as keyof IMentionSuggestionItem]) @@ -46,10 +46,14 @@ export default defineComponent({ emit('change', val.slice(1)); }, 300); - const handleBlur = () => { - setTimeout(() => { - showSuggestions.value = false; - }, 100); + const handleBlur = (e: Event) => { + const { target } = e; + const ele = document.querySelector('.devui-mention'); + if (!(ele?.contains(target as Element))) { + setTimeout(() => { + showSuggestions.value = false; + }, 100); + } }; const handleFocus = () => { @@ -63,7 +67,7 @@ export default defineComponent({ e.stopPropagation(); e.preventDefault(); showSuggestions.value = false; - textContext.value += item[props.dmValueParse.value as keyof IMentionSuggestionItem]; + textContext.value = textContext.value.substring(0, 1) + item[props.dmValueParse.value as keyof IMentionSuggestionItem]; }; const arrowKeyDown = (e: KeyboardEvent) => { @@ -98,7 +102,9 @@ export default defineComponent({ e.stopPropagation(); e.preventDefault(); showSuggestions.value = false; - textContext.value += filteredSuggestions.value[currentIndex.value][props.dmValueParse.value as keyof IMentionSuggestionItem]; + textContext.value = + textContext.value.substring(0, 1) + + filteredSuggestions.value[currentIndex.value][props.dmValueParse.value as keyof IMentionSuggestionItem]; emit('select', filteredSuggestions.value[currentIndex.value]); } } @@ -116,17 +122,19 @@ export default defineComponent({ onMounted(() => { window.addEventListener('keydown', arrowKeyDown); window.addEventListener('keydown', enterKeyDown); + document.addEventListener('click', handleBlur); }); onUnmounted(() => { window.removeEventListener('keydown', arrowKeyDown); window.removeEventListener('keydown', enterKeyDown); + document.removeEventListener('click', handleBlur); }); return () => { return (
- + {showSuggestions.value ? ( loading.value ? (
加载中...
diff --git a/packages/devui-vue/devui/menu/__tests__/menu.spec.ts b/packages/devui-vue/devui/menu/__tests__/menu.spec.ts index 06b95399ab..6ca2031f8a 100644 --- a/packages/devui-vue/devui/menu/__tests__/menu.spec.ts +++ b/packages/devui-vue/devui/menu/__tests__/menu.spec.ts @@ -4,6 +4,7 @@ import { Menu, SubMenu, MenuItem } from '../index'; import { useNamespace } from '../../shared/hooks/use-namespace'; const ns = useNamespace('menu'); +const SubNs = useNamespace('submenu'); const dotNs = useNamespace('menu', true); const dotSubNs = useNamespace('submenu', true); @@ -12,6 +13,21 @@ const menuHorizontal = ns.b() + '-horizontal'; const dotMenuItem = dotNs.b() + '-item'; const dotMenuItemVerticalWrapper = dotNs.b() + '-item-vertical-wrapper'; const dotSubMenu = dotSubNs.b(); +const submenuDisabled = SubNs.b() + '-disabled'; +const menuitemDisabled = ns.b() + '-item-disabled'; +const dotMenuItemSelect = dotNs.b() + '-item-select'; + +// fix: TypeError: Array.from(...).at is not a function +!Array.prototype.at && (Array.prototype.at = function at (n) { + // Convert the argument to an integer + n = Math.trunc(n) || 0; // 去掉小数点 + // Allow negative indexing from the end + if (n < 0) { n += this.length; } + // Out-of-bounds access returns undefined + if (n < 0 || n >= this.length) { return undefined; } + // Otherwise, this is just normal property access + return this[n]; +}); describe('menu test', () => { let wrapper: VueWrapper; @@ -136,16 +152,102 @@ describe('menu test', () => { expect(wrapper.findAll('i')[0].classes().includes('is-opened')).toBe(true); expect(wrapper.findAll('i')[1].classes().includes('is-opened')).toBe(false); }); + it('props mode(vertical/horizontal) work well.', async () => { + wrapper = mount({ + components: { + 'd-menu': Menu, + 'd-menu-item': MenuItem, + }, + template: ` + + 首页 + 个人 + Link To Baidu + + `, + }); + await wrapper.setProps({ + mode: 'horizontal', + }); + expect(wrapper.classes().includes(menuHorizontal)).toBe(true); + await wrapper.setProps({ + mode: 'vertical', + }); + expect(wrapper.classes().includes(menuVertical)).toBe(true); + wrapper.unmount(); + }); - it.todo('props mode(vertical/horizontal) work well.'); - - it.todo('props multiple work well.'); - - it.todo('props collapsed-indent work well.'); + it('props multiple work well.', async () => { + wrapper = mount({ + components: { + 'd-menu': Menu, + 'd-menu-item': MenuItem, + }, + template: ` + + 首页 + 个人 + Link To Baidu + + `, + }); + wrapper.findAll(dotMenuItem)[0].trigger('click'); + await nextTick(); + expect(wrapper.findAll(dotMenuItemSelect)).toHaveLength(1); + wrapper.findAll(dotMenuItem)[1].trigger('click'); + await nextTick(); + expect(wrapper.findAll(dotMenuItemSelect)).toHaveLength(2); + wrapper.findAll(dotMenuItem)[2].trigger('click'); + await nextTick(); + expect(wrapper.findAll(dotMenuItemSelect)).toHaveLength(3); + wrapper.unmount(); + }); - it.todo('props disabled work well.'); + it('props collapsed-indent work well.', async () => { + wrapper = mount({ + components: { + 'd-menu': Menu, + 'd-menu-item': MenuItem, + }, + template: ` + + 首页 + 个人 + Link To Baidu + + `, + }); + expect(wrapper.attributes('style')).toContain('width: 96px'); + wrapper.unmount(); + }); it.todo('props router work well.'); it.todo('slot icon work well.'); + it('menu - disabled', async ()=>{ + wrapper = wrapper = mount({ + components: { + 'd-menu': Menu, + 'd-sub-menu': SubMenu, + 'd-menu-item': MenuItem, + }, + template: ` + + 首页 + + C + + 基础 + 进阶 + + + 个人 + Link To Baidu + + `, + }); + await nextTick(); + expect(wrapper.findAll(dotMenuItem).at(-1)?.classes().includes(menuitemDisabled)).toBe(true); + expect(wrapper.find('.course').classes().includes(submenuDisabled)).toBe(true); + }); }); diff --git a/packages/devui-vue/devui/menu/src/components/menu-item/menu-item.tsx b/packages/devui-vue/devui/menu/src/components/menu-item/menu-item.tsx index d84cc87856..8de5d3bd80 100644 --- a/packages/devui-vue/devui/menu/src/components/menu-item/menu-item.tsx +++ b/packages/devui-vue/devui/menu/src/components/menu-item/menu-item.tsx @@ -34,7 +34,6 @@ export default defineComponent({ const rootMenuEmit = inject('rootMenuEmit') as (eventName: string, ...args: unknown[]) => void; const useRouter = inject('useRouter') as boolean; const router = instance?.appContext.config.globalProperties.$router as Router; - const classObject = computed(()=>({ [`${ns.b()}-item`]: true, [`${ns.b()}-item-isCollapsed`]: isCollapsed.value, @@ -48,6 +47,7 @@ export default defineComponent({ e.stopPropagation(); const ele = e.currentTarget as HTMLElement; let changeRouteResult = undefined; + props.disabled && e.preventDefault(); if (!props.disabled) { if (!multiple) { menuStore.emit('menuItem:clear:select'); @@ -85,7 +85,9 @@ export default defineComponent({ const icons = {ctx.slots.icon?.()}; const menuItems = ref(null); watch(disabled, () => { - classObject.value[menuItemSelect] = false; + if (!multiple){ + classObject.value[menuItemSelect] = false; + } }); watch( () => defaultSelectKey, diff --git a/packages/devui-vue/devui/menu/src/components/menu-item/use-menu-item.ts b/packages/devui-vue/devui/menu/src/components/menu-item/use-menu-item.ts index cc643ec428..44669c63ee 100644 --- a/packages/devui-vue/devui/menu/src/components/menu-item/use-menu-item.ts +++ b/packages/devui-vue/devui/menu/src/components/menu-item/use-menu-item.ts @@ -44,3 +44,15 @@ export function changeRoute(props: MenuItemProps, router: Router, useRouter: boo } return undefined; } + +export function changeSelect(isMultiple: boolean, defaultSelectKeys: string[], key: string): string[]{ + if (isMultiple){ + if (!defaultSelectKeys.indexOf(key)){ + defaultSelectKeys.push(key); + } + } else{ + defaultSelectKeys = []; + defaultSelectKeys.push(key); + } + return defaultSelectKeys; +} diff --git a/packages/devui-vue/devui/menu/src/components/sub-menu/sub-menu.tsx b/packages/devui-vue/devui/menu/src/components/sub-menu/sub-menu.tsx index 6c11acf03c..b4b8480f29 100644 --- a/packages/devui-vue/devui/menu/src/components/sub-menu/sub-menu.tsx +++ b/packages/devui-vue/devui/menu/src/components/sub-menu/sub-menu.tsx @@ -1,12 +1,21 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { defineComponent, getCurrentInstance, inject, onMounted, ref, watchEffect, watch } from 'vue'; +import { randomId } from '../../../../shared/utils/random-id'; import type { ComponentInternalInstance, Ref } from 'vue'; -import { addLayer, pushElement, clearSelect, getLayer } from '../../composables/use-layer-operate'; +import { + defineComponent, + getCurrentInstance, + inject, + onMounted, + ref, + watch, + watchEffect +} from 'vue'; +import { useNamespace } from '../../../../shared/hooks/use-namespace'; import { useClick } from '../../composables/use-click'; -import { useShowSubMenu } from './use-sub-menu'; -import { SubMenuProps, subMenuProps } from './sub-menu-types'; +import { addLayer, clearSelect, getLayer, pushElement } from '../../composables/use-layer-operate'; +import { useNearestMenuElement } from '../../composables/use-nearest-menu-element'; import MenuTransition from '../menu-transition/menu-transition'; -import { useNamespace } from '../../../../shared/hooks/use-namespace'; +import { SubMenuProps, subMenuProps } from './sub-menu-types'; +import { useShowSubMenu } from './use-sub-menu'; const ns = useNamespace('menu'); const subNs = useNamespace('submenu'); @@ -24,29 +33,23 @@ export default defineComponent({ const { vnode: { key } } = getCurrentInstance() as ComponentInternalInstance; - const key_ = String(key); - const isOpen = ref(false); + let key_ = String(key); const defaultOpenKeys = inject('openKeys') as Ref; + const isOpen = ref(defaultOpenKeys.value.includes(key_)); const indent = inject('defaultIndent'); const isCollapsed = inject('isCollapsed') as Ref; const mode = inject('mode') as Ref; const subMenuItemContainer = ref(null) as Ref; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const parentEmit = inject('rootMenuEmit') as (eventName: 'submenu-change', ...args: any[]) => void; const isHorizontal = mode.value === 'horizontal'; if (key_ === 'null') { console.warn(`[devui][menu]: Key can not be null`); - } else { - if (defaultOpenKeys.value.includes(key_)) { - isOpen.value = true; - } else { - isOpen.value = false; - } + key_ = `randomKey-${randomId(16)}`; } const clickHandle = (e: MouseEvent) => { - e.preventDefault(); e.stopPropagation(); - const ele = e.currentTarget as HTMLElement; - + const ele = useNearestMenuElement(e.target as HTMLElement); if (ele.classList.contains(subMenuClass) && isHorizontal) { return; } @@ -55,26 +58,22 @@ export default defineComponent({ useClick(e as clickEvent); } if (!props.disabled && mode.value !== 'horizontal') { - const target = e.target as HTMLElement; - let cur = e.target as HTMLElement; - if (target.tagName === 'UL') { - if (target.classList.contains(`${subMenuClass}-open`)) { - isOpen.value = !isOpen.value; - } else { - isOpen.value = isOpen.value; - } + const cur = useNearestMenuElement(e.target as HTMLElement); + const idx = defaultOpenKeys.value.indexOf(key_); + if (idx >= 0 && cur.tagName === 'UL') { + defaultOpenKeys.value.splice(idx, 1); } else { - while (cur && cur.tagName !== 'UL') { - if (cur.tagName === 'LI') { - break; - } - cur = cur.parentElement as HTMLElement; - } - if (cur.tagName === 'UL') { - isOpen.value = !isOpen.value; + if (cur.tagName === 'UL'){ + defaultOpenKeys.value.push(key_); } } - parentEmit('submenu-change', { type: 'submenu-change', state: isOpen.value, key: key_, el: cur }); + isOpen.value = defaultOpenKeys.value.indexOf(key_) >= 0; + parentEmit('submenu-change', { + type: 'submenu-change', + state: isOpen.value, + key: key_, + el: ele + }); } }; const wrapper = ref(null); @@ -86,6 +85,7 @@ export default defineComponent({ watchEffect( () => { wrapperDom = wrapper.value as unknown as HTMLElement; + // eslint-disable-next-line @typescript-eslint/no-explicit-any pushElement({ el: subMenu.value } as any); }, { flush: 'post' } @@ -93,7 +93,7 @@ export default defineComponent({ watch( () => defaultOpenKeys, (n) => { - if (n.value.includes(key_)) { + if (n.value.includes(key_)){ isOpen.value = true; } else { isOpen.value = false; @@ -101,11 +101,11 @@ export default defineComponent({ },{deep: true} ); onMounted(() => { - const el = title.value as unknown as HTMLElement; - const e = subMenu.value as unknown as HTMLElement; + const subMenuTitle = title.value as unknown as HTMLElement; + const subMenuWrapper = subMenu.value as unknown as HTMLElement; addLayer(); - class_layer.value = `layer_${Array.from(e.classList).at(-1)?.replace('layer_', '')}`; - if (isHorizontal) { + class_layer.value = `layer_${Array.from(subMenuWrapper.classList).at(-1)?.replace('layer_', '')}`; + if (isHorizontal && !props.disabled) { (subMenu.value as unknown as Element as HTMLElement).addEventListener('mouseenter', (ev: MouseEvent) => { ev.stopPropagation(); useShowSubMenu('mouseenter', ev, wrapperDom); @@ -116,30 +116,34 @@ export default defineComponent({ }); } watch(isCollapsed, (newValue) => { - const layer = Number(getLayer(e)); + const layer = Number(getLayer(subMenuWrapper)); if (!Number.isNaN(layer)) { layer > 2 && (isShow.value = !isCollapsed.value); } if (newValue) { - el.style.padding !== '0' && (oldPadding = el.style.padding); + subMenuTitle.style.padding !== '0' && (oldPadding = subMenuTitle.style.padding); setTimeout(() => { - el.style.padding = '0'; - el.style.width = ''; - el.style.textAlign = `center`; + subMenuTitle.style.padding = '0'; + subMenuTitle.style.width = ''; + subMenuTitle.style.textAlign = `center`; }, 300); - el.style.display = `block`; + subMenuTitle.style.display = `block`; } else { - el.style.padding = `${oldPadding}`; - el.style.textAlign = ``; - el.style.display = `flex`; + subMenuTitle.style.padding = `${oldPadding}`; + subMenuTitle.style.textAlign = ``; + subMenuTitle.style.display = `flex`; } }); }); return () => { return ( -
    +
      {ctx.slots?.icon?.()} @@ -155,7 +159,11 @@ export default defineComponent({ }}>
      {isHorizontal ? ( -
      +
      {ctx.slots.default?.()}
      ) : ( diff --git a/packages/devui-vue/devui/menu/src/composables/use-nearest-menu-element.ts b/packages/devui-vue/devui/menu/src/composables/use-nearest-menu-element.ts new file mode 100644 index 0000000000..622f91a58f --- /dev/null +++ b/packages/devui-vue/devui/menu/src/composables/use-nearest-menu-element.ts @@ -0,0 +1,6 @@ +export function useNearestMenuElement(ele: HTMLElement): HTMLElement{ + while (ele && ele.tagName !== 'LI' && ele.tagName !== 'UL'){ + ele = ele.parentElement as HTMLElement; + } + return ele; +} diff --git a/packages/devui-vue/devui/menu/src/composables/use-store.ts b/packages/devui-vue/devui/menu/src/composables/use-store.ts index 4502b01b92..9fcca009d1 100644 --- a/packages/devui-vue/devui/menu/src/composables/use-store.ts +++ b/packages/devui-vue/devui/menu/src/composables/use-store.ts @@ -22,7 +22,7 @@ export class Store{ emit(eventName: string, ...args: any[]): void{ recordTable[this.rootMenuName][eventName].forEach((fn)=>fn(...args)); } - off(eventName: string, fn: (...args: []) => void): void { + off(eventName: string, fn: (...args: []) => void): void{ const idx = recordTable[this.rootMenuName][eventName].indexOf(fn); if (idx >= 0) { recordTable[this.rootMenuName][eventName].splice(idx, 1); @@ -30,7 +30,7 @@ export class Store{ } } -export function useStore(rootName: string): Store { +export function useStore(rootName: string): Store{ if (!recordTable[rootName]){ Reflect.set(recordTable, rootName, {}); } diff --git a/packages/devui-vue/devui/menu/src/menu.scss b/packages/devui-vue/devui/menu/src/menu.scss index 79bc8e84b0..40ec54647d 100644 --- a/packages/devui-vue/devui/menu/src/menu.scss +++ b/packages/devui-vue/devui/menu/src/menu.scss @@ -8,6 +8,7 @@ $devui-menu-item: var(--devui-menu-item); $devui-menu-item-sub: var(--devui-menu-item-sub); $devui-menu-item-disabled: var(--devui-menu-disabled); $devui-menu-item-select: var(--devui-menu-item-hover); +$devui-menu-item-hover: var(--devui-menu-item-hover); $devui-menu-item-selectBar: var(--devui-primary-hover, #5e7ce0); $devui-menu-active-parent: var(--devui-icon-fill-active); diff --git a/packages/devui-vue/devui/menu/src/menu.tsx b/packages/devui-vue/devui/menu/src/menu.tsx index 60e95a189a..cbd5390a29 100644 --- a/packages/devui-vue/devui/menu/src/menu.tsx +++ b/packages/devui-vue/devui/menu/src/menu.tsx @@ -1,4 +1,4 @@ -import { defineComponent, provide, ref, computed, onMounted, toRefs } from 'vue'; +import { defineComponent, provide, ref, computed, onMounted, toRefs, reactive } from 'vue'; import type { ComponentPublicInstance } from 'vue'; import { menuProps, MenuProps } from './menu-types'; import './menu.scss'; @@ -32,52 +32,69 @@ export default defineComponent({ provide('useRouter', props.router); setDefaultIndent(props['indentSize']); const menuRoot = ref(null); - const overflow_container = ref(null); const overflowItemLength = ref(0); + const overflowContainer = ref(null); + const selectClassName = `${ns.b()}-item-select`; const rootClassName = computed(()=>({ [`${ns.b()}`]: true, [`${ns.b()}-vertical`]: mode.value === 'vertical', [`${ns.b()}-horizontal`]: mode.value === 'horizontal', [`${ns.b()}-collapsed`]: collapsed.value })); + const overflowContainerClassName = reactive({ + [selectClassName]: false, + [`${ns.b()}-overflow-container`]: true + }); + // 如果一个或多个菜单元素被选中,当宽度发生变化时。如果溢出容易中有被选中的元素,那么溢出容器也应当被选中 + const resetOverflowContainerSelectState = (e: Element) => { + const children = Array.from(e.children); + for (const item of children){ + if (item.classList.contains(selectClassName)){ + overflowContainerClassName[selectClassName] = true; + break; + } else { + overflowContainerClassName[selectClassName] = false; + } + } + }; onMounted(() => { if (props['mode'] === 'horizontal') { let flag = false; - const overflowContainer = overflow_container.value?.$el as unknown as HTMLElement; + const overflowContainerElement = overflowContainer.value?.$el as unknown as HTMLElement; const root = menuRoot.value as unknown as HTMLElement; const children = root.children; - const container = overflowContainer.children[1]; + const container = overflowContainerElement.children[1]; const ob = new IntersectionObserver( (entries: IntersectionObserverEntry[]) => { - entries.forEach((v: IntersectionObserverEntry) => { - if (!v.isIntersecting) { - const cloneNode = v.target.cloneNode(true) as Element as HTMLElement; - if (v.target.classList.contains(`${ns.b()}-overflow-container`)){ - if (flag && v.target.previousElementSibling && container.children.length){ - root.appendChild(v.target.previousElementSibling); + entries.forEach((entry: IntersectionObserverEntry) => { + if (!entry.isIntersecting) { + const cloneNode = entry.target.cloneNode(true) as Element as HTMLElement; + if (entry.target.classList.contains(`${ns.b()}-overflow-container`)){ + if (flag && entry.target.previousElementSibling && container.children.length){ + root.appendChild(entry.target.previousElementSibling); } else {flag = true;} } else { overflowItemLength.value += 1; - (v.target as Element as HTMLElement).style.visibility = 'hidden'; - if (overflowContainer.nextSibling) { - root.insertBefore(v.target, overflowContainer.nextSibling); + (entry.target as Element as HTMLElement).style.visibility = 'hidden'; + if (overflowContainerElement.nextSibling) { + root.insertBefore(entry.target, overflowContainerElement.nextSibling); } else { - root.appendChild(v.target); + root.appendChild(entry.target); } container.appendChild(cloneNode); + resetOverflowContainerSelectState(container); } } else { if ( - !v.target.classList.contains(`${ns.b()}-overflow-container`) && - (v.target as HTMLElement).style.visibility === 'hidden' + !entry.target.classList.contains(`${ns.b()}-overflow-container`) && + (entry.target as HTMLElement).style.visibility === 'hidden' ) { - ob.unobserve(v.target); - const el = container.lastChild; - if (el){ - root.insertBefore(el, overflowContainer); - } - const obItem = overflowContainer.previousElementSibling; - if (obItem) { + ob.unobserve(entry.target); + root.insertBefore(entry.target, overflowContainerElement); + (entry.target as HTMLElement).style.visibility = ''; + const obItem = overflowContainerElement.previousElementSibling; + const canObAgin = obItem && (entry.boundingClientRect.width % entry.target.getBoundingClientRect().width === 0); + if (canObAgin) { ob.observe(obItem); } if (obItem?.classList.contains('devui-submenu')){ @@ -92,9 +109,12 @@ export default defineComponent({ useShowSubMenu('mouseleave', ev, wrapper); }); } - (v.target as HTMLElement).style.visibility = ''; - v.target.remove(); overflowItemLength.value -= 1; + ob.observe(entry.target); + if (container.lastChild){ + container.removeChild(container.lastChild); + } + resetOverflowContainerSelectState(container); } } }); @@ -117,14 +137,13 @@ export default defineComponent({ class={rootClassName.value} style={[ props['collapsed'] ? `width:${props['collapsedIndent'] * 2}px` : `width: ${props['width']}`, - 'white-space: nowrap', ]}> {ctx.slots.default?.()} 0 && mode.value === 'horizontal'}>
    diff --git a/packages/devui-vue/devui/menu/src/styles/clear.scss b/packages/devui-vue/devui/menu/src/styles/clear.scss index ede506aaf6..6b76b4c387 100644 --- a/packages/devui-vue/devui/menu/src/styles/clear.scss +++ b/packages/devui-vue/devui/menu/src/styles/clear.scss @@ -1,6 +1,9 @@ .#{$devui-prefix}-menu-vertical, .#{$devui-prefix}-menu-horizontal { - a { + a, + a:hover, + a:active, + a:visited { text-decoration: none; } diff --git a/packages/devui-vue/devui/menu/src/styles/horizontal.scss b/packages/devui-vue/devui/menu/src/styles/horizontal.scss index c020c924c8..36419bac56 100644 --- a/packages/devui-vue/devui/menu/src/styles/horizontal.scss +++ b/packages/devui-vue/devui/menu/src/styles/horizontal.scss @@ -170,4 +170,20 @@ } } } + + .#{$devui-prefix}-menu-item-disabled, + .#{$devui-prefix}-submenu-disabled { + span, + a { + color: $devui-menu-item-disabled !important; + cursor: not-allowed; + } + + &::after { + content: unset !important; + } + &+.#{$devui-prefix}-menu-item-horizontal-wrapper { + display: none; + } + } } diff --git a/packages/devui-vue/devui/menu/src/styles/public.scss b/packages/devui-vue/devui/menu/src/styles/public.scss index 8b2959ebf1..732c32b3de 100644 --- a/packages/devui-vue/devui/menu/src/styles/public.scss +++ b/packages/devui-vue/devui/menu/src/styles/public.scss @@ -3,12 +3,6 @@ margin-left: $devui-menu-item-margin; } -.#{$devui-prefix}-menu-item-disabled, -.#{$devui-prefix}-submenu-disabled { - color: $devui-menu-item-disabled !important; - cursor: not-allowed !important; -} - .#{$devui-prefix}-menu-item-disabled:hover, .#{$devui-prefix}-submenu-disabled:hover { color: $devui-menu-item-disabled !important; @@ -34,3 +28,13 @@ .fade-leave-to { opacity: 0; } + +.#{$devui-prefix}-menu-item-disabled, +.#{$devui-prefix}-menu-item-disabled.devui-menu-vertical +.#{$devui-prefix}-menu-item-disabled.devui-menu-item-select +.#{$devui-prefix}-submenu-disabled, +.#{$devui-prefix}-submenu-disabled.devui-menu-vertical, +.#{$devui-prefix}-submenu-disabled.devui-menu-item-select { + color: $devui-menu-item-disabled !important; + cursor: not-allowed !important; +} diff --git a/packages/devui-vue/devui/menu/src/styles/vertical.scss b/packages/devui-vue/devui/menu/src/styles/vertical.scss index 5ec1d3d33b..4077c0e002 100644 --- a/packages/devui-vue/devui/menu/src/styles/vertical.scss +++ b/packages/devui-vue/devui/menu/src/styles/vertical.scss @@ -1,7 +1,9 @@ .#{$devui-prefix}-submenu-menu-item-vertical-wrapper { overflow: hidden; } - +.#{$devui-prefix}-submenu-menu-item { + color: $devui-menu-item; +} .#{$devui-prefix}-menu-vertical { padding: 0; transition: @@ -80,69 +82,23 @@ opacity: 1; background: $devui-menu-item-selectBar; } - - .#{$devui-prefix}-menu-item-select { - background: $devui-primary-bg !important; - position: relative; - - span, - a { - color: $devui-menu-item-select; - } - - &::after { - display: block; - position: absolute; - right: 0; - top: 0; - height: 100%; - width: 4px; - content: ''; - opacity: 1; - background: var(--devui-brand, #5e7ce0); - transform: scaleX(1); - } - } - - .#{$devui-prefix}-submenu > div:hover { - span.#{$devui-prefix}-submenu-title-content { - color: $devui-menu-item-select; - } - } - - .#{$devui-prefix}-menu-item:hover { - color: $devui-menu-item-select; - } - - li.#{$devui-prefix}-menu-item, - div.#{$devui-prefix}-submenu-title { - white-space: nowrap; - overflow: hidden; - - span:nth-child(2) { - overflow: hidden; - text-overflow: ellipsis; - - span { - overflow: hidden; - text-overflow: ellipsis; - } - } - } - - .#{$devui-prefix}-menu-item-isCollapsed { - width: fit-content; - - .#{$devui-prefix}-menu-icon { - margin: auto; - } - } - // sub menu ul.#{$devui-prefix}-submenu { margin: 0; padding: 0; + .#{$devui-prefix}-menu-item { + display: flex; + background: $devui-area; + & > span { + flex: auto; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + transition: all $devui-animation-duration-fast $devui-animation-ease-in-smooth; + color: $devui-menu-item; + } + } div.#{$devui-prefix}-submenu-title { display: flex; cursor: pointer; @@ -161,6 +117,7 @@ span.#{$devui-prefix}-submenu-title-content { font-size: $devui-font-size-lg; flex: auto; + color: $devui-menu-item; } span.#{$devui-prefix}-menu-icon { @@ -175,22 +132,14 @@ transform: rotate(180deg); } } - - .#{$devui-prefix}-menu-item { - display: flex; - - & > span { - flex: auto; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - transition: all $devui-animation-duration-fast $devui-animation-ease-in-smooth; - color: $devui-menu-item-sub; + .#{$devui-prefix}-submenu-title:hover { + span { + color: $devui-menu-item-hover !important; } } .#{$devui-prefix}-menu-item:hover { - & > span { + span { color: $devui-menu-item-select; } } @@ -202,11 +151,70 @@ } } + .#{$devui-prefix}-menu-item-select { + background: $devui-primary-bg !important; + position: relative; + + span, + a { + color: $devui-menu-item-select; + } + + &::after { + display: block; + position: absolute; + right: 0; + top: 0; + height: 100%; + width: 4px; + content: ''; + opacity: 1; + background: var(--devui-brand, #5e7ce0); + transform: scaleX(1); + } + } + + .#{$devui-prefix}-menu-item:hover { + color: $devui-menu-item-select; + } + + li.#{$devui-prefix}-menu-item, + div.#{$devui-prefix}-submenu-title { + white-space: nowrap; + overflow: hidden; + + span:nth-child(2) { + overflow: hidden; + text-overflow: ellipsis; + + span { + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + .#{$devui-prefix}-menu-item-isCollapsed { + width: fit-content; + + .#{$devui-prefix}-menu-icon { + margin: auto; + } + } + ul li ~ ul > div { margin-top: 0 !important; } - - ul li { - background: $devui-area !important; + .#{$devui-prefix}-menu-item-disabled, + .#{$devui-prefix}-submenu-disabled { + * { + color: $devui-menu-item-disabled !important; + cursor: not-allowed !important; + background: $devui-block !important; + } + } + .#{$devui-prefix}-menu-item-disabled::after, + .#{$devui-prefix}-submenu-disabled::after { + content: unset; } } diff --git a/packages/devui-vue/devui/message/__tests__/message.spec.tsx b/packages/devui-vue/devui/message/__tests__/message.spec.tsx index 1ef4b5eb96..4883d0ada0 100644 --- a/packages/devui-vue/devui/message/__tests__/message.spec.tsx +++ b/packages/devui-vue/devui/message/__tests__/message.spec.tsx @@ -5,6 +5,11 @@ import { useNamespace } from '../../shared/hooks/use-namespace'; const ns = useNamespace('message', true); describe('d-message', () => { describe('service', () => { + afterEach(() => { + const messageDom = document.querySelector(ns.b()); + messageDom?.parentNode?.removeChild(messageDom); + }); + it('render correctly when using service', async () => { message({ message: 'message content', @@ -51,9 +56,32 @@ describe('d-message', () => { expect(closeCallback).toBeCalled(); }); - it.todo('bordered should work well.'); + it('bordered should work well.', async () => { + message({ + message: 'message bordered should work well', + bordered: false, + }); + await nextTick(); + const messageDom = document.querySelector(ns.b()) as HTMLElement; - it.todo('shadow should work well.'); + expect(messageDom).toBeTruthy(); + expect(messageDom.style['border-top']).toBeFalsy(); + expect(messageDom.style['border-bottom']).toBeFalsy(); + expect(messageDom.style['border-left']).toBeFalsy(); + expect(messageDom.style['border-right']).toBeFalsy(); + }); + + it('shadow should work well.', async () => { + message({ + message: 'message shadow should work well', + shadow: false, + }); + await nextTick(); + const messageDom = document.querySelector(ns.b()) as HTMLElement; + + expect(messageDom).toBeTruthy(); + expect(messageDom.style['box-shadow']).toBe('none'); + }); }); describe('function', () => { diff --git a/packages/devui-vue/devui/message/src/message.scss b/packages/devui-vue/devui/message/src/message.scss index e7a2e95c46..fd7424708f 100644 --- a/packages/devui-vue/devui/message/src/message.scss +++ b/packages/devui-vue/devui/message/src/message.scss @@ -40,7 +40,8 @@ .#{$devui-prefix}-message__close { margin-left: auto; padding-left: 10px; - margin-top: -2px; + line-height: 0; + cursor: pointer; } // 图标样式 .#{$devui-prefix}-message__image { diff --git a/packages/devui-vue/devui/message/src/message.tsx b/packages/devui-vue/devui/message/src/message.tsx index a42c60f626..bced2c2c7f 100644 --- a/packages/devui-vue/devui/message/src/message.tsx +++ b/packages/devui-vue/devui/message/src/message.tsx @@ -31,7 +31,7 @@ export default defineComponent({ // 鼠标移入后结束定时器 const interrupt = () => { - if (timer) { + if (timer && props.duration) { clearTimeout(timer); timer = null; } @@ -39,7 +39,7 @@ export default defineComponent({ // 鼠标移出后重新计算时间 如果超时则直接移除message const removeReset = () => { - if (props.visible) { + if (props.visible && props.duration) { const remainTime = props.duration - (Date.now() - timestamp); timer = setTimeout(close, remainTime); } diff --git a/packages/devui-vue/devui/modal/src/composables/use-draggable.ts b/packages/devui-vue/devui/modal/src/composables/use-draggable.ts index acbddebbe4..82c75c18a1 100644 --- a/packages/devui-vue/devui/modal/src/composables/use-draggable.ts +++ b/packages/devui-vue/devui/modal/src/composables/use-draggable.ts @@ -1,4 +1,4 @@ -import { onBeforeUnmount, onMounted, watchEffect } from 'vue'; +import { onBeforeUnmount, onMounted, watchEffect, ref } from 'vue'; import type { ComputedRef, Ref } from 'vue'; function addUnit(value?: string | number, defaultUnit = 'px'): string { if (!value) { @@ -13,11 +13,18 @@ function addUnit(value?: string | number, defaultUnit = 'px'): string { } } +interface Draggable { + clearPosition: () => void; + modalPosition: Ref; +} + export const useDraggable = ( targetRef: Ref, dragRef: Ref, draggable: ComputedRef -): void => { +): Draggable => { + const modalPosition = ref('translate(-50%, -50%)'); + let transform = { offsetX: 0, offsetY: 0, @@ -49,7 +56,7 @@ export const useDraggable = ( offsetX: moveX, offsetY: moveY, }; - (targetRef.value as HTMLElement).style.transform = `translate(${addUnit(moveX)}, ${addUnit(moveY)})`; + modalPosition.value = `translate(calc(-50% + ${addUnit(moveX)}), calc(-50% + ${addUnit(moveY)}))`; }; const onMouseup = () => { @@ -86,4 +93,17 @@ export const useDraggable = ( onBeforeUnmount(() => { offDraggable(); }); + + const clearPosition = () => { + transform = { + offsetX: 0, + offsetY: 0, + }; + modalPosition.value = 'translate(-50%, -50%)'; + }; + + return { + clearPosition, + modalPosition, + }; }; diff --git a/packages/devui-vue/devui/modal/src/modal-types.ts b/packages/devui-vue/devui/modal/src/modal-types.ts index 055944a4d9..04e6601fc9 100644 --- a/packages/devui-vue/devui/modal/src/modal-types.ts +++ b/packages/devui-vue/devui/modal/src/modal-types.ts @@ -1,4 +1,4 @@ -import type { PropType, ExtractPropTypes, Ref } from 'vue'; +import type { PropType, ExtractPropTypes } from 'vue'; export type ModalType = 'success' | 'failed' | 'warning' | 'info' | ''; @@ -50,6 +50,10 @@ export const modalProps = { type: String as PropType, default: '', }, + keepLast: { + type: Boolean, + default: false, + }, }; export type EmitName = 'update:modelValue'; diff --git a/packages/devui-vue/devui/modal/src/modal.scss b/packages/devui-vue/devui/modal/src/modal.scss index 6e4c917693..ce753279be 100644 --- a/packages/devui-vue/devui/modal/src/modal.scss +++ b/packages/devui-vue/devui/modal/src/modal.scss @@ -45,13 +45,14 @@ align-items: center; &-icon { - display: inline-block; + display: flex; + align-items: center; + justify-content: center; vertical-align: middle; margin-right: 8px; width: 24px; height: 24px; line-height: 16px; - text-align: center; } &-text { diff --git a/packages/devui-vue/devui/modal/src/modal.tsx b/packages/devui-vue/devui/modal/src/modal.tsx index fb2fc7393f..af5afbff12 100644 --- a/packages/devui-vue/devui/modal/src/modal.tsx +++ b/packages/devui-vue/devui/modal/src/modal.tsx @@ -1,4 +1,4 @@ -import { computed, defineComponent, ref, Teleport, toRefs, Transition } from 'vue'; +import { computed, defineComponent, ref, Teleport, toRefs, Transition, watch } from 'vue'; import { modalProps, ModalProps, ModalType } from './modal-types'; import { Icon } from '../../icon'; import { FixedOverlay } from '../../overlay'; @@ -22,13 +22,19 @@ export default defineComponent({ emits: ['update:modelValue'], setup(props: ModalProps, { slots, attrs, emit }) { const ns = useNamespace('modal'); - const { modelValue, title, showClose, showOverlay, appendToBody, closeOnClickOverlay } = toRefs(props); + const { modelValue, title, showClose, showOverlay, appendToBody, closeOnClickOverlay, keepLast } = toRefs(props); const { execClose } = useModal(props, emit); useModalRender(props); const dialogRef = ref(); const headerRef = ref(); const draggable = computed(() => props.draggable); - useDraggable(dialogRef, headerRef, draggable); + const { clearPosition, modalPosition } = useDraggable(dialogRef, headerRef, draggable); + + watch(modelValue, (val) => { + if (val && !keepLast.value) { + clearPosition(); + } + }); const renderType = () => { const typeList: TypeList[] = [ @@ -55,14 +61,16 @@ export default defineComponent({ ]; const item = typeList.find((i) => i.type === props.type); return ( - -
    -
    - +
    + +
    +
    + +
    +
    {item?.text}
    -
    {item?.text}
    -
    - + +
    ); }; @@ -71,7 +79,7 @@ export default defineComponent({ {showOverlay.value && ( {modelValue.value && ( -
    e.stopPropagation()}> +
    e.stopPropagation()} + style={{ transform: modalPosition.value }}> {showClose.value && (
    diff --git a/packages/devui-vue/devui/notification/src/use-notification.ts b/packages/devui-vue/devui/notification/src/use-notification.ts index 708fb9f2cc..d14c8b6eba 100644 --- a/packages/devui-vue/devui/notification/src/use-notification.ts +++ b/packages/devui-vue/devui/notification/src/use-notification.ts @@ -25,14 +25,14 @@ export function useEvent( }; const interrupt = () => { - if (timer) { + if (timer && props.duration) { clearTimeout(timer); timer = null; } }; const removeReset = () => { - if (props.modelValue) { + if (props.modelValue && props.duration) { const remainTime = props.duration - (Date.now() - timestamp); timer = setTimeout(close, remainTime); } diff --git a/packages/devui-vue/devui/pagination/__tests__/pagination.spec.ts b/packages/devui-vue/devui/pagination/__tests__/pagination.spec.ts index 789f7ee25d..7178748500 100644 --- a/packages/devui-vue/devui/pagination/__tests__/pagination.spec.ts +++ b/packages/devui-vue/devui/pagination/__tests__/pagination.spec.ts @@ -18,7 +18,6 @@ const globalOption = { }, }; const ns = useNamespace('pagination', true); -const selectNs = useNamespace('select', true); describe('pagination: ', () => { it('test pageSize', async () => { @@ -54,10 +53,10 @@ describe('pagination: ', () => { expect(wrapper.find(`${ns.e('item')}.active`).text()).toEqual('5'); await nextTick(); - expect((wrapper.find(selectNs.e('input')).element as HTMLInputElement).value).toEqual('20'); + expect(wrapper.find('.devui-icon__container span').text()).toEqual('20'); const btns = wrapper.findAll(`a${ns.e('link')}`); - expect(btns.map((ele: DOMWrapper) => ele.text()).join()).toEqual('<,1,...,4,5,6,...,16,>'); + expect(btns.map((ele: DOMWrapper) => ele.text()).join()).toEqual(',1,...,4,5,6,...,16,'); expect(wrapper.find(ns.e('list')).classes()).toContain(ns.m('sm').slice(1)); // // 跳转按钮 @@ -68,16 +67,16 @@ describe('pagination: ', () => { await btns[0].trigger('click'); expect(wrapper.find(`${ns.e('item')}.active`).text()).toEqual('4'); const btns1 = wrapper.findAll(`a${ns.e('link')}`); - expect(btns1.map((ele: DOMWrapper) => ele.text()).join()).toEqual('<,1,...,3,4,5,...,16,>'); + expect(btns1.map((ele: DOMWrapper) => ele.text()).join()).toEqual(',1,...,3,4,5,...,16,'); // // 改变每页条数 - await wrapper.find(selectNs.e('input')).trigger('click'); - const selectItems = document.querySelectorAll(selectNs.e('item')); + await wrapper.find('.devui-icon__container span').trigger('click'); + const selectItems = document.querySelectorAll('.devui-pagination__size-list li'); await selectItems[1].dispatchEvent(new Event('click')); - expect((wrapper.find(selectNs.e('input')).element as HTMLInputElement).value).toEqual('10'); + expect(wrapper.find('.devui-icon__container span').text()).toEqual('10'); const btns2 = wrapper.findAll(`a${ns.e('link')}`); - expect(btns2.map((ele: DOMWrapper) => ele.text()).join()).toEqual('<,1,...,3,4,5,...,31,>'); + expect(btns2.map((ele: DOMWrapper) => ele.text()).join()).toEqual(',1,...,3,4,5,...,31,'); wrapper.unmount(); }); @@ -119,15 +118,15 @@ describe('pagination: ', () => { expect(wrapper.find(ns.e('list')).classes()).toContain(ns.m('lg').slice(1)); const btns = wrapper.findAll(`a${ns.e('link')}`); const pageIndexs = btns.map((ele: DOMWrapper) => ele.text()); - expect(pageIndexs.join()).toEqual('<,1,...,6,7,8,9,10,11,12,13,...,31,>'); + expect(pageIndexs.join()).toEqual(',1,...,6,7,8,9,10,11,12,13,...,31,'); // 当前页改变回调 await btns[0].trigger('click'); expect(pageIndexChange).toHaveBeenCalled(); // 每页条数改变回调 - await wrapper.find(selectNs.e('input')).trigger('click'); - const selectItems = document.querySelectorAll(selectNs.e('item')); + await wrapper.find('.devui-icon__container span').trigger('click'); + const selectItems = document.querySelectorAll('.devui-pagination__size-list li'); await selectItems[0].dispatchEvent(new Event('click')); expect(pageSizeChange).toHaveBeenCalled(); diff --git a/packages/devui-vue/devui/pagination/src/components/page-size.tsx b/packages/devui-vue/devui/pagination/src/components/page-size.tsx index a3740fe225..67d28ed41f 100644 --- a/packages/devui-vue/devui/pagination/src/components/page-size.tsx +++ b/packages/devui-vue/devui/pagination/src/components/page-size.tsx @@ -28,7 +28,9 @@ export default defineComponent({ menu: () => (
      {pageSizeOptions.value.map((item, index) => ( -
    • +
    • {item}
    • ))} diff --git a/packages/devui-vue/devui/pagination/src/pagination.scss b/packages/devui-vue/devui/pagination/src/pagination.scss index 3e8a1c1736..6214e6eb41 100644 --- a/packages/devui-vue/devui/pagination/src/pagination.scss +++ b/packages/devui-vue/devui/pagination/src/pagination.scss @@ -387,9 +387,14 @@ transition: color $devui-animation-duration-fast $devui-animation-ease-in-out-smooth, background-color $devui-animation-duration-fast $devui-animation-ease-in-out-smooth; - &:hover { + &:hover:not(.active) { color: $devui-list-item-hover-text; background-color: $devui-list-item-hover-bg; } + + &.active { + color: $devui-list-item-active-text; + background-color: $devui-list-item-active-bg; + } } } diff --git a/packages/devui-vue/devui/popover/__tests__/popover.spec.tsx b/packages/devui-vue/devui/popover/__tests__/popover.spec.tsx index b0e8cd140a..5d5918da18 100644 --- a/packages/devui-vue/devui/popover/__tests__/popover.spec.tsx +++ b/packages/devui-vue/devui/popover/__tests__/popover.spec.tsx @@ -2,14 +2,22 @@ import { mount } from '@vue/test-utils'; import { nextTick, ref } from 'vue'; import DPopover from '../src/popover'; import { useNamespace } from '../../shared/hooks/use-namespace'; +import { Placement } from '../src/popover-types'; +import { wait } from '../../shared/utils'; const ns = useNamespace('popover', true); const buttonNs = useNamespace('button', true); const buttonBaseClass = buttonNs.b(); const popoverContentClass = ns.e('content'); const popoverIconClass = useNamespace('popover').e('icon'); +const popoverArrowClass = '.devui-flexible-overlay__arrow'; describe('d-popover', () => { + beforeEach(() => { + const popoverContent = document.body.querySelector(popoverContentClass); + popoverContent && popoverContent.parentNode?.removeChild(popoverContent); + }); + it('visible', async () => { const wrapper = mount({ setup() { @@ -56,11 +64,9 @@ describe('d-popover', () => { }, }); await wrapper.find(buttonBaseClass).trigger('mouseenter'); - setTimeout(() => { - const popoverContent = document.body.querySelector(popoverContentClass); - expect(popoverContent).toBeTruthy(); - wrapper.unmount(); - }, 150); + await wait(500); + const popoverContent = document.body.querySelector(popoverContentClass); + expect(popoverContent).toBeTruthy(); }); it('trigger manually', async () => { @@ -117,7 +123,7 @@ describe('d-popover', () => { }); it('popover disabled work', async () => { - let disabled = ref(false); + const disabled = ref(false); const wrapper = mount({ setup() { return () => ( @@ -128,23 +134,153 @@ describe('d-popover', () => { }, }); await wrapper.find(buttonBaseClass).trigger('mouseenter'); - const popoverContent = document.body.querySelector(popoverContentClass); - setTimeout(() => { - expect(popoverContent).toBeTruthy(); - }, 150); - disabled = ref(true); + await wait(500); + let popoverContent = document.body.querySelector(popoverContentClass); + expect(popoverContent).toBeTruthy(); + disabled.value = true; await nextTick(); + popoverContent = document.body.querySelector(popoverContentClass); expect(popoverContent).toBeFalsy(); wrapper.unmount(); }); - it.todo('props position work well.'); + it('props position work well.', async () => { + let position = ref>(['top']); + let wrapper = mount({ + setup() { + return () => ( + + default + + ); + }, + }); + await wrapper.find(buttonBaseClass).trigger('click'); + await wait(500); + expect(document.querySelector(popoverArrowClass)?.style.bottom).toBe('-4px'); + const popoverContent = document.querySelector(popoverContentClass); + expect(popoverContent?.getAttribute('style')?.includes('transform-origin: 50% calc(100% + 8px)')).toBe(true); + wrapper.unmount(); - it.todo('props align work well.'); + position = ref>(['bottom']); + wrapper = mount({ + setup() { + return () => ( + + default + + ); + }, + }); + await wrapper.find(buttonBaseClass).trigger('click'); + await wait(500); + expect(document.querySelector(popoverArrowClass)?.style.top).toBe('-4px'); + expect(document.querySelector(popoverContentClass)?.getAttribute('style')?.includes('transform-origin: 50% -8px')).toBe(true); + wrapper.unmount(); + + position = ref>(['left']); + wrapper = mount({ + setup() { + return () => ( + + default + + ); + }, + }); + await wrapper.find(buttonBaseClass).trigger('click'); + await wait(500); + expect(document.querySelector(popoverArrowClass)?.style.right).toBe('-4px'); + expect(document.querySelector(popoverContentClass)?.getAttribute('style')?.includes('transform-origin: calc(100% + 8px)')).toBe(true); + wrapper.unmount(); + + position = ref>(['right']); + wrapper = mount({ + setup() { + return () => ( + + default + + ); + }, + }); + await wrapper.find(buttonBaseClass).trigger('click'); + await wait(500); + expect(document.querySelector(popoverArrowClass)?.style.left).toBe('-4px'); + expect(document.querySelector(popoverContentClass)?.getAttribute('style')?.includes('transform-origin: -8px 50%')).toBe(true); + wrapper.unmount(); - it.todo('props offset work well.'); + position = ref>(['right-start']); + wrapper = mount({ + setup() { + return () => ( + + default + + ); + }, + }); + await wrapper.find(buttonBaseClass).trigger('click'); + await wait(500); + expect(document.querySelector(popoverArrowClass)?.style.left).toBe('-4px'); + expect(document.querySelector(popoverContentClass)?.getAttribute('style')?.includes('transform-origin: -8px 50%')).toBe(true); + wrapper.unmount(); - it.todo('props mouse-enter-delay work well.'); + position = ref>(['right-end']); + wrapper = mount({ + setup() { + return () => ( + + default + + ); + }, + }); + await wrapper.find(buttonBaseClass).trigger('click'); + await wait(500); + expect(document.querySelector(popoverArrowClass)?.style.left).toBe('-4px'); + expect(document.querySelector(popoverContentClass)?.getAttribute('style')?.includes('transform-origin: -8px 50%')).toBe(true); + wrapper.unmount(); + }); - it.todo('props mouse-leave-delay work well.'); + it.todo('props align work well.'); + + it('props mouse-enter-delay work well.', async () => { + const wrapper = mount({ + setup() { + return () => ( + + default + + ); + }, + }); + await wrapper.find(buttonBaseClass).trigger('mouseenter'); + await wait(500); + expect(document.querySelector(popoverContentClass)).toBeFalsy(); + await wait(1100); + expect(document.querySelector(popoverContentClass)).toBeTruthy(); + wrapper.unmount(); + }); + + it('props mouse-leave-delay work well.', async () => { + const wrapper = mount({ + setup() { + return () => ( + + default + + ); + }, + }); + await wrapper.find(buttonBaseClass).trigger('mouseenter'); + await wait(500); + expect(document.querySelector(popoverContentClass)).toBeTruthy(); + await wrapper.find(buttonBaseClass).trigger('mouseleave'); + await wait(500); + expect(document.querySelector(popoverContentClass)).toBeTruthy(); + await wait(1100); + expect(document.querySelector(popoverContentClass)).toBeFalsy(); + wrapper.unmount(); + }); }); diff --git a/packages/devui-vue/devui/radio/__tests__/radio-group.spec.ts b/packages/devui-vue/devui/radio/__tests__/radio-group.spec.ts index 0d9f78ee50..7148524d9a 100644 --- a/packages/devui-vue/devui/radio/__tests__/radio-group.spec.ts +++ b/packages/devui-vue/devui/radio/__tests__/radio-group.spec.ts @@ -168,7 +168,7 @@ describe('RadioGroup', () => { const radio1 = wrapper.findAllComponents({ name: 'DRadio' })[0]; const radio1Wrapper = radio1.find(radioBaseClass); - expect(radio1Wrapper.classes()).not.toContain(sizeNs); + expect(radio1Wrapper.classes()).toContain(sizeNs); await wrapper.setProps({ border: true, }); diff --git a/packages/devui-vue/devui/radio/__tests__/radio.spec.ts b/packages/devui-vue/devui/radio/__tests__/radio.spec.ts index 76d5f0af02..e8fa3ee8e7 100644 --- a/packages/devui-vue/devui/radio/__tests__/radio.spec.ts +++ b/packages/devui-vue/devui/radio/__tests__/radio.spec.ts @@ -119,7 +119,7 @@ describe('Radio', () => { }, }); const container = wrapper.find(baseClass); - expect(container.classes()).not.toContain(sizeNs); + expect(container.classes()).toContain(sizeNs); await wrapper.setProps({ border: true, }); diff --git a/packages/devui-vue/devui/radio/src/radio-button.scss b/packages/devui-vue/devui/radio/src/radio-button.scss index cc6a245bd3..44c3d300b1 100644 --- a/packages/devui-vue/devui/radio/src/radio-button.scss +++ b/packages/devui-vue/devui/radio/src/radio-button.scss @@ -1,32 +1,41 @@ @import '../../styles-var/devui-var.scss'; +$devui-btn-sm-padding: var(--devui-btn-sm-padding, 0 16px); +$devui-btn-padding: var(--devui-btn-padding, 0 20px); +$devui-btn-lg-padding: var(--devui-btn-lg-padding, 0 24px); + $font-size-map: ( lg: $devui-font-size-lg, - md: $devui-font-size-lg, + md: $devui-font-size-md, sm: $devui-font-size-sm, - xs: $devui-font-size-sm, +); +$radio-label-height-map: ( + lg: $devui-size-lg, + md: $devui-size-md, + sm: $devui-size-sm, ); $button-padding-map: ( - lg: 12px 20px, - md: 10px 20px, - sm: 9px 15px, - xs: 7px 15px, + lg: $devui-btn-lg-padding, + md: $devui-btn-padding, + sm: $devui-btn-sm-padding, ); .#{$devui-prefix}-radio-button { - display: inline-block; + display: flex; + align-items: center; position: relative; - font-size: 14px; + padding: $devui-btn-padding; + height: $devui-size-md; + font-size: $devui-font-size-md; color: $devui-text; - padding: 10px 20px; cursor: pointer; border: 1px solid $devui-line; - border-left: none; - line-height: 1; user-select: none; - @each $size in ('lg', 'md', 'sm', 'xs') { + @each $size in ('lg', 'md', 'sm') { &.#{$devui-prefix}-radio-button--#{$size} { font-size: map-get($font-size-map, #{$size}); padding: map-get($button-padding-map, #{$size}); + height: map-get($radio-label-height-map, #{$size}); + line-height: map-get($radio-label-height-map, #{$size}); } } @@ -59,16 +68,41 @@ $button-padding-map: ( background-color: #ffffff; border-color: $devui-disabled-line; } +} + +.#{$devui-prefix}-radio-group { + &.is-row { + .#{$devui-prefix}-radio-button { + border-left: none; + + &:first-child { + border-top-left-radius: $devui-border-radius; + border-bottom-left-radius: $devui-border-radius; + border-left: 1px solid $devui-disabled-line; + } - &:first-child { - border-top-left-radius: $devui-border-radius; - border-bottom-left-radius: $devui-border-radius; - border-left: 1px solid $devui-disabled-line; - box-shadow: none; + &:last-child { + border-top-right-radius: $devui-border-radius; + border-bottom-right-radius: $devui-border-radius; + } + } } - &:last-child { - border-top-right-radius: $devui-border-radius; - border-bottom-right-radius: $devui-border-radius; + &.is-column { + .#{$devui-prefix}-radio-button { + width: 100%; + border-top: none; + + &:first-child { + border-top-left-radius: $devui-border-radius; + border-top-right-radius: $devui-border-radius; + border-top: 1px solid $devui-disabled-line; + } + + &:last-child { + border-bottom-left-radius: $devui-border-radius; + border-bottom-right-radius: $devui-border-radius; + } + } } } diff --git a/packages/devui-vue/devui/radio/src/radio-types.ts b/packages/devui-vue/devui/radio/src/radio-types.ts index ccde5ba0ab..2692dfda3f 100644 --- a/packages/devui-vue/devui/radio/src/radio-types.ts +++ b/packages/devui-vue/devui/radio/src/radio-types.ts @@ -1,6 +1,6 @@ import type { InjectionKey, PropType, Ref, ExtractPropTypes, ComputedRef } from 'vue'; export type valueTypes = string | number | boolean; -export type sizeTypes = 'lg' | 'md' | 'sm' | 'xs'; +export type sizeTypes = 'lg' | 'md' | 'sm'; /** radio、radio-group 共用 props */ const radioCommonProps = { @@ -25,8 +25,7 @@ const radioCommonProps = { default: false, }, size: { - type: String as PropType, - default: 'md', + type: String as PropType }, }; diff --git a/packages/devui-vue/devui/radio/src/radio.scss b/packages/devui-vue/devui/radio/src/radio.scss index 54a4ffc256..06e4b192f4 100644 --- a/packages/devui-vue/devui/radio/src/radio.scss +++ b/packages/devui-vue/devui/radio/src/radio.scss @@ -2,21 +2,18 @@ $font-size-map: ( lg: $devui-font-size-lg, - md: $devui-font-size-lg, + md: $devui-font-size-md, sm: $devui-font-size-sm, - xs: $devui-font-size-sm, ); $radio-width-map: ( - lg: 16px, + lg: 18px, md: 16px, sm: 14px, - xs: 14px, ); $radio-label-height-map: ( - lg: 40px, - md: 36px, - sm: 32px, - xs: 28px, + lg: $devui-size-lg, + md: $devui-size-md, + sm: $devui-size-sm, ); .#{$devui-prefix}-radio { @@ -65,7 +62,8 @@ $radio-label-height-map: ( .#{$devui-prefix}-radio__material-inner { opacity: 1; transform: scale(1); - transition: transform $devui-animation-duration-base $devui-animation-ease-in-out, + transition: + transform $devui-animation-duration-base $devui-animation-ease-in-out, opacity $devui-animation-duration-base $devui-animation-ease-in-out; &.disabled { @@ -154,8 +152,11 @@ $radio-label-height-map: ( border-radius: $devui-border-radius; padding: 0 15px 0 10px; } - @each $size in ('lg', 'md', 'sm', 'xs') { + @each $size in ('lg', 'md', 'sm') { &.#{$devui-prefix}-radio--#{$size} { + display: flex; + align-items: center; + .#{$devui-prefix}-radio__label { font-size: map-get($font-size-map, #{$size}); } @@ -163,7 +164,6 @@ $radio-label-height-map: ( .#{$devui-prefix}-radio__material { width: map-get($radio-width-map, #{$size}); height: map-get($radio-width-map, #{$size}); - line-height: map-get($radio-width-map, #{$size}); & > svg { width: map-get($radio-width-map, #{$size}); @@ -173,7 +173,6 @@ $radio-label-height-map: ( &.#{$devui-prefix}-radio--bordered { height: map-get($radio-label-height-map, #{$size}); - line-height: map-get($radio-label-height-map, #{$size}); } } } diff --git a/packages/devui-vue/devui/radio/src/radio.tsx b/packages/devui-vue/devui/radio/src/radio.tsx index b3853a13f9..0b80854ca6 100644 --- a/packages/devui-vue/devui/radio/src/radio.tsx +++ b/packages/devui-vue/devui/radio/src/radio.tsx @@ -20,7 +20,7 @@ export default defineComponent({ disabled: isDisabled.value, [ns.b()]: true, [ns.m('bordered')]: border.value, - [ns.m(size.value)]: border.value, + [ns.m(size.value)]: size.value, }; return ( diff --git a/packages/devui-vue/devui/radio/src/use-radio.ts b/packages/devui-vue/devui/radio/src/use-radio.ts index 73b5896a0f..0c55fdfd99 100644 --- a/packages/devui-vue/devui/radio/src/use-radio.ts +++ b/packages/devui-vue/devui/radio/src/use-radio.ts @@ -60,8 +60,9 @@ export function useRadio(props: RadioProps, ctx: SetupContext): UseRadioFn { }); const size = computed(() => { - return formContext?.size || radioGroupConf?.size.value || props.size; + return props.size || radioGroupConf?.size.value || formContext?.size || 'md'; }); + watch( () => props.modelValue, () => { @@ -79,7 +80,9 @@ export function useRadio(props: RadioProps, ctx: SetupContext): UseRadioFn { } export function useRadioGroup(props: RadioGroupProps, ctx: SetupContext): void { + const formContext = inject(FORM_TOKEN, undefined); const formItemContext = inject(FORM_ITEM_TOKEN, undefined); + /** change 事件 */ const emitChange = (radioValue: valueTypes) => { ctx.emit('update:modelValue', radioValue); @@ -93,13 +96,16 @@ export function useRadioGroup(props: RadioGroupProps, ctx: SetupContext): void { } ); + // 组件 size 优先于表单 size + const radioGroupSize = computed(() => props.size || formContext?.size || ''); + // 注入给子组件 provide(radioGroupInjectionKey, { modelValue: toRef(props, 'modelValue'), name: toRef(props, 'name'), disabled: toRef(props, 'disabled'), border: toRef(props, 'border'), - size: toRef(props, 'size'), + size: radioGroupSize, beforeChange: props.beforeChange, emitChange, fill: toRef(props, 'fill'), diff --git a/packages/devui-vue/devui/search/__tests__/search.spec.ts b/packages/devui-vue/devui/search/__tests__/search.spec.ts index bde1fbbd74..6b476a2cba 100644 --- a/packages/devui-vue/devui/search/__tests__/search.spec.ts +++ b/packages/devui-vue/devui/search/__tests__/search.spec.ts @@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'; import DSearch from '../src/search'; import { ref, nextTick } from 'vue'; import { useNamespace } from '../../shared/hooks/use-namespace'; +import { Form as DForm, FormItem as DFormItem } from '../../form'; const searchNs = useNamespace('search'); const dotSearchNs = useNamespace('search', true); @@ -13,27 +14,23 @@ const disableSearchClass = searchNs.m('disabled'); const dotSearchClass = dotSearchNs.b(); const dotClearSearchClass = dotSearchNs.e('clear'); const dotIconSearchClass = dotSearchNs.e('icon'); +const leftIconPositionClass = searchNs.m('left'); +const rightIconPositionClass = searchNs.m('right'); +const noBorderClass = searchNs.m('no-border'); describe('search test', () => { - // TODO: 这个单测应该按功能进行拆分 it('should render correctly', async () => { const value = ref('test'); - const size = ref(''); - const disabled = ref(false); const wrapper = mount({ components: { DSearch }, template: ` `, setup() { return { value, - size, - disabled, }; }, }); @@ -42,7 +39,94 @@ describe('search test', () => { const input = search.find('input'); expect(input.element.value).toBe('test'); - // test size + wrapper.unmount(); + }); + + it('should event correctly', async () => { + const value = ref('test'); + const onSearch = jest.fn(); + const wrapper = mount({ + components: { DSearch }, + template: ` + + `, + setup() { + return { + value, + onSearch, + }; + }, + }); + const search = wrapper.find(dotSearchClass); + const searchBtn = search.find(dotIconSearchClass); + + await searchBtn.trigger('click'); + await onSearch((str: string) => { + expect(str).toBe('test'); + }); + expect(onSearch).toBeCalledTimes(1); + + // test input focus after trigger search button + // TODO: 在单元测试环境中,input虽然处于focus状态,但是无法通过document.activeElement获取到 + // expect(input.element === document.activeElement).toBe(true); + wrapper.unmount(); + }); + + it('props v-model should work well.', async () => { + const value = ref('test'); + + const wrapper = mount({ + components: { DSearch }, + template: ` + + `, + setup() { + return { + value, + }; + }, + }); + + const search = wrapper.find(dotSearchClass); + const input = search.find('input'); + expect(input.element.value).toBe('test'); + + // test v-model + await input.setValue('def'); + expect(value.value).toBe('def'); + + value.value = 'change value'; + await nextTick(); + expect(input.element.value).toBe('change value'); + + wrapper.unmount(); + }); + + it('props size(sm/md/lg) should work well.', async () => { + const size = ref(''); + + const wrapper = mount({ + components: { DSearch }, + template: ` + + `, + setup() { + return { + size, + }; + }, + }); + + const search = wrapper.find(dotSearchClass); + const input = search.find('input'); + expect(input.classes()).not.toContain(smSearchClass); expect(input.classes()).not.toContain(lgSearchClass); @@ -55,81 +139,153 @@ describe('search test', () => { expect(wrapper.classes()).not.toContain(smSearchClass); expect(wrapper.classes()).toContain(lgSearchClass); - // test v-model - await input.setValue('def'); - expect(value.value).toBe('def'); + wrapper.unmount(); + }); - value.value = 'change value'; - await nextTick(); - expect(input.element.value).toBe('change value'); + it('props size priority', async () => { + const dFormSize = ref('lg'); + const dSearchSize = ref('sm'); - // test clear - const clear = wrapper.find(dotClearSearchClass); - await clear.trigger('click'); - expect(input.element.value).toBe(''); - expect(value.value).toBe(''); + const wrapper = mount({ + components: {DSearch, DForm, DFormItem}, + template: ` + + + + + `, + setup() { + return { + dFormSize, + dSearchSize + }; + }, + }); - // test input focus after trigger clear button - // TODO: 在单元测试环境中,input虽然处于focus状态,但是无法通过document.activeElement获取到 - // expect(input.element === document.activeElement).toBe(true); + const dSearch = wrapper.find(dotSearchClass); + // form 与 元素同时存在size 属性,以元素为准。 + expect(dSearch.classes()).toContain(smSearchClass); - // test disabled - expect(input.attributes('disabled')).toBe(undefined); - expect(wrapper.classes()).not.toContain(disableSearchClass); + dSearchSize.value = ''; + await nextTick(); - disabled.value = true; + // 元素不存在 size ,form 存在,以表单为准 + expect(dSearch.classes()).toContain(lgSearchClass); + + dFormSize.value = ''; await nextTick(); - expect(wrapper.classes()).toContain(disableSearchClass); - expect(input.attributes('disabled')).toBe(''); + + // form 与 元素都不存在 size 属性,使用默认值。 + expect(dSearch.classes()).not.toContain(smSearchClass); + expect(dSearch.classes()).not.toContain(lgSearchClass); + + wrapper.unmount(); }); - it('should event correctly', async () => { + it('clear operation should work well.', async () => { const value = ref('test'); - const onSearch = jest.fn(); const wrapper = mount({ components: { DSearch }, template: ` `, setup() { return { value, - onSearch, }; }, }); + const search = wrapper.find(dotSearchClass); - const searchBtn = search.find(dotIconSearchClass); - // const input = search.find('input'); - await searchBtn.trigger('click'); - await onSearch((str: string) => { - expect(str).toBe('test'); + const input = search.find('input'); + expect(input.element.value).toBe('test'); + + // test clear + const clear = wrapper.find(dotClearSearchClass); + await clear.trigger('click'); + expect(input.element.value).toBe(''); + expect(value.value).toBe(''); + + wrapper.unmount(); + }); + + it('props disabled should work well.', async () => { + const disabled = ref(false); + const wrapper = mount({ + components: { DSearch }, + template: ` + + `, + setup() { + return { + disabled, + }; + }, }); - expect(onSearch).toBeCalledTimes(1); + const search = wrapper.find(dotSearchClass); + const input = search.find('input'); - // test input focus after trigger search button - // TODO: 在单元测试环境中,input虽然处于focus状态,但是无法通过document.activeElement获取到 - // expect(input.element === document.activeElement).toBe(true); + // test disabled + expect(input.attributes('disabled')).toBe(undefined); + expect(wrapper.classes()).not.toContain(disableSearchClass); + + disabled.value = true; + await nextTick(); + expect(wrapper.classes()).toContain(disableSearchClass); + expect(input.attributes('disabled')).toBe(''); + + wrapper.unmount(); }); - it.todo('props size(sm/md/lg) should work well.'); + it('props icon-position(right/left) should work well.', async () => { + const wrapper = mount(DSearch); - it.todo('props auto-focus should work well.'); + const iconSearch = wrapper.find(dotIconSearchClass); - it.todo('props is-keyup-search should work well.'); + expect(iconSearch.exists()).toBe(true); - it.todo('props delay should work well.'); + expect(wrapper.classes()).toContain(rightIconPositionClass); + + await wrapper.setProps({ + iconPosition: 'left', + }); + expect(wrapper.classes()).toContain(leftIconPositionClass); - it.todo('props disabled should work well.'); + await wrapper.setProps({ + iconPosition: 'right', + }); + expect(wrapper.classes()).toContain(rightIconPositionClass); + + wrapper.unmount(); + }); - it.todo('props icon-position should work well.'); + it('props no-border should work well.', async () => { + const wrapper = mount(DSearch); + + expect(wrapper.classes()).not.toContain(noBorderClass); + + await wrapper.setProps({ + noBorder: true, + }); + + expect(wrapper.classes()).toContain(noBorderClass); + + wrapper.unmount(); + }); it.todo('props placeholder should work well.'); - it.todo('props no-border should work well.'); + it.todo('props auto-focus should work well.'); + + it.todo('props is-keyup-search should work well.'); + + it.todo('props delay should work well.'); it.todo('props max-length should work well.'); }); diff --git a/packages/devui-vue/devui/search/src/components/search-close-icon.tsx b/packages/devui-vue/devui/search/src/components/search-close-icon.tsx new file mode 100644 index 0000000000..15cc1e88e1 --- /dev/null +++ b/packages/devui-vue/devui/search/src/components/search-close-icon.tsx @@ -0,0 +1,11 @@ +const SearchCloseIcon = (): JSX.Element => ( + + + +); +export default SearchCloseIcon; diff --git a/packages/devui-vue/devui/search/src/components/search-icon.tsx b/packages/devui-vue/devui/search/src/components/search-icon.tsx new file mode 100644 index 0000000000..616065e652 --- /dev/null +++ b/packages/devui-vue/devui/search/src/components/search-icon.tsx @@ -0,0 +1,10 @@ +const SearchIcon = (): JSX.Element => ( + + + +); +export default SearchIcon; diff --git a/packages/devui-vue/devui/search/src/composables/use-search-class.ts b/packages/devui-vue/devui/search/src/composables/use-search-class.ts index ab07917553..e662e94dcb 100644 --- a/packages/devui-vue/devui/search/src/composables/use-search-class.ts +++ b/packages/devui-vue/devui/search/src/composables/use-search-class.ts @@ -1,29 +1,35 @@ /** * 定义组件class */ -import { computed, ComputedRef } from 'vue'; +import { computed, inject } from 'vue'; import type { Ref } from 'vue'; -import { SearchProps } from '../search-types'; +import { UseSearchClassTypes, SearchProps } from '../search-types'; +import { FORM_TOKEN } from '../../../form'; import { useNamespace } from '../../../shared/hooks/use-namespace'; -const SIZE_CLASS = { - lg: 'lg', - md: 'md', - sm: 'sm', -} as const; -const ICON_POSITION = { - right: 'right', - left: 'left', -}; -const ns = useNamespace('search'); +export const useSearchClass = (props: SearchProps, isFocus: Ref): UseSearchClassTypes => { + const formContext = inject(FORM_TOKEN, undefined); + + const ICON_POSITION = { + right: 'right', + left: 'left', + }; + + const ns = useNamespace('search'); -export const getRootClass = (props: SearchProps, isFocus: Ref): ComputedRef => { - return computed(() => ({ + const searchSize = computed(() => props.size || formContext?.size || 'md'); + + const rootClass = computed(() => ({ [ns.b()]: true, [ns.m('focus')]: isFocus.value, [ns.m('disabled')]: props.disabled, [ns.m('no-border')]: props.noBorder, - [ns.m(props.size)]: SIZE_CLASS[props.size], + [ns.m(searchSize.value)]: !!searchSize.value, [ns.m(props.iconPosition)]: ICON_POSITION[props.iconPosition], })); + + return { + rootClass, + searchSize + }; }; diff --git a/packages/devui-vue/devui/search/src/search-types.ts b/packages/devui-vue/devui/search/src/search-types.ts index 9edc84d615..75ca6e7b42 100644 --- a/packages/devui-vue/devui/search/src/search-types.ts +++ b/packages/devui-vue/devui/search/src/search-types.ts @@ -5,8 +5,7 @@ export type IconPosition = 'right' | 'left'; export const searchProps = { size: { - type: String as PropType, - default: 'md', + type: String as PropType }, placeholder: { type: String, @@ -56,6 +55,11 @@ export const searchProps = { export type SearchProps = ExtractPropTypes; +export interface UseSearchClassTypes { + rootClass: ComputedRef<{ [p: string]: string | boolean }>; + searchSize: ComputedRef; +} + export interface KeywordsReturnTypes { keywords: Ref; clearIconShow: ComputedRef; diff --git a/packages/devui-vue/devui/search/src/search.scss b/packages/devui-vue/devui/search/src/search.scss index a96416e99a..59cbad5404 100644 --- a/packages/devui-vue/devui/search/src/search.scss +++ b/packages/devui-vue/devui/search/src/search.scss @@ -13,11 +13,14 @@ } .#{$devui-prefix}-input { - padding: 4px 60px 4px 8px; display: inline-flex; align-items: center; justify-content: center; width: 100%; + height: $devui-size-md; + // 60px = clear + icon 的宽度 + padding: 4px 60px 4px 8px; + font-size: $devui-font-size-md; color: $devui-text; box-sizing: border-box; border: 1px solid $devui-form-control-line; @@ -41,19 +44,23 @@ } } - svg.svg-icon-clear path, - svg.svg-icon-search path { - fill: $devui-icon-text; + &__clear, + &__icon { + @include size($devui-size-md, $devui-size-md); + @include flex; + + & svg { + path { + fill: $devui-icon-fill; + } + @include size($devui-font-size-md, $devui-font-size-md); + } } &__clear { position: absolute; - right: 36px; + right: $devui-size-md; cursor: pointer; - height: 100%; - font-size: 10px; - @include size(30px, 100%); - @include flex; &::after { content: ''; @@ -74,50 +81,51 @@ z-index: 1; right: 0; top: 0; - font-size: 16px; - width: 36px; - height: 100%; - @include flex; } &--sm { .#{$devui-prefix}-input { + height: $devui-size-sm; + font-size: $devui-font-size-sm; + &.#{$devui-prefix}-input--sm { - font-size: $devui-font-size; - padding-right: 60px; + padding: 0 48px 0 6px; } } - .#{$devui-prefix}-search__icon { - font-size: 14px; - @include size(34px, 26px); + .#{$devui-prefix}-search__icon, .#{$devui-prefix}-search__clear { + @include size($devui-size-sm, $devui-size-sm); + + svg { + @include size($devui-font-size-sm, $devui-font-size-sm); + } } .#{$devui-prefix}-search__clear { - @include size(26px, 100%); - - right: 34px; + right: $devui-size-sm; } } &--lg { .#{$devui-prefix}-input { - padding: 4px 36px 4px 10px; + height: $devui-size-lg; + font-size: $devui-font-size-lg; &.#{$devui-prefix}-input--lg { - padding-right: 75px; + padding: 4px 80px 4px 10px; } } - .#{$devui-prefix}-search__icon { - font-size: 18px; - @include size(46px, 46px); + .#{$devui-prefix}-search__icon, .#{$devui-prefix}-search__clear { + @include size($devui-size-lg, $devui-size-lg); + + svg { + @include size($devui-font-size-lg, $devui-font-size-lg); + } } .#{$devui-prefix}-search__clear { - @include size(40px, 100%); - - right: 46px; + right: $devui-size-lg; } } @@ -140,15 +148,17 @@ &--left { .#{$devui-prefix}-input { - padding: 4px 26px 4px 30px; + padding-right: $devui-size-md; + padding-left: $devui-size-md; &.#{$devui-prefix}-input--lg { - padding-right: 28px; - padding-left: 48px; + padding-right: $devui-size-lg; + padding-left: $devui-size-lg; } &.#{$devui-prefix}-input--sm { - padding-right: 24px; + padding-right: $devui-size-sm; + padding-left: $devui-size-sm; } } diff --git a/packages/devui-vue/devui/search/src/search.tsx b/packages/devui-vue/devui/search/src/search.tsx index 7c285ff5b4..c7f3efd888 100644 --- a/packages/devui-vue/devui/search/src/search.tsx +++ b/packages/devui-vue/devui/search/src/search.tsx @@ -1,13 +1,14 @@ import { defineComponent, getCurrentInstance, ref } from 'vue'; import { SearchProps, searchProps } from './search-types'; -import { getRootClass } from './composables/use-search-class'; +import { useSearchClass } from './composables/use-search-class'; import { keywordsHandles } from './composables/use-search-keywords'; import { keydownHandles } from './composables/use-search-keydown'; import DInput from '../../input/src/input'; -import { Icon } from '../../icon'; import { useNamespace } from '../../shared/hooks/use-namespace'; import './search.scss'; import { createI18nTranslate } from '../../locale/create'; +import SearchCloseIcon from './components/search-close-icon'; +import SearchIcon from './components/search-icon'; export default defineComponent({ name: 'DSearch', @@ -19,7 +20,7 @@ export default defineComponent({ const ns = useNamespace('search'); const isFocus = ref(false); - const rootClasses = getRootClass(props, isFocus); + const {rootClass, searchSize} = useSearchClass(props, isFocus); const { keywords, clearIconShow, onClearHandle } = keywordsHandles(ctx, props); const { onInputKeydown, onClickHandle, useEmitKeyword } = keydownHandles(ctx, keywords, props); @@ -40,7 +41,7 @@ export default defineComponent({ return () => { const inputProps = { - size: props.size, + size: searchSize.value, disabled: props.disabled, autoFocus: props.autoFocus, modelValue: keywords.value, @@ -50,22 +51,23 @@ export default defineComponent({ onFocus: onFocus, onBlur: onBlur, }; + return ( -