diff --git a/devui/style/core/_form.scss b/devui/style/core/_form.scss index 191408c86ae7e83c66fdc1fda2dd0b772b367928..c2527f0f45590097d3e3805b4aaa008c43873cdd 100755 --- a/devui/style/core/_form.scss +++ b/devui/style/core/_form.scss @@ -1,16 +1,35 @@ @import '../theme/color.scss'; -@import './font'; +@import '../theme/shadow'; +@import '../theme/corner'; +@import '../theme/font'; @mixin border-position-radius($position: left) { border-top-#{$position}-radius: 0; border-bottom-#{$position}-radius: 0; } +$border-change-time: 300ms; +$border-change-function: cubic-bezier(0.645, 0.045, 0.355, 1); + +@mixin border-transition { + transition: border-color $border-change-time $border-change-function; +} .devui-form-controls input[type='text'], .devui-form-controls input[type='password'], [dTextInput] { width: 100%; height: 28px; + font-size: $devui-font-size; + + &.devui-textinput-sm { + font-size: $devui-font-size-sm; + height: 26px; + } + + &.devui-textinput-lg { + font-size: $devui-font-size-lg; + height: 46px; + } } [dTextArea] { @@ -25,9 +44,10 @@ color: $devui-text; vertical-align: middle; border: 1px solid $devui-form-control-line; - border-radius: 2px; + border-radius: $devui-border-radius; outline: none; background-color: $devui-base-bg; + @include border-transition(); &:not([disabled]):not(.disabled):not(.devui-disabled):not(.error):not(.devui-error) { &:hover { @@ -66,7 +86,7 @@ .devui-input-group-addon { border: 1px solid $devui-form-control-line; - border-radius: 2px; + border-radius: $devui-border-radius; display: table-cell; padding: 0 10px; text-align: center; @@ -88,7 +108,7 @@ outline: none; background-color: $devui-base-bg; border: 1px solid $devui-form-control-line; - border-radius: 2px; + border-radius: $devui-border-radius; padding: 5px 10px; line-height: 16px; // 解决ie中文字不居中,由于height28px,有10px的padding和2px的border,剩余16px,使用其他的会使文字不居中 font-size: $devui-font-size; @@ -97,6 +117,7 @@ display: block; cursor: text; height: 28px; + @include border-transition; &:hover { border-color: $devui-form-control-line-hover; @@ -187,8 +208,9 @@ display: block; width: 100%; height: 28px; - border-radius: 2px; + border-radius: $devui-border-radius; outline: 0; + @include border-transition; &[disabled], &.disabled, @@ -216,92 +238,46 @@ } } -:not(.devui-dropdown-origin-wrapper):not(.multiple-label-auto-complete-disabled) { - > .devui-dropdown-origin { - position: relative; - z-index: 1; - } - - > .devui-dropdown-origin:hover:not([disabled]):not(.disabled):not(.devui-disabled):not(:focus):not(.devui-dropdown-origin-open) { - border-color: $devui-form-control-line-hover; - } - - > .devui-dropdown-origin:focus:not([disabled]):not(.disabled):not(.devui-disabled) { - outline: none; - border-color: $devui-form-control-line-active; - - &:not(.devui-select-underlined-border):not(.devui-dropdown-no-border) { - box-shadow: 0 2px 5px 0 $devui-shadow; - } - } - - > .devui-dropdown-origin.devui-dropdown-origin-open:not([disabled]):not(.disabled):not(.devui-disabled) { - outline: none; - border-color: $devui-connected-overlay-line; - border-radius: 2px 2px 0 0; - - &:not(.devui-select-underlined-border):not(.devui-dropdown-no-border) { - box-shadow: 0 2px 5px 0 $devui-shadow; - } - } - - > .devui-dropdown-origin.devui-dropdown-origin-open:not(.devui-no-border):not([disabled]):not(.disabled):not(.devui-disabled):not(.devui-dropdown-no-border) { - z-index: 1052; // 为了让dividing边框覆盖cdk边框颜色 - - & ~ .devui-form-control-feedback { - z-index: 1052; - } +:not(.multiple-label-auto-complete-disabled):not(.devui-error) { + > .devui-dropdown-origin:not(d-button):not(.icon):not([class^='icon-']):not([disabled]):not(.disabled):not(.devui-disabled) { + &:not(.devui-dropdown-no-border):not(.devui-no-border) { + border-color: $devui-form-control-line; + @include border-transition; - &.devui-dropdown-origin-bottom { - border-bottom-color: $devui-dividing-line; - box-shadow: none; - - &::after { - content: ''; - width: 100%; - height: 1px; - background-color: $devui-dividing-line; - position: absolute; - z-index: 1001; - left: 0; - top: 100%; + &:hover:not(:focus):not(.devui-dropdown-origin-open) { + border-color: $devui-form-control-line-hover; } - } - - &.devui-dropdown-origin-top { - border-top-color: $devui-dividing-line; - &:not(.devui-select-underlined-border) { - box-shadow: 0 2px 5px 0 $devui-shadow; + &:focus, + &:focus-within { + outline: none; + border-color: $devui-form-control-line-active; } - &::after { - content: ''; - width: 100%; - height: 1px; - background-color: $devui-dividing-line; - position: absolute; - z-index: 1001; - left: 0; - top: -1px; + &.devui-dropdown-origin-open { + outline: none; + border-color: $devui-connected-overlay-line; } } } } -.devui-dropdown-origin-wrapper { - color: $devui-text; - background-color: $devui-base-bg; +.devui-dropdown-origin:not(d-button):not(.icon):not([class^='icon-']) { min-height: 28px; + &:not([disabled]):not(.disabled):not(.devui-disabled) { + color: $devui-text; + } + & > .devui-input, & > .devui-form-control { height: 26px; } - &:not(.devui-select-underlined-border) { - border: 1px solid $devui-form-control-line; - border-radius: 2px; + &:not(.devui-select-underlined-border):not(.devui-dropdown-no-border):not(.devui-no-border) { + border-radius: $devui-border-radius; + border-width: 1px; + border-style: solid; } .devui-form-control, @@ -315,8 +291,6 @@ > .devui-dropdown-default:hover, > .devui-dropdown-default:active, > .devui-dropdown-default:focus { - position: relative; - z-index: 1001; border-color: transparent; } @@ -335,20 +309,6 @@ background-color: $devui-disabled-bg; } } - - &:not([disabled]):not(.disabled):not(.devui-disabled):not(.devui-error) { - .devui-form-control:not([disabled]):not(.disabled):not(.devui-disabled):not(.devui-error), - .devui-form-control:not([disabled]):not(.disabled):not(.devui-disabled):not(.devui-error):hover, - .devui-form-control:not([disabled]):not(.disabled):not(.devui-disabled):not(.devui-error):focus, - .devui-form-control:not([disabled]):not(.disabled):not(.devui-disabled):not(.devui-error):focus:hover, - .devui-input-group-addon, - > .devui-dropdown-default, - > .devui-dropdown-default:hover, - > .devui-dropdown-default:active, - > .devui-dropdown-default:focus { - background-color: $devui-base-bg; - } - } } // css选择器不可合并,否则css解析器总是会失败 @@ -379,9 +339,9 @@ input::-webkit-input-placeholder { background-color: $devui-danger-bg; } -d-select:not([disabled]):not(.disabled):not(.devui-disabled):not(.devui-dropdown-origin-wrapper):not(.multiple-label-auto-complete-disabled).devui-error { - div.devui-dropdown-origin.devui-dropdown-origin-wrapper.devui-dropup, - div.devui-dropdown-origin.devui-dropdown-origin-wrapper.devui-dropdown { +d-select:not([disabled]):not(.disabled):not(.devui-disabled):not(.multiple-label-auto-complete-disabled).devui-error { + div.devui-dropdown-origin.devui-dropup, + div.devui-dropdown-origin.devui-dropdown { border-color: $devui-danger-line; .devui-form-group .devui-input.devui-form-control.devui-select-input:not(.devui-select-search), @@ -392,7 +352,7 @@ d-select:not([disabled]):not(.disabled):not(.devui-disabled):not(.devui-dropdown } d-editable-select:not([disabled]):not(.disabled):not(.devui-disabled).devui-error { - .devui-form-group:not(.devui-dropdown-origin-wrapper):not(.multiple-label-auto-complete-disabled) { + .devui-form-group:not(.multiple-label-auto-complete-disabled) { input.devui-form-control.devui-dropdown-origin { border-color: $devui-danger-line; background-color: $devui-danger-bg; /* TODO: open时,下边框颜色 */ @@ -400,6 +360,28 @@ d-editable-select:not([disabled]):not(.disabled):not(.devui-disabled).devui-erro } } +d-datepicker-pro:not([disabled]):not(.disabled):not(.devui-disabled).devui-error { + .devui-datepicker-pro-wrapper:not([disabled]):not(.disabled):not(.devui-disabled) .devui-dropdown-toggle .devui-single-picker { + border-color: $devui-danger-line; + background-color: $devui-danger-bg; + + .devui-input:not(.devui-disabled) { + background-color: $devui-danger-bg; + } + } +} + +d-range-datepicker-pro:not([disabled]):not(.disabled):not(.devui-disabled).devui-error { + .devui-datepicker-pro-wrapper:not([disabled]):not(.disabled):not(.devui-disabled) .devui-dropdown-toggle .devui-range-picker { + border-color: $devui-danger-line; + background-color: $devui-danger-bg; + + .devui-input:not(.devui-disabled) { + background-color: $devui-danger-bg; + } + } +} + d-input-number:not([disabled]):not(.disabled):not(.devui-disabled).devui-error { .input-box:not(:disabled) { border-color: $devui-danger-line; @@ -408,7 +390,7 @@ d-input-number:not([disabled]):not(.disabled):not(.devui-disabled).devui-error { } d-multi-auto-complete:not([disabled]):not(.disabled):not(.devui-disabled).devui-error { - :not(.devui-dropdown-origin-wrapper):not(.multiple-label-auto-complete-disabled) { + :not(.multiple-label-auto-complete-disabled) { &.multiple-label-auto-complete.multiple-label-auto-complete-border ul.devui-dropdown-origin { border-color: $devui-danger-line; background-color: $devui-danger-bg; @@ -421,7 +403,7 @@ d-multi-auto-complete:not([disabled]):not(.disabled):not(.devui-disabled).devui- } d-tags-input:not([disabled]):not(.disabled):not(.devui-disabled).devui-error { - :not(.devui-dropdown-origin-wrapper):not(.multiple-label-auto-complete-disabled) { + :not(.multiple-label-auto-complete-disabled) { div.devui-tags.devui-form-control { border-color: $devui-danger-line; background-color: $devui-danger-bg; @@ -434,7 +416,7 @@ d-tags-input:not([disabled]):not(.disabled):not(.devui-disabled).devui-error { } d-tree-select:not([disabled]):not(.disabled):not(.devui-disabled).devui-error { - :not(.devui-dropdown-origin-wrapper):not(.multiple-label-auto-complete-disabled) { + :not(.multiple-label-auto-complete-disabled) { & > div.devui-select-input.devui-dropdown-origin.devui-tree-select-input { border-color: $devui-danger-line; background-color: $devui-danger-bg; /* TODO: open时,下边框颜色 */ @@ -467,6 +449,20 @@ d-form-control { } } +.devui-form-controls.devui-form-control-has-suffix { + [dTextInput], + [dTextArea] { + padding-right: 28px; + } +} + +.devui-form-controls.devui-form-control-has-suffix.devui-form-control-has-feedback { + [dTextInput], + [dTextArea] { + padding-right: 56px; + } +} + .devui-form-controls.devui-form-control-has-feedback { [dTextInput], [dTextarea] { diff --git a/devui/text-input/__tests__/text-input.spec.ts b/devui/text-input/__tests__/text-input.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee3c28a0b6847384928e8dc5bf313e904f6ca829 --- /dev/null +++ b/devui/text-input/__tests__/text-input.spec.ts @@ -0,0 +1,129 @@ +import { mount } from '@vue/test-utils'; +import { ref, nextTick } from 'vue'; +import DTextInput from '../src/text-input'; + +describe('d-text-input', () => { + it('d-text-input render work', async () => { + const value = ref('abc'); + const wrapper = mount({ + components: { DTextInput }, + template: ` + + `, + setup () { + return { + value + }; + } + }); + const input = wrapper.find('input'); + expect(input.attributes('dtextinput')).toBe('true'); + expect(input.element.value).toBe('abc'); + + await input.setValue('def'); + expect(value.value).toBe('def'); + + value.value = 'thx'; + await nextTick(); + expect(input.element.value).toBe('thx'); + }); + + it('d-text-input bindEvents work', async () => { + const onChange = jest.fn(), + onFocus = jest.fn(), + onBlur = jest.fn(), + onKeydown = jest.fn(); + const wrapper = mount({ + components: { DTextInput }, + template: ` + + `, + setup () { + return { + onChange, + onFocus, + onBlur, + onKeydown + }; + } + }); + const input = wrapper.find('input'); + + await input.trigger('change'); + expect(onChange).toBeCalledTimes(1); + + await input.trigger('focus'); + expect(onFocus).toBeCalledTimes(1); + + await input.trigger('blur'); + expect(onBlur).toBeCalledTimes(1); + + await input.trigger('keydown'); + expect(onKeydown).toBeCalledTimes(1); + }); + + it('d-text-input disabled work', async () => { + const wrapper = mount(DTextInput, { + props: { + disabled: false + } + }); + const input = wrapper.find('input'); + expect(input.attributes('disabled')).toBe(undefined); + + await wrapper.setProps({ + disabled: true + }); + expect(input.attributes('disabled')).toBe(''); + }); + + it('d-text-input error work', async () => { + const wrapper = mount(DTextInput, { + props: { + error: false + } + }); + const input = wrapper.find('input'); + expect(input.classes()).not.toContain('error'); + + await wrapper.setProps({ + error: true + }); + expect(input.classes()).toContain('error'); + }); + + it('d-text-input size work', async () => { + const wrapper = mount(DTextInput); + const input = wrapper.find('input'); + expect(input.classes()).not.toContain('devui-textinput-sm'); + expect(input.classes()).not.toContain('devui-textinput-lg'); + + await wrapper.setProps({ + size: 'sm' + }); + expect(input.classes()).toContain('devui-textinput-sm'); + expect(input.classes()).not.toContain('devui-textinput-lg'); + + await wrapper.setProps({ + size: 'lg' + }); + expect(input.classes()).not.toContain('devui-textinput-sm'); + expect(input.classes()).toContain('devui-textinput-lg'); + }); + + it('d-text-input showPassword work', async () => { + const wrapper = mount(DTextInput); + const input = wrapper.find('input'); + + expect(input.attributes('type')).toBe('text'); + + await wrapper.setProps({ + showPassword: true + }); + expect(input.attributes('type')).toBe('password'); + }); +}); diff --git a/devui/text-input/demo/demo-basic.tsx b/devui/text-input/demo/demo-basic.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b4bf82be22d74738aa5b09b0fd47517b6c297bf1 --- /dev/null +++ b/devui/text-input/demo/demo-basic.tsx @@ -0,0 +1,20 @@ +import { defineComponent } from 'vue'; +import DTextInput from '../src/text-input'; + +export default defineComponent({ + name: 'DemoBasic', + setup() { + return () => { + return ( + <> +

Default

+ +

Disabled

+ +

Error

+ + + ); + } + } +}); diff --git a/devui/text-input/demo/demo-size.tsx b/devui/text-input/demo/demo-size.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2b8bb83ec31232de8cc81c0d013bf2d84dd285a7 --- /dev/null +++ b/devui/text-input/demo/demo-size.tsx @@ -0,0 +1,20 @@ +import { defineComponent } from 'vue'; +import DTextInput from '../src/text-input'; + +export default defineComponent({ + name: 'DemoSize', + setup() { + return () => { + return ( + <> +

small

+ +

Disabled

+ +

Error

+ + + ); + } + } +}); diff --git a/devui/text-input/demo/text-input-demo.tsx b/devui/text-input/demo/text-input-demo.tsx index fbfc389ac0453ceb346d24ab9c604307cbb306df..66ef800c9ee8e9dd7cd82051ca83a52033a9e43c 100644 --- a/devui/text-input/demo/text-input-demo.tsx +++ b/devui/text-input/demo/text-input-demo.tsx @@ -1,12 +1,27 @@ -import { defineComponent } from 'vue' + + +import { defineComponent } from 'vue'; +import { useDemo } from 'hooks/use-demo'; +import DemoBasic from './demo-basic'; +import DemoBasicCode from './demo-basic?raw'; +import DemoSize from './demo-size'; +import DemoSizeCode from './demo-size?raw'; export default defineComponent({ - name: 'd-text-input-demo', - props: { - }, - setup(props, ctx) { - return () => { - return
devui-text-input-demo
- } + name: 'DTextInputDemo', + render () { + return useDemo([ + { + id: 'demo-basic', + title: '基本用法', + code: DemoBasicCode, + content: + }, { + id: 'demo-size', + title: '尺寸', + code: DemoSizeCode, + content: + } + ]); } -}) \ No newline at end of file +}); diff --git a/devui/text-input/src/text-input.tsx b/devui/text-input/src/text-input.tsx new file mode 100644 index 0000000000000000000000000000000000000000..eafc297936b12f57cfce81518201dd15d627fef2 --- /dev/null +++ b/devui/text-input/src/text-input.tsx @@ -0,0 +1,72 @@ +import { defineComponent, computed } from 'vue'; +import { inputProps } from './use-input'; + +export default defineComponent({ + name: 'DTextInput', + props: inputProps, + emits: ['update:value', 'focus', 'blur', 'change', 'keydown'], + setup(props, ctx) { + const sizeCls = computed(() => `devui-textinput-${props.size}`); + const inputCls = computed(() => { + return { + error: props.error, + [sizeCls.value]: props.size !== '' + } + }); + const inputType = computed(() => props.showPassword ? 'password' : 'text'); + const onInput = ($event: Event) => { + ctx.emit('update:value', ($event.target as HTMLInputElement).value); + }, + onFocus = () => { + ctx.emit('focus'); + }, + onBlur = () => { + ctx.emit('blur'); + }, + onChange = ($event: Event) => { + ctx.emit('change', ($event.target as HTMLInputElement).value); + }, + onKeydown = ($event: KeyboardEvent) => { + ctx.emit('keydown', $event); + }; + + return { + inputCls, + inputType, + onInput, + onFocus, + onBlur, + onChange, + onKeydown + }; + }, + render () { + const { + inputCls, + inputType, + placeholder, + disabled, + onInput, + onFocus, + onBlur, + onChange, + onKeydown, + value + } = this; + + return ( + + ); + } +}); diff --git a/devui/text-input/src/use-input.tsx b/devui/text-input/src/use-input.tsx new file mode 100644 index 0000000000000000000000000000000000000000..58f1d6a07a41fdbe8b850f024071d6cb71bc3d70 --- /dev/null +++ b/devui/text-input/src/use-input.tsx @@ -0,0 +1,48 @@ +import { PropType } from 'vue'; + +export const inputProps = { + placeholder: { + type: String, + default: undefined + }, + disabled: { + type: Boolean, + default: false + }, + error: { + type: Boolean, + default: false + }, + size: { + type: String as PropType<'sm' | '' | 'lg'>, + default: '' + }, + showPassword: { + type: Boolean, + default: false + }, + value: { + type: String, + default: '' + }, + 'onUpdate:value': { + type: Function as PropType<(v: string) => void>, + default: undefined + }, + 'onChange': { + type: Function as PropType<(v: string) => void>, + default: undefined + }, + 'onKeydown': { + type: Function as PropType<(v: KeyboardEvent) => void>, + default: undefined + }, + 'onFocus': { + type: Function as PropType<() => void>, + default: undefined + }, + 'onBlur': { + type: Function as PropType<() => void>, + default: undefined + } +} as const; diff --git a/devui/text-input/text-input.tsx b/devui/text-input/text-input.tsx deleted file mode 100644 index 8f23393d02fa4e0a9d1f4edb8c65af0087f6f59a..0000000000000000000000000000000000000000 --- a/devui/text-input/text-input.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { defineComponent } from 'vue' - -export default defineComponent({ - name: 'd-text-input', - props: { - }, - setup(props, ctx) { - return () => { - return
devui-text-input
- } - } -}) \ No newline at end of file