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/README.md b/README.md index 7f3bc13e5d..e66f6dc9ae 100644 --- a/README.md +++ b/README.md @@ -143,36 +143,38 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
iel

🚧 💻
chenxi24

💻
小九九

💻 -
AlanLee

💻 -
Echo

💻 +
AlanLee

💻 +
Echo

💻
GaoNeng

💻
行言

💻 🐛
devin

💻
无声

💻
sleep_fish

💻
迷心whylost

💻 + +
X.Q. Chen

🚇 💻
葉家男孩

💻
lihai

💻
纳撸多

💻 - -
ElsaOOo

🚧 🚇 💻
刘小迪

💻
unfound

💻
Roading

💻 + +
Chestnut

💻
c0dedance

💻
杜庆愉

💻
linxiang

💻
掘墓忍者

💻
一个大胖子

💻 📖 - -
Ikko Ashimine

📖
Bob

💻 + +
populus

💻
tohalf

💻
Miliky

💻 ⚠️ @@ -191,24 +193,33 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
newer2333

💻
哈士奇-黄

💻

💻 -
Anthonio OuYang

💻 -
FlingYP

💻 +
Anthonio OuYang

💻 +
FlingYP

💻
xzxldl55

💻
79

💻
wailen

💻
jenson

💻
dbsdaicheng

⚠️
qinwencheng

💻 + +
Angelanana

💻
joo1es

💻
handsomezyw

💻
Yoki

💻 - -
luopei

💻
Mr.Cheng

💻 +
Bbbtt04

💻 +
Zz-ZzzZ

💻 + + +
buaalkn

💻 +
hxj9102

💻 +
Whbbit1999

💻 +
zhaoShijuan

💻 +
XiaoRIGE

💻 @@ -223,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 d036b16430..1f82421f74 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -121,77 +121,94 @@ pnpm scripts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

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

💻

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

💻
@@ -204,6 +221,7 @@ pnpm scripts ## 合作项目 - [H5-Dooring - 让H5制作,更简单](http://h5.dooring.cn/) +- [灯塔 - 公益性质的反霸凌团队](https://www.light-tower.top/) ## 开源许可 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/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/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/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.scss b/packages/devui-vue/devui/button/src/button.scss index 4b427c2529..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, @@ -369,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/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 cbed774458..29e24b498a 100644 --- a/packages/devui-vue/devui/checkbox/__tests__/checkbox-group.spec.ts +++ b/packages/devui-vue/devui/checkbox/__tests__/checkbox-group.spec.ts @@ -48,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 () => { @@ -86,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 () => { @@ -116,6 +120,8 @@ describe('d-checkbox-group', () => { direction.value = 'row'; await nextTick(); expect(wrapper.find('.is-row').exists()).toBe(true); + + wrapper.unmount(); }); it('checkbox-group itemWidth work', () => { @@ -141,6 +147,8 @@ describe('d-checkbox-group', () => { }); expect(wrapper.findAll(wrapClass).length).toBe(2); + + wrapper.unmount(); }); it('checkbox-group options work', () => { @@ -174,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 () => { @@ -213,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 () => { @@ -250,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', () => { @@ -274,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 () => { @@ -302,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 () => { @@ -327,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 83bdeb3747..be66e264af 100644 --- a/packages/devui-vue/devui/checkbox/src/checkbox-button.scss +++ b/packages/devui-vue/devui/checkbox/src/checkbox-button.scss @@ -42,11 +42,9 @@ $checkbox-label-height-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') { &.#{$devui-prefix}-checkbox-button--#{$size} { font-size: map-get($font-size-map, #{$size}); @@ -81,20 +79,60 @@ $checkbox-label-height-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-types.ts b/packages/devui-vue/devui/checkbox/src/checkbox-types.ts index ebf4f8edec..1022e2242b 100644 --- a/packages/devui-vue/devui/checkbox/src/checkbox-types.ts +++ b/packages/devui-vue/devui/checkbox/src/checkbox-types.ts @@ -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.tsx b/packages/devui-vue/devui/checkbox/src/checkbox.tsx index 2c75365c0f..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(); 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 fd53af98c6..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 { defineComponent, ref, computed, onMounted, watch, nextTick, provide, unref, readonly, Transition } from 'vue'; import type { StyleValue, Ref } from 'vue'; -import { - useReactive, - colorPickerResize, - isExhibitionColorPicker, - changeColorValue -} from './utils/composeable'; +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 @@ -74,22 +59,12 @@ export default defineComponent({ // 点击展示 colorpicker window.addEventListener('click', isExhibition, true); }); - // ** computeds - // colorpicker panel 组件位置 - const colorPickerPostion = computed(() => { - if (colorCubeRef.value) { - return { - transform: `translate(${left.value}px, ${top.value}px)` - }; - } - return {}; - }); // 交互触发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 @@ -115,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); } }); } @@ -143,46 +128,35 @@ export default defineComponent({ return () => { return ( -
-
-
-
+
+
+
+
-
+ '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 100% rename from packages/devui-vue/devui/color-picker/src/utils/composeable.ts rename to packages/devui-vue/devui/color-picker/src/utils/composable.ts 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 5a8967d9dc..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({ @@ -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 5f76e73bba..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({ @@ -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/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 5a46b62031..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); @@ -117,6 +123,47 @@ 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); const wrapper = mount({ 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/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 2674e3d4bf..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]) diff --git a/packages/devui-vue/devui/menu/__tests__/menu.spec.ts b/packages/devui-vue/devui/menu/__tests__/menu.spec.ts index e7796d0693..6ca2031f8a 100644 --- a/packages/devui-vue/devui/menu/__tests__/menu.spec.ts +++ b/packages/devui-vue/devui/menu/__tests__/menu.spec.ts @@ -15,7 +15,19 @@ 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; @@ -140,13 +152,74 @@ 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.todo('props mode(vertical/horizontal) work well.'); - - it.todo('props multiple work well.'); + 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 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.'); 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 0daad8e758..82c75c18a1 100644 --- a/packages/devui-vue/devui/modal/src/composables/use-draggable.ts +++ b/packages/devui-vue/devui/modal/src/composables/use-draggable.ts @@ -13,10 +13,9 @@ function addUnit(value?: string | number, defaultUnit = 'px'): string { } } -export const modalPosition = ref('translate(-50%, -50%)'); - interface Draggable { clearPosition: () => void; + modalPosition: Ref; } export const useDraggable = ( @@ -24,6 +23,8 @@ export const useDraggable = ( dragRef: Ref, draggable: ComputedRef ): Draggable => { + const modalPosition = ref('translate(-50%, -50%)'); + let transform = { offsetX: 0, offsetY: 0, @@ -103,5 +104,6 @@ export const useDraggable = ( return { clearPosition, + modalPosition, }; }; diff --git a/packages/devui-vue/devui/modal/src/modal.tsx b/packages/devui-vue/devui/modal/src/modal.tsx index 0063666524..af5afbff12 100644 --- a/packages/devui-vue/devui/modal/src/modal.tsx +++ b/packages/devui-vue/devui/modal/src/modal.tsx @@ -3,7 +3,7 @@ import { modalProps, ModalProps, ModalType } from './modal-types'; import { Icon } from '../../icon'; import { FixedOverlay } from '../../overlay'; import { useModal, useModalRender } from './composables/use-modal'; -import { useDraggable, modalPosition } from './composables/use-draggable'; +import { useDraggable } from './composables/use-draggable'; import DModalHeader from './components/header'; import DModalBody from './components/body'; import { useNamespace } from '../../shared/hooks/use-namespace'; @@ -28,7 +28,7 @@ export default defineComponent({ const dialogRef = ref(); const headerRef = ref(); const draggable = computed(() => props.draggable); - const { clearPosition } = useDraggable(dialogRef, headerRef, draggable); + const { clearPosition, modalPosition } = useDraggable(dialogRef, headerRef, draggable); watch(modelValue, (val) => { if (val && !keepLast.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/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 36b3ba94cd..44c3d300b1 100644 --- a/packages/devui-vue/devui/radio/src/radio-button.scss +++ b/packages/devui-vue/devui/radio/src/radio-button.scss @@ -29,7 +29,6 @@ $button-padding-map: ( color: $devui-text; cursor: pointer; border: 1px solid $devui-line; - border-left: none; user-select: none; @each $size in ('lg', 'md', 'sm') { &.#{$devui-prefix}-radio-button--#{$size} { @@ -69,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; - box-shadow: none; + &:first-child { + border-top-left-radius: $devui-border-radius; + border-bottom-left-radius: $devui-border-radius; + border-left: 1px solid $devui-disabled-line; + } + + &: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 7ab79f763b..2692dfda3f 100644 --- a/packages/devui-vue/devui/radio/src/radio-types.ts +++ b/packages/devui-vue/devui/radio/src/radio-types.ts @@ -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.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 ac654e09a3..59cbad5404 100644 --- a/packages/devui-vue/devui/search/src/search.scss +++ b/packages/devui-vue/devui/search/src/search.scss @@ -44,18 +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: $devui-size-md; cursor: pointer; - height: 100%; - @include size($devui-size-md, $devui-size-md); - @include flex; &::after { content: ''; @@ -76,9 +81,6 @@ z-index: 1; right: 0; top: 0; - width: $devui-size-md; - height: $devui-size-md; - @include flex; } &--sm { @@ -91,15 +93,15 @@ } } - .#{$devui-prefix}-search__icon { - font-size: $devui-font-size-sm; + .#{$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 { - font-size: $devui-font-size-sm; - @include size($devui-size-sm, $devui-size-sm); - right: $devui-size-sm; } } @@ -114,15 +116,15 @@ } } - .#{$devui-prefix}-search__icon { - font-size: $devui-font-size-lg; + .#{$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 { - font-size: $devui-font-size-lg; - @include size($devui-size-lg, $devui-size-lg); - right: $devui-size-lg; } } 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 ( -