diff --git a/packages/cdk/a11y/docs/Index.zh.md b/packages/cdk/a11y/docs/Index.zh.md index 3d3b26b22..c4809a72d 100644 --- a/packages/cdk/a11y/docs/Index.zh.md +++ b/packages/cdk/a11y/docs/Index.zh.md @@ -37,6 +37,13 @@ export interface FocusMonitor { * @param options 可用于配置焦点行为的参数 */ focusVia(element: ElementType, origin: FocusOrigin, options?: FocusOptions): void + + /** + * 让元素失去焦点. + * + * @param element 要失去焦点的元素. + */ + blurVia: (element: ElementType) => void } /** diff --git a/packages/cdk/a11y/src/focusMonitor.ts b/packages/cdk/a11y/src/focusMonitor.ts index aaf85ffaf..eb96f39d6 100644 --- a/packages/cdk/a11y/src/focusMonitor.ts +++ b/packages/cdk/a11y/src/focusMonitor.ts @@ -125,6 +125,13 @@ export interface FocusMonitor { * @param options Options that can be used to configure the focus behavior. */ focusVia(element: ElementType, origin: FocusOrigin, options?: FocusOptions): void + + /** + * Blur the element. + * + * @param element Element to blur. + */ + blurVia: (element: ElementType) => void } /** Monitors mouse and keyboard events to determine the cause of focus events. */ @@ -293,6 +300,26 @@ export function useFocusMonitor(options?: FocusMonitorOptions): FocusMonitor { } } + /** + * Blur the element. + * + * @param element Element to blur. + */ + function blurVia(element: ElementType): void { + const nativeElement = convertElement(element) + if (!nativeElement) { + return + } + + const focusedElement = _getDocument().activeElement + // If the element is focused already, calling `focus` again won't trigger the event listener + // which means that the focus classes won't be updated. If that's the case, update the classes + // directly without waiting for an event. + if (nativeElement === focusedElement && typeof nativeElement.blur === 'function') { + nativeElement.blur() + } + } + /** Access injected document if available or fallback to global document reference */ function _getDocument(): Document { return document @@ -539,7 +566,7 @@ export function useFocusMonitor(options?: FocusMonitorOptions): FocusMonitor { onScopeDispose(() => _elementInfo.forEach((_info, element) => stopMonitoring(element))) - return { monitor, stopMonitoring, focusVia } + return { monitor, stopMonitoring, focusVia, blurVia } } export const useSharedFocusMonitor = createSharedComposable(() => useFocusMonitor()) diff --git a/packages/components/_private/input/__tests__/__snapshots__/input.spec.ts.snap b/packages/components/_private/input/__tests__/__snapshots__/input.spec.ts.snap new file mode 100644 index 000000000..f8eff4c2c --- /dev/null +++ b/packages/components/_private/input/__tests__/__snapshots__/input.spec.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Input render work 1`] = `""`; + +exports[`Input render work 2`] = `"addonAfter"`; + +exports[`Input render work 3`] = ` +" +" +`; + +exports[`Input render work 4`] = ` +" +addonAfter" +`; diff --git a/packages/components/_private/input/__tests__/input.spec.ts b/packages/components/_private/input/__tests__/input.spec.ts new file mode 100644 index 000000000..0772458fd --- /dev/null +++ b/packages/components/_private/input/__tests__/input.spec.ts @@ -0,0 +1,196 @@ +import { MountingOptions, mount } from '@vue/test-utils' + +import { renderWork } from '@tests' + +import Input from '../src/Input' +import { InputProps } from '../src/types' + +describe('Input', () => { + const InputMount = (options?: MountingOptions>) => { + const { props, ...rest } = (options || {}) as MountingOptions + return mount(Input, { props: { size: 'md', ...props }, ...rest }) + } + + renderWork(Input, { + props: { size: 'md' }, + }) + + renderWork(Input, { + props: { size: 'md', addonAfter: 'addonAfter' }, + }) + + renderWork(Input, { + props: { size: 'md', suffix: 'setting' }, + }) + + renderWork(Input, { + props: { size: 'md', addonAfter: 'addonAfter', suffix: 'setting' }, + }) + + test('addonAfter and addonBefore work', async () => { + const wrapper = InputMount({ props: { addonAfter: 'addonAfter', addonBefore: 'addonBefore' } }) + + expect(wrapper.classes()).toContain('ix-input-with-addon-after') + expect(wrapper.classes()).toContain('ix-input-with-addon-before') + + const addons = wrapper.findAll('.ix-input-addon') + + expect(addons[0].text()).toBe('addonBefore') + expect(addons[1].text()).toBe('addonAfter') + + await wrapper.setProps({ addonAfter: 'addonAfter change' }) + + expect(addons[1].text()).toBe('addonAfter change') + + await wrapper.setProps({ addonBefore: 'addonBefore change' }) + + expect(addons[0].text()).toBe('addonBefore change') + + await wrapper.setProps({ addonAfter: '' }) + + expect(wrapper.classes()).not.toContain('ix-input-with-addon-after') + expect(wrapper.findAll('.ix-input-addon').length).toBe(1) + + await wrapper.setProps({ addonBefore: '' }) + + expect(wrapper.classes()).not.toContain('ix-input-with-addon-before') + expect(wrapper.findAll('.ix-input-addon').length).toBe(0) + }) + + test('addonAfter and addonBefore slots work', async () => { + const wrapper = InputMount({ + props: { addonAfter: 'addonAfter', addonBefore: 'addonBefore' }, + slots: { addonAfter: 'addonAfter slot', addonBefore: 'addonBefore slot' }, + }) + + expect(wrapper.classes()).toContain('ix-input-with-addon-after') + expect(wrapper.classes()).toContain('ix-input-with-addon-before') + + const addons = wrapper.findAll('.ix-input-addon') + + expect(addons[0].text()).toBe('addonBefore slot') + expect(addons[1].text()).toBe('addonAfter slot') + }) + + test('borderless work', async () => { + const wrapper = InputMount({ props: { borderless: true } }) + + expect(wrapper.classes()).toContain('ix-input-borderless') + + await wrapper.setProps({ borderless: false }) + + expect(wrapper.classes()).not.toContain('ix-input-borderless') + }) + + test('clearable work', async () => { + const onClear = jest.fn() + const wrapper = InputMount({ props: { clearIcon: 'close', clearable: true, onClear } }) + + expect(wrapper.find('.ix-input-clear').exists()).toBe(true) + + await wrapper.setProps({ clearVisible: true }) + + expect(wrapper.find('.ix-input-clear').classes()).toContain('visible') + + await wrapper.find('.ix-input-clear').trigger('click') + + expect(onClear).toBeCalled() + + await wrapper.setProps({ clearVisible: false }) + + expect(wrapper.find('.ix-input-clear').classes()).not.toContain('visible') + + await wrapper.setProps({ clearable: false }) + + expect(wrapper.find('.ix-input-clear').exists()).toBe(false) + }) + + test('disabled work', async () => { + const onFocus = jest.fn() + const onBlur = jest.fn() + + const wrapper = InputMount({ props: { disabled: true, onFocus, onBlur } }) + await wrapper.find('input').trigger('focus') + + expect(wrapper.classes()).toContain('ix-input-disabled') + expect(onFocus).not.toBeCalled() + + await wrapper.find('input').trigger('blur') + + expect(onBlur).not.toBeCalled() + + await wrapper.setProps({ disabled: false }) + await wrapper.find('input').trigger('focus') + + expect(wrapper.classes()).not.toContain('ix-input-disabled') + expect(onFocus).toBeCalled() + + await wrapper.find('input').trigger('blur') + + expect(onBlur).toBeCalled() + }) + + test('focused work', async () => { + const wrapper = InputMount({ props: { focused: true } }) + + expect(wrapper.classes()).toContain('ix-input-focused') + + await wrapper.setProps({ focused: false }) + + expect(wrapper.classes()).not.toContain('ix-input-focused') + }) + + test('suffix and prefix work', async () => { + const wrapper = InputMount({ props: { suffix: 'up', prefix: 'down' } }) + + const suffix = wrapper.find('.ix-input-suffix') + const prefix = wrapper.find('.ix-input-prefix') + + expect(suffix.find('.ix-icon-up').exists()).toBe(true) + expect(prefix.find('.ix-icon-down').exists()).toBe(true) + + await wrapper.setProps({ suffix: 'left' }) + + expect(suffix.find('.ix-icon-left').exists()).toBe(true) + + await wrapper.setProps({ prefix: 'right' }) + + expect(prefix.find('.ix-icon-right').exists()).toBe(true) + + await wrapper.setProps({ suffix: '' }) + + expect(wrapper.find('.ix-input-suffix').exists()).toBe(false) + + await wrapper.setProps({ prefix: '' }) + + expect(wrapper.find('.ix-input-prefix').exists()).toBe(false) + }) + + test('suffix and prefix slots work', async () => { + const wrapper = InputMount({ + props: { suffix: 'up', prefix: 'down' }, + slots: { suffix: 'suffix slot', prefix: 'prefix slot' }, + }) + + const suffix = wrapper.find('.ix-input-suffix') + const prefix = wrapper.find('.ix-input-prefix') + + expect(suffix.find('.ix-icon-up').exists()).toBe(false) + expect(prefix.find('.ix-icon-down').exists()).toBe(false) + + expect(suffix.text()).toBe('suffix slot') + expect(prefix.text()).toBe('prefix slot') + }) + + test('size work', async () => { + const wrapper = InputMount({ props: { size: 'lg' } }) + + expect(wrapper.classes()).toContain('ix-input-lg') + + await wrapper.setProps({ size: 'sm' }) + expect(wrapper.classes()).toContain('ix-input-sm') + + await wrapper.setProps({ size: 'md' }) + expect(wrapper.classes()).toContain('ix-input-md') + }) +}) diff --git a/packages/components/_private/input/index.ts b/packages/components/_private/input/index.ts new file mode 100644 index 000000000..464d2bff5 --- /dev/null +++ b/packages/components/_private/input/index.ts @@ -0,0 +1,20 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { InputComponent } from './src/types' + +import Input from './src/Input' + +const ɵInput = Input as unknown as InputComponent + +export { ɵInput } + +export type { + InputInstance as ɵInputInstance, + InputComponent as ɵInputComponent, + InputPublicProps as ɵInputProps, +} from './src/types' diff --git a/packages/components/_private/input/src/Input.tsx b/packages/components/_private/input/src/Input.tsx new file mode 100644 index 000000000..275ff5261 --- /dev/null +++ b/packages/components/_private/input/src/Input.tsx @@ -0,0 +1,115 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { CSSProperties, Slot } from 'vue' + +import { computed, defineComponent, normalizeClass, ref } from 'vue' + +import { useGlobalConfig } from '@idux/components/config' +import { IxIcon } from '@idux/components/icon' + +import { inputProps } from './types' + +export default defineComponent({ + inheritAttrs: false, + props: inputProps, + setup(props, { attrs, slots, expose }) { + const common = useGlobalConfig('common') + const mergedPrefixCls = computed(() => `${common.prefixCls}-input`) + const inputRef = ref() + const getInputElement = () => inputRef.value + expose({ getInputElement }) + + const classes = computed(() => { + const { borderless, clearable, disabled, focused, size, addonAfter, addonBefore, prefix, suffix } = props + const prefixCls = mergedPrefixCls.value + return normalizeClass({ + [prefixCls]: true, + [`${prefixCls}-${size}`]: true, + [`${prefixCls}-borderless`]: borderless, + [`${prefixCls}-clearable`]: clearable, + [`${prefixCls}-disabled`]: disabled, + [`${prefixCls}-focused`]: focused, + [`${prefixCls}-with-addon-after`]: addonAfter || slots.addonAfter, + [`${prefixCls}-with-addon-before`]: addonBefore || slots.addonBefore, + [`${prefixCls}-with-prefix`]: prefix || slots.prefix, + [`${prefixCls}-with-suffix`]: suffix || slots.suffix, + }) + }) + + return () => { + const { clearable, clearIcon, clearVisible, disabled, addonAfter, addonBefore, prefix, suffix, onClear } = props + const prefixCls = mergedPrefixCls.value + + const addonBeforeNode = renderAddon(slots.addonBefore, addonBefore, `${prefixCls}-addon`) + const addonAfterNode = renderAddon(slots.addonAfter, addonAfter, `${prefixCls}-addon`) + const prefixNode = renderIcon(slots.prefix, prefix, `${prefixCls}-prefix`) + const suffixNode = renderIcon(slots.suffix, suffix, `${prefixCls}-suffix`) + const clearNode = clearable && ( + + + + ) + + if (!(addonBeforeNode || addonAfterNode || prefixNode || suffixNode || clearNode)) { + return + } + + const { class: className, style, ...rest } = attrs + const classNames = normalizeClass([classes.value, className]) + const inputNode = + + if (!(addonBeforeNode || addonAfterNode)) { + return ( + + {prefixNode} + {inputNode} + {suffixNode} + {clearNode} + + ) + } + + if (!(prefixNode || suffixNode || clearNode)) { + return ( + + {addonBeforeNode} + {inputNode} + {addonAfterNode} + + ) + } + + return ( + + {addonBeforeNode} + + {prefixNode} + {inputNode} + {suffixNode} + {clearNode} + + {addonAfterNode} + + ) + } + }, +}) + +function renderAddon(slot: Slot | undefined, prop: string | undefined, cls: string) { + if (!(slot || prop)) { + return undefined + } + return {slot ? slot() : prop} +} + +function renderIcon(slot: Slot | undefined, prop: string | undefined, cls: string) { + if (!(slot || prop)) { + return undefined + } + return {slot ? slot() : } +} diff --git a/packages/components/_private/input/src/types.ts b/packages/components/_private/input/src/types.ts new file mode 100644 index 000000000..ffc31a9d9 --- /dev/null +++ b/packages/components/_private/input/src/types.ts @@ -0,0 +1,38 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { IxInnerPropTypes, IxPublicPropTypes } from '@idux/cdk/utils' +import type { FormSize } from '@idux/components/form' +import type { DefineComponent, InputHTMLAttributes } from 'vue' + +import { IxPropTypes } from '@idux/cdk/utils' + +export const inputProps = { + addonAfter: IxPropTypes.string, + addonBefore: IxPropTypes.string, + borderless: IxPropTypes.bool, + clearable: IxPropTypes.bool, + clearIcon: IxPropTypes.string, + clearVisible: IxPropTypes.bool, + disabled: IxPropTypes.bool, + focused: IxPropTypes.bool, + prefix: IxPropTypes.string, + size: IxPropTypes.oneOf(['sm', 'md', 'lg']), + suffix: IxPropTypes.string, + onClear: IxPropTypes.func<(evt: MouseEvent) => void>(), +} + +export type InputProps = IxInnerPropTypes +export type InputPublicProps = IxPublicPropTypes +export interface InputBindings { + getInputElement: () => HTMLInputElement | undefined +} +export type InputComponent = DefineComponent< + Omit & InputPublicProps, + InputBindings +> +export type InputInstance = InstanceType> diff --git a/packages/components/_private/input/style/index.less b/packages/components/_private/input/style/index.less new file mode 100644 index 000000000..99a40d839 --- /dev/null +++ b/packages/components/_private/input/style/index.less @@ -0,0 +1,274 @@ +@import '../../../style/mixins/borderless.less'; +@import '../../../style/mixins/placeholder.less'; +@import '../../../style/mixins/reset.less'; +@import './mixin.less'; + +.@{input-prefix} { + .reset-component(); + .input-inner(); + + background-color: @input-background-color; + border: @input-border-width @input-border-style @input-border-color; + border-radius: @input-border-radius; + transition: all @input-transition-duration @input-transition-function; + + .@{input-prefix}-inner { + .input-inner(); + + border: @input-border-width @input-border-style @input-border-color; + } + + &:hover { + .input-hover(); + } + + &-inner, + &-wrapper { + + &:hover { + .input-hover(); + } + } + + &:focus, + &-focused { + .input-active(); + + .@{input-prefix}-inner, + .@{input-prefix}-wrapper { + .input-active(); + } + } + + &[disabled], + &-disabled { + .input-disabled(); + + .@{input-prefix}-inner, + .@{input-prefix}-wrapper { + .input-disabled(); + } + + &:hover { + .input-hover(@input-border-color); + + .@{input-prefix}-inner, + .@{input-prefix}-wrapper { + .input-hover(@input-border-color); + } + } + } + + &-borderless { + + &, + &:hover, + &:focus, + &-focused, + &[disabled], + &-disabled { + .borderless(); + + .@{input-prefix}-inner, + .@{input-prefix}-wrapper { + .borderless(); + } + } + } + + &-sm { + .input-size(@input-font-size-sm; @input-padding-vertical-sm; @input-padding-horizontal-sm;); + } + + &-md { + .input-size(@input-font-size-md; @input-padding-vertical-md; @input-padding-horizontal-md;); + } + + &-lg { + .input-size(@input-font-size-lg; @input-padding-vertical-lg; @input-padding-horizontal-lg;); + } + + &-with-addon-after, + &-with-addon-before { + display: inline-flex; + padding: 0; + border: none; + box-shadow: none; + + &.@{input-prefix}-sm { + .@{input-prefix}-addon, + .@{input-prefix}-wrapper, + .@{input-prefix}-inner { + padding: @input-padding-vertical-sm @input-padding-horizontal-sm; + } + + .@{input-prefix}-addon { + .@{select-prefix} { + margin: -@input-padding-vertical-sm -@input-padding-horizontal-sm; + } + } + + .@{input-prefix}-wrapper .@{input-prefix}-inner { + padding: 0; + } + } + + &.@{input-prefix}-md { + .@{input-prefix}-addon, + .@{input-prefix}-wrapper, + .@{input-prefix}-inner { + padding: @input-padding-vertical-md @input-padding-horizontal-md; + } + + .@{input-prefix}-addon { + .@{select-prefix} { + margin: -@input-padding-vertical-md -@input-padding-horizontal-md; + } + } + + .@{input-prefix}-wrapper .@{input-prefix}-inner { + padding: 0; + } + } + + &.@{input-prefix}-lg { + .@{input-prefix}-addon, + .@{input-prefix}-wrapper, + .@{input-prefix}-inner { + padding: @input-padding-vertical-lg @input-padding-horizontal-lg; + } + + .@{input-prefix}-addon { + .@{select-prefix} { + margin: -@input-padding-vertical-lg -@input-padding-horizontal-lg; + } + } + + .@{input-prefix}-wrapper .@{input-prefix}-inner { + padding: 0; + } + } + } + + &-addon { + display: inline-block; + text-align: center; + background-color: @input-addon-background-color; + border: @input-border-width @input-border-style @input-border-color; + border-radius: @input-border-radius; + white-space: nowrap; + + &:first-child { + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + .@{idux-prefix}-select { + + &-selector { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + } + } + + &:last-child { + border-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + .@{idux-prefix}-select { + + &-selector { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + } + + .@{idux-prefix}-select { + + &-selector { + background-color: inherit; + border: @input-border-width @input-border-style transparent; + box-shadow: none; + } + } + } + + &-clearable, + &-with-prefix, + &-with-suffix { + display: inline-flex; + + .@{input-prefix}-inner { + padding: 0; + border: none; + box-shadow: none; + } + } + + &-suffix, + &-prefix { + display: inline-flex; + align-items: center; + color: @input-placeholder-color; + } + + &-prefix { + margin-right: @input-wrapper-inner-margin; + } + + &-suffix { + margin-left: @input-wrapper-inner-margin; + } + + &-clear { + position: absolute; + z-index: 1; + cursor: pointer; + opacity: 0; + color: @input-placeholder-color; + background-color: @input-background-color; + transition: all @input-transition-duration; + + &:hover { + color: @input-color; + } + } + + &-clearable { + + &:hover { + .@{input-prefix}-clear.visible { + opacity: 1; + } + } + + &.@{input-prefix}-sm .@{input-prefix}-clear { + right: @input-padding-horizontal-sm; + } + + &.@{input-prefix}-md .@{input-prefix}-clear { + right: @input-padding-horizontal-md; + } + + &.@{input-prefix}-lg .@{input-prefix}-clear { + right: @input-padding-horizontal-lg; + } + } + + &-wrapper { + position: relative; + display: inline-flex; + align-items: center; + width: 100%; + border: @input-border-width @input-border-style @input-border-color; + + .@{input-prefix}-inner { + padding: 0; + border: none; + box-shadow: none; + } + } +} diff --git a/packages/components/_private/input/style/mixin.less b/packages/components/_private/input/style/mixin.less new file mode 100644 index 000000000..09c34c620 --- /dev/null +++ b/packages/components/_private/input/style/mixin.less @@ -0,0 +1,40 @@ +.input-inner() { + position: relative; + display: inline-block; + width: 100%; + min-width: 0; + outline: 0; + + &[disabled] { + cursor: not-allowed; + } + + .placeholder(@input-placeholder-color); +} + +.input-size(@font-size; @padding-vertical; @padding-horizontal;) { + font-size: @font-size; + padding: @padding-vertical @padding-horizontal; +} + +.input-hover(@color: @input-hover-color) { + border-color: @color; +} + +.input-active(@border-color: @input-active-color; @box-shadow: @input-active-box-shadow) { + border-color: @border-color; + box-shadow: @box-shadow; + + &:hover { + .input-hover(@border-color); + } +} + +.input-disabled() { + color: @input-disabled-color; + background-color: @input-disabled-background-color; + border-color: @input-border-color; + box-shadow: none; + cursor: not-allowed; + opacity: 1; +} diff --git a/packages/components/_private/input/style/themes/default.less b/packages/components/_private/input/style/themes/default.less new file mode 100644 index 000000000..c1c89d158 --- /dev/null +++ b/packages/components/_private/input/style/themes/default.less @@ -0,0 +1,2 @@ +@import '../index.less'; +@import './default.variable.less'; diff --git a/packages/components/_private/input/style/themes/default.ts b/packages/components/_private/input/style/themes/default.ts new file mode 100644 index 000000000..027ca3f89 --- /dev/null +++ b/packages/components/_private/input/style/themes/default.ts @@ -0,0 +1,4 @@ +// style dependencies +import '@idux/components/style/core/default' + +import './default.less' diff --git a/packages/components/_private/input/style/themes/default.variable.less b/packages/components/_private/input/style/themes/default.variable.less new file mode 100644 index 000000000..89572ce9f --- /dev/null +++ b/packages/components/_private/input/style/themes/default.variable.less @@ -0,0 +1,37 @@ +@import '../../../../style/themes/default.less'; +@import '../../../../form/style/themes/default.variable.less'; + +@input-font-size-sm: @form-font-size-sm; +@input-font-size-md: @form-font-size-md; +@input-font-size-lg: @form-font-size-lg; +@input-line-height: @form-line-height; +@input-height-sm: @form-height-sm; +@input-height-md: @form-height-md; +@input-height-lg: @form-height-lg; +@input-padding-horizontal-sm: @form-padding-horizontal-sm; +@input-padding-horizontal-md: @form-padding-horizontal-md; +@input-padding-horizontal-lg: @form-padding-horizontal-lg; +@input-padding-vertical-sm: @form-padding-vertical-sm; +@input-padding-vertical-md: @form-padding-vertical-md; +@input-padding-vertical-lg: @form-padding-vertical-lg; + +@input-border-width: @form-border-width; +@input-border-style: @form-border-style; +@input-border-color: @form-border-color; +@input-border-radius: @border-radius-sm; + +@input-color: @form-color; +@input-color-secondary: @form-color-secondary; +@input-background-color: @form-background-color; +@input-placeholder-color: @form-placeholder-color; +@input-hover-color: @form-hover-color; +@input-active-color: @form-active-color; +@input-active-box-shadow: @form-active-box-shadow; +@input-disabled-color: @form-disabled-color; +@input-disabled-background-color: @form-disabled-background-color; + +@input-transition-duration: @form-transition-duration; +@input-transition-function: @form-transition-function; + +@input-addon-background-color: @background-color-base; +@input-wrapper-inner-margin: @spacing-xs; diff --git a/packages/components/default.less b/packages/components/default.less index e344cd905..aa6f6a53d 100644 --- a/packages/components/default.less +++ b/packages/components/default.less @@ -1,6 +1,7 @@ @import './style/core/default.less'; @import './_private/collapse-transition/style/themes/default.less'; +@import './_private/input/style/themes/default.less'; @import './_private/mask/style/themes/default.less'; @import './_private/overlay/style/themes/default.less'; @import './_private/time-panel/style/themes/default.less'; diff --git a/packages/components/form/style/themes/default.less b/packages/components/form/style/themes/default.less index 7bfe1181a..c1c89d158 100644 --- a/packages/components/form/style/themes/default.less +++ b/packages/components/form/style/themes/default.less @@ -1,3 +1,2 @@ -@import '../../../style/themes/default.less'; -@import './default.variable.less'; @import '../index.less'; +@import './default.variable.less'; diff --git a/packages/components/form/style/themes/default.variable.less b/packages/components/form/style/themes/default.variable.less index 157abd7db..f47e438a4 100644 --- a/packages/components/form/style/themes/default.variable.less +++ b/packages/components/form/style/themes/default.variable.less @@ -1,3 +1,5 @@ +@import '../../../style/themes/default.less'; + @form-font-size-xs: @font-size-sm; @form-font-size-sm: @font-size-md; @form-font-size-md: @font-size-md; diff --git a/packages/components/input-number/__tests__/__snapshots__/inputNumber.spec.ts.snap b/packages/components/input-number/__tests__/__snapshots__/inputNumber.spec.ts.snap index da1579d26..4132ec811 100644 --- a/packages/components/input-number/__tests__/__snapshots__/inputNumber.spec.ts.snap +++ b/packages/components/input-number/__tests__/__snapshots__/inputNumber.spec.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`InputNumber render work 1`] = `""`; +exports[`InputNumber render work 1`] = `""`; diff --git a/packages/components/input-number/__tests__/inputNumber.spec.ts b/packages/components/input-number/__tests__/inputNumber.spec.ts index 414a5c409..0a8dc8057 100644 --- a/packages/components/input-number/__tests__/inputNumber.spec.ts +++ b/packages/components/input-number/__tests__/inputNumber.spec.ts @@ -47,41 +47,31 @@ describe('InputNumber', () => { }) test('disabled work', async () => { - const onFocus = jest.fn() - const onBlur = jest.fn() + const onUpdateValue = jest.fn() + const wrapper = InputNumberMount({ props: { disabled: true, 'onUpdate:value': onUpdateValue } }) - const wrapper = InputNumberMount({ props: { disabled: true, onFocus, onBlur } }) - await wrapper.find('input').trigger('focus') - - expect(wrapper.classes()).toContain('ix-input-number-disabled') - expect(onFocus).not.toBeCalled() - - await wrapper.find('input').trigger('blur') + await wrapper.find('.ix-input-number-increase').trigger('click') - expect(onBlur).not.toBeCalled() + expect(onUpdateValue).not.toBeCalled() await wrapper.setProps({ disabled: false }) - await wrapper.find('input').trigger('focus') - - expect(wrapper.classes()).not.toContain('ix-input-number-disabled') - expect(onFocus).toBeCalled() - - await wrapper.find('input').trigger('blur') + await wrapper.find('.ix-input-number-increase').trigger('click') - expect(onBlur).toBeCalled() + expect(onUpdateValue).toBeCalled() }) test('readonly work', async () => { - const onFocus = jest.fn() - const onBlur = jest.fn() - const wrapper = InputNumberMount({ props: { readonly: true, onFocus, onBlur } }) - await wrapper.find('input').trigger('focus') + const onUpdateValue = jest.fn() + const wrapper = InputNumberMount({ props: { readonly: true, 'onUpdate:value': onUpdateValue } }) + + await wrapper.find('.ix-input-number-increase').trigger('click') - expect(onFocus).toBeCalled() + expect(onUpdateValue).not.toBeCalled() - await wrapper.find('input').trigger('blur') + await wrapper.setProps({ readonly: false }) + await wrapper.find('.ix-input-number-increase').trigger('click') - expect(onBlur).toBeCalled() + expect(onUpdateValue).toBeCalled() }) test('step work', async () => { diff --git a/packages/components/input-number/demo/Basic.vue b/packages/components/input-number/demo/Basic.vue index 2016170f1..a23978275 100644 --- a/packages/components/input-number/demo/Basic.vue +++ b/packages/components/input-number/demo/Basic.vue @@ -1,5 +1,12 @@ diff --git a/packages/components/input-number/src/InputNumber.tsx b/packages/components/input-number/src/InputNumber.tsx index a0b649381..f64145824 100644 --- a/packages/components/input-number/src/InputNumber.tsx +++ b/packages/components/input-number/src/InputNumber.tsx @@ -5,33 +5,31 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { StyleValue } from 'vue' +import type { ɵInputInstance } from '@idux/components/_private/input' -import { computed, defineComponent, inject, normalizeClass } from 'vue' +import { computed, defineComponent, inject, normalizeClass, onMounted, ref } from 'vue' +import { ɵInput } from '@idux/components/_private/input' import { useGlobalConfig } from '@idux/components/config' import { FORM_TOKEN } from '@idux/components/form' import { IxIcon } from '@idux/components/icon' -import { IxInput } from '@idux/components/input' -import { useFormElement } from '@idux/components/utils' +import { useFormFocusMonitor } from '@idux/components/utils' import { inputNumberProps } from './types' import { useInputNumber } from './useInputNumber' export default defineComponent({ name: 'IxInputNumber', - inheritAttrs: false, props: inputNumberProps, - setup(props, { attrs, expose }) { + setup(props, { expose }) { const common = useGlobalConfig('common') const config = useGlobalConfig('inputNumber') - const { elementRef, focus, blur } = useFormElement() - const formContext = inject(FORM_TOKEN, null) const { displayValue, nowValue, isIllegal, isDisabled, + isFocused, handleInput, handleFocus, handleBlur, @@ -39,58 +37,57 @@ export default defineComponent({ handleDec, handleInc, } = useInputNumber(props, config) + const { elementRef, focus, blur } = useFormFocusMonitor({ handleBlur, handleFocus }) + expose({ focus, blur }) + const formContext = inject(FORM_TOKEN, null) const mergedPrefixCls = computed(() => `${common.prefixCls}-input-number`) const size = computed(() => props.size ?? formContext?.size.value ?? config.size) const classes = computed(() => { const prefixCls = mergedPrefixCls.value - const classes = { + return normalizeClass({ [prefixCls]: true, - [`${prefixCls}-${size.value}`]: true, - [`${prefixCls}-disabled`]: isDisabled.value, [`${prefixCls}-illegal`]: isIllegal.value, - } - return normalizeClass([classes, attrs.class]) + }) }) - expose({ focus, blur }) + const inputRef = ref<ɵInputInstance>() + onMounted(() => { + elementRef.value = inputRef.value!.getInputElement() + }) return () => { - const { class: className, style, ...rest } = attrs return ( - - ( - - - - ), - addonAfter: () => ( - - - - ), - }} - /> - + <ɵInput + class={classes.value} + ref={inputRef} + type="text" + autocomplete="off" + aria-valuemin={props.min} + aria-valuemax={props.max} + aria-valuenow={nowValue.value} + disabled={isDisabled.value} + focused={isFocused.value} + readonly={props.readonly} + placeholder={props.placeholder} + size={size.value} + value={displayValue.value} + onInput={handleInput} + onKeydown={handleKeyDown} + v-slots={{ + addonBefore: () => ( + + + + ), + addonAfter: () => ( + + + + ), + }} + /> ) } }, diff --git a/packages/components/input-number/src/useInputNumber.ts b/packages/components/input-number/src/useInputNumber.ts index d69485d91..26160b814 100644 --- a/packages/components/input-number/src/useInputNumber.ts +++ b/packages/components/input-number/src/useInputNumber.ts @@ -19,6 +19,7 @@ export interface InputNumberBindings { displayValue: Ref isIllegal: Ref isDisabled: ComputedRef + isFocused: Ref nowValue: ComputedRef handleKeyDown: (evt: KeyboardEvent) => void @@ -164,13 +165,17 @@ export function useInputNumber(props: InputNumberProps, config: InputNumberConfi } } + const isFocused = ref(false) function handleFocus(evt: FocusEvent) { + isFocused.value = true callEmit(props.onFocus, evt) } function handleBlur(evt: FocusEvent) { + isFocused.value = false updateModelValueFromDisplayValue() callEmit(props.onBlur, evt) + accessor.markAsBlurred() } watch( @@ -192,6 +197,7 @@ export function useInputNumber(props: InputNumberProps, config: InputNumberConfi displayValue, isIllegal, isDisabled, + isFocused, nowValue, handleKeyDown, handleDec, diff --git a/packages/components/input-number/style/index.less b/packages/components/input-number/style/index.less index 2b0c28f4a..27c9bbedc 100644 --- a/packages/components/input-number/style/index.less +++ b/packages/components/input-number/style/index.less @@ -1,50 +1,61 @@ @import '../../style/mixins/reset.less'; .@{input-number-prefix} { - .reset-component(); - display: inline-block; - width: @input-number-width-md; - - & .@{idux-prefix}-input-wrapper { - border-radius: 0; - } - - & .@{idux-prefix}-input-inner { - text-align: center; + &-decrease:hover, + &-increase:hover { + color: @input-number-button-hover-color; } - & .@{idux-prefix}-input-addon { + & .@{input-prefix}-addon { padding: 0 !important; background-color: @input-number-button-bg; } - &-sm { + & .@{input-prefix}-inner { + text-align: center; + } + + &.@{input-prefix}-sm { width: @input-number-width-sm; - font-size: @input-number-font-size-sm; + + .@{input-number-prefix}-decrease, + .@{input-number-prefix}-increase { + width: @input-number-height-sm; + } } - &-md { + &.@{input-prefix}-md { width: @input-number-width-md; - font-size: @input-number-font-size-md; + + .@{input-number-prefix}-decrease, + .@{input-number-prefix}-increase { + width: @input-number-height-md; + } } - &-lg { + &.@{input-prefix}-lg { width: @input-number-width-lg; - font-size: @input-number-font-size-lg; + + .@{input-number-prefix}-decrease, + .@{input-number-prefix}-increase { + width: @input-number-height-lg; + } } - &-disabled { - & .@{input-number-prefix}-decrease, - & .@{input-number-prefix}-increase { - cursor: not-allowed; - color: @input-number-disabled-color !important; + &.@{input-prefix}-disabled { + .@{input-prefix}-addon { background-color: @input-number-disabled-bg; } + .@{input-number-prefix}-decrease, + .@{input-number-prefix}-increase { + cursor: not-allowed; + color: @input-number-disabled-color; + } } &-illegal { - & .@{idux-prefix}-input-inner { + .@{idux-prefix}-input-inner { color: @input-number-error; } } @@ -56,30 +67,4 @@ justify-content: center; align-items: center; } - - &&-sm { - .@{input-number-prefix}-decrease, - .@{input-number-prefix}-increase { - width: @input-number-height-sm; - } - } - - &&-md { - .@{input-number-prefix}-decrease, - .@{input-number-prefix}-increase { - width: @input-number-height-md; - } - } - - &&-lg { - .@{input-number-prefix}-decrease, - .@{input-number-prefix}-increase { - width: @input-number-height-lg; - } - } - - & &-decrease:hover, - &-increase:hover { - color: @input-number-button-hover-color; - } } diff --git a/packages/components/input-number/style/themes/default.less b/packages/components/input-number/style/themes/default.less index c26931dfe..2460f8f1f 100644 --- a/packages/components/input-number/style/themes/default.less +++ b/packages/components/input-number/style/themes/default.less @@ -1,5 +1,2 @@ -@import '../../../style/themes/default.less'; -@import '../../../form/style/themes/default.variable.less'; -@import '../../../input/style/themes/default.variable.less'; @import '../index.less'; @import './default.variable.less'; diff --git a/packages/components/input-number/style/themes/default.ts b/packages/components/input-number/style/themes/default.ts index 8aaddc579..8c3bd36df 100644 --- a/packages/components/input-number/style/themes/default.ts +++ b/packages/components/input-number/style/themes/default.ts @@ -1,5 +1,6 @@ // style dependencies import '@idux/components/style/core/default' +import '@idux/components/_private/input/style/themes/default' import '@idux/components/icon/style/themes/default' import './default.less' diff --git a/packages/components/input-number/style/themes/default.variable.less b/packages/components/input-number/style/themes/default.variable.less index fcbd91505..e9f25eab5 100644 --- a/packages/components/input-number/style/themes/default.variable.less +++ b/packages/components/input-number/style/themes/default.variable.less @@ -1,6 +1,5 @@ -@input-number-font-size-sm: @input-font-size-sm; -@input-number-font-size-md: @input-font-size-md; -@input-number-font-size-lg: @input-font-size-lg; +@import '../../../style/themes/default.less'; +@import '../../../_private/input/style/themes/default.variable.less'; @input-number-height-sm: @input-height-sm; @input-number-height-md: @input-height-md; diff --git a/packages/components/input/__tests__/__snapshots__/input.spec.ts.snap b/packages/components/input/__tests__/__snapshots__/input.spec.ts.snap index b99715030..8eff15b3f 100644 --- a/packages/components/input/__tests__/__snapshots__/input.spec.ts.snap +++ b/packages/components/input/__tests__/__snapshots__/input.spec.ts.snap @@ -1,6 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Input render work 1`] = ` -" -" -`; +exports[`Input render work 1`] = `""`; diff --git a/packages/components/input/__tests__/input.spec.ts b/packages/components/input/__tests__/input.spec.ts index f84629284..5924fa6bd 100644 --- a/packages/components/input/__tests__/input.spec.ts +++ b/packages/components/input/__tests__/input.spec.ts @@ -68,179 +68,11 @@ describe('Input', () => { }) test('disabled work', async () => { - const onFocus = jest.fn() - const onBlur = jest.fn() - - const wrapper = InputMount({ props: { disabled: true, onFocus, onBlur } }) - await wrapper.find('input').trigger('focus') - + const wrapper = InputMount({ props: { disabled: true } }) expect(wrapper.classes()).toContain('ix-input-disabled') - expect(wrapper.classes()).not.toContain('ix-input-focused') - expect(onFocus).not.toBeCalled() - - await wrapper.find('input').trigger('blur') - - expect(onBlur).not.toBeCalled() await wrapper.setProps({ disabled: false }) - await wrapper.find('input').trigger('focus') expect(wrapper.classes()).not.toContain('ix-input-disabled') - expect(wrapper.classes()).toContain('ix-input-focused') - expect(onFocus).toBeCalled() - - await wrapper.find('input').trigger('blur') - - expect(onBlur).toBeCalled() - }) - - test('readonly work', async () => { - const onFocus = jest.fn() - const onBlur = jest.fn() - const wrapper = InputMount({ props: { readonly: true, onFocus, onBlur } }) - await wrapper.find('input').trigger('focus') - - expect(onFocus).toBeCalled() - - await wrapper.find('input').trigger('blur') - - expect(onBlur).toBeCalled() - }) - - test('addonAfter and addonBefore work', async () => { - const wrapper = InputMount({ props: { addonAfter: 'addonAfter', addonBefore: 'addonBefore' } }) - - expect(wrapper.classes()).toContain('ix-input-with-addon-after') - expect(wrapper.classes()).toContain('ix-input-with-addon-before') - - const addons = wrapper.findAll('.ix-input-addon') - - expect(addons[0].text()).toBe('addonBefore') - expect(addons[1].text()).toBe('addonAfter') - - await wrapper.setProps({ addonAfter: 'addonAfter change' }) - - expect(addons[1].text()).toBe('addonAfter change') - - await wrapper.setProps({ addonBefore: 'addonBefore change' }) - - expect(addons[0].text()).toBe('addonBefore change') - - await wrapper.setProps({ addonAfter: '' }) - - expect(wrapper.classes()).not.toContain('ix-input-with-addon-after') - expect(wrapper.findAll('.ix-input-addon').length).toBe(1) - - await wrapper.setProps({ addonBefore: '' }) - - expect(wrapper.classes()).not.toContain('ix-input-with-addon-before') - expect(wrapper.findAll('.ix-input-addon').length).toBe(0) - }) - - test('addonAfter and addonBefore slots work', async () => { - const wrapper = InputMount({ - props: { addonAfter: 'addonAfter', addonBefore: 'addonBefore' }, - slots: { addonAfter: 'addonAfter slot', addonBefore: 'addonBefore slot' }, - }) - - expect(wrapper.classes()).toContain('ix-input-with-addon-after') - expect(wrapper.classes()).toContain('ix-input-with-addon-before') - - const addons = wrapper.findAll('.ix-input-addon') - - expect(addons[0].text()).toBe('addonBefore slot') - expect(addons[1].text()).toBe('addonAfter slot') - }) - - test('suffix and prefix work', async () => { - const wrapper = InputMount({ props: { suffix: 'up', prefix: 'down' } }) - - const suffix = wrapper.find('.ix-input-suffix') - const prefix = wrapper.find('.ix-input-prefix') - - expect(suffix.find('.ix-icon-up').exists()).toBe(true) - expect(prefix.find('.ix-icon-down').exists()).toBe(true) - - await wrapper.setProps({ suffix: 'left' }) - - expect(suffix.find('.ix-icon-left').exists()).toBe(true) - - await wrapper.setProps({ prefix: 'right' }) - - expect(prefix.find('.ix-icon-right').exists()).toBe(true) - - await wrapper.setProps({ suffix: '' }) - - expect(wrapper.find('.ix-input-suffix').exists()).toBe(false) - - await wrapper.setProps({ prefix: '' }) - - expect(wrapper.find('.ix-input-prefix').exists()).toBe(false) - }) - - test('suffix and prefix slots work', async () => { - const wrapper = InputMount({ - props: { suffix: 'up', prefix: 'down' }, - slots: { suffix: 'suffix slot', prefix: 'prefix slot' }, - }) - - const suffix = wrapper.find('.ix-input-suffix') - const prefix = wrapper.find('.ix-input-prefix') - - expect(suffix.find('.ix-icon-up').exists()).toBe(false) - expect(prefix.find('.ix-icon-down').exists()).toBe(false) - - expect(suffix.text()).toBe('suffix slot') - expect(prefix.text()).toBe('prefix slot') - }) - - test('size work', async () => { - const wrapper = InputMount({ props: { size: 'lg' } }) - - expect(wrapper.classes()).toContain('ix-input-lg') - - await wrapper.setProps({ size: 'sm' }) - expect(wrapper.classes()).toContain('ix-input-sm') - - await wrapper.setProps({ size: undefined }) - expect(wrapper.classes()).toContain('ix-input-md') - }) - - test('clearable work', async () => { - const onClear = jest.fn() - const wrapper = InputMount({ props: { clearable: true, onClear } }) - - expect(wrapper.find('.ix-icon-close-circle').exists()).toBe(true) - expect(wrapper.find('.ix-input-suffix-hidden').exists()).toBe(true) - - await wrapper.find('.ix-icon-close-circle').trigger('click') - - expect(onClear).toBeCalled() - - await wrapper.setProps({ value: 'value' }) - - expect(wrapper.find('.ix-input-suffix-hidden').exists()).toBe(false) - - await wrapper.setProps({ disabled: true }) - - expect(wrapper.find('.ix-input-suffix-hidden').exists()).toBe(true) - - await wrapper.setProps({ disabled: false, readonly: true }) - - expect(wrapper.find('.ix-input-suffix-hidden').exists()).toBe(true) - - await wrapper.setProps({ clearable: false }) - - expect(wrapper.find('.ix-icon-close-circle').exists()).toBe(false) - }) - - test('borderless work', async () => { - const wrapper = InputMount({ props: { borderless: true } }) - - expect(wrapper.classes()).toContain('ix-input-borderless') - - await wrapper.setProps({ borderless: false }) - - expect(wrapper.classes()).not.toContain('ix-input-borderless') }) }) diff --git a/packages/components/input/index.ts b/packages/components/input/index.ts index fc37c9fca..a65916c3c 100644 --- a/packages/components/input/index.ts +++ b/packages/components/input/index.ts @@ -16,4 +16,4 @@ export { IxInput } export type { InputInstance, InputComponent, InputPublicProps as InputProps } from './src/types' export { commonProps as ɵCommonProps } from './src/types' -export { useCommonBindings as ɵUseCommonBindings } from './src/useCommonBindings' +export { useInput as ɵUseInput } from './src/useInput' diff --git a/packages/components/input/src/Input.tsx b/packages/components/input/src/Input.tsx index 344a437b3..c72aaa084 100644 --- a/packages/components/input/src/Input.tsx +++ b/packages/components/input/src/Input.tsx @@ -5,140 +5,77 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { InputProps } from './types' -import type { Slot, Slots, StyleValue, VNodeTypes } from 'vue' +import type { ɵInputInstance } from '@idux/components/_private/input' -import { computed, defineComponent, inject, normalizeClass } from 'vue' +import { computed, defineComponent, inject, onMounted, ref } from 'vue' +import { ɵInput } from '@idux/components/_private/input' import { useGlobalConfig } from '@idux/components/config' import { FORM_TOKEN } from '@idux/components/form' -import { IxIcon } from '@idux/components/icon' import { inputProps } from './types' -import { useCommonBindings } from './useCommonBindings' +import { useInput } from './useInput' export default defineComponent({ name: 'IxInput', - inheritAttrs: false, props: inputProps, - setup(props, { slots, expose, attrs }) { - const common = useGlobalConfig('common') - const mergedPrefixCls = computed(() => `${common.prefixCls}-input`) + setup(props, { slots, expose }) { const config = useGlobalConfig('input') const formContext = inject(FORM_TOKEN, null) + const size = computed(() => props.size ?? formContext?.size.value ?? config.size) const { elementRef, isDisabled, + clearable, clearIcon, - clearHidden, - isClearable, + clearVisible, isFocused, focus, blur, - handlerInput, - handlerCompositionStart, - handlerCompositionEnd, - handlerFocus, - handlerBlur, - handlerClear, - } = useCommonBindings(props, config) + handleInput, + handleCompositionStart, + handleCompositionEnd, + handleClear, + syncValue, + } = useInput(props, config) expose({ focus, blur }) - const size = computed(() => props.size ?? formContext?.size.value ?? config.size) - const classes = computed(() => { - const { borderless = config.borderless, addonAfter, addonBefore } = props - const prefixCls = mergedPrefixCls.value - const classes = { - [prefixCls]: true, - [`${prefixCls}-borderless`]: borderless, - [`${prefixCls}-disabled`]: isDisabled.value, - [`${prefixCls}-focused`]: isFocused.value, - [`${prefixCls}-${size.value}`]: true, - [`${prefixCls}-with-addon-after`]: addonAfter || slots.addonAfter, - [`${prefixCls}-with-addon-before`]: addonBefore || slots.addonBefore, - } - return normalizeClass([classes, attrs.class]) + const inputRef = ref<ɵInputInstance>() + onMounted(() => { + elementRef.value = inputRef.value!.getInputElement() + syncValue() }) return () => { - const { class: className, style, ...rest } = attrs - const prefixCls = mergedPrefixCls.value + const { addonAfter, addonBefore, borderless, prefix, suffix } = props + return ( - - {renderAddon(slots.addonBefore, props.addonBefore, prefixCls)} - - {renderPrefix(slots.prefix, props.prefix, prefixCls)} - - {renderSuffix(props, slots, isClearable.value, clearIcon.value, clearHidden.value, handlerClear, prefixCls)} - - {renderAddon(slots.addonAfter, props.addonAfter, prefixCls)} - + <ɵInput + v-slots={slots} + ref={inputRef} + addonAfter={addonAfter} + addonBefore={addonBefore} + borderless={borderless} + clearable={clearable.value} + clearIcon={clearIcon.value} + clearVisible={clearVisible.value} + disabled={isDisabled.value} + focused={isFocused.value} + prefix={prefix} + size={size.value} + suffix={suffix} + onClear={handleClear} + readonly={props.readonly} + onInput={handleInput} + onCompositionstart={handleCompositionStart} + onCompositionend={handleCompositionEnd} + > ) } }, }) - -function renderAddon(addonSlot: Slot | undefined, addon: string | undefined, prefixCls: string) { - if (!(addonSlot || addon)) { - return null - } - const child = addonSlot ? addonSlot() : addon - return {child} -} - -function renderPrefix(prefixSlot: Slot | undefined, icon: string | undefined, prefixCls: string) { - if (!(prefixSlot || icon)) { - return null - } - const child = prefixSlot ? prefixSlot() : - return {child} -} - -function renderSuffix( - props: InputProps, - slots: Slots, - isClearable: boolean, - clearIcon: string, - clearHidden: boolean, - onClear: (evt: MouseEvent) => void, - prefixCls: string, -) { - if (!(isClearable || slots.suffix || props.suffix)) { - return null - } - - let classes = `${prefixCls}-suffix` - - if (isClearable && !(slots.suffix || props.suffix)) { - if (clearHidden) { - classes += ` ${prefixCls}-suffix-hidden` - } - const child = slots.clearIcon?.({ onClear }) ?? - return {child} - } - - let child: VNodeTypes - if (isClearable && !clearHidden) { - child = slots.clearIcon?.({ onClear }) ?? - } else { - child = slots.suffix?.() ?? - } - - return {child} -} diff --git a/packages/components/input/src/useCommonBindings.ts b/packages/components/input/src/useInput.ts similarity index 60% rename from packages/components/input/src/useCommonBindings.ts rename to packages/components/input/src/useInput.ts index 3c5bd4b51..4c03b0f50 100644 --- a/packages/components/input/src/useCommonBindings.ts +++ b/packages/components/input/src/useInput.ts @@ -10,44 +10,65 @@ import type { FormAccessor } from '@idux/cdk/forms' import type { InputConfig, TextareaConfig } from '@idux/components/config' import type { ComputedRef, Ref } from 'vue' -import { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue' +import { computed, nextTick, ref, toRaw, watch } from 'vue' import { useValueAccessor } from '@idux/cdk/forms' import { callEmit } from '@idux/cdk/utils' import { useFormItemRegister } from '@idux/components/form' -import { useFormElement } from '@idux/components/utils' +import { useFormFocusMonitor } from '@idux/components/utils' -export interface CommonBindings { +export interface InputContext { elementRef: Ref accessor: FormAccessor - isDisabled: ComputedRef clearIcon: ComputedRef - clearHidden: ComputedRef - isClearable: ComputedRef + clearVisible: ComputedRef + clearable: ComputedRef isFocused: Ref focus: (options?: FocusOptions) => void blur: () => void - handlerInput: (evt: Event) => void - handlerCompositionStart: (evt: CompositionEvent) => void - handlerCompositionEnd: (evt: CompositionEvent) => void - handlerFocus: (evt: FocusEvent) => void - handlerBlur: (evt: FocusEvent) => void - handlerClear: (evt: MouseEvent) => void + handleInput: (evt: Event) => void + handleCompositionStart: (evt: CompositionEvent) => void + handleCompositionEnd: (evt: CompositionEvent) => void + handleFocus: (evt: FocusEvent) => void + handleBlur: (evt: FocusEvent) => void + handleClear: (evt: MouseEvent) => void + syncValue: () => void } -export function useCommonBindings( +export function useInput( props: CommonProps, config: InputConfig | TextareaConfig, -): CommonBindings { - const { elementRef, focus, blur } = useFormElement() +): InputContext { const { accessor, control } = useValueAccessor() useFormItemRegister(control) + const isDisabled = computed(() => accessor.disabled.value) + + const clearable = computed(() => props.clearable ?? config.clearable) + const clearIcon = computed(() => props.clearIcon ?? config.clearIcon) + const clearVisible = computed(() => !isDisabled.value && !props.readonly && !!accessor.valueRef.value) + + const isFocused = ref(false) + const handleFocus = (evt: FocusEvent) => { + isFocused.value = true + callEmit(props.onFocus, evt) + } + const handleBlur = (evt: FocusEvent) => { + isFocused.value = false + callEmit(props.onBlur, evt) + accessor.markAsBlurred() + } + + const { elementRef, focus, blur } = useFormFocusMonitor({ + handleFocus, + handleBlur, + }) + const syncValue = () => { - const element = elementRef.value + const element = elementRef.value! const value = accessor.valueRef.value ?? '' if (element && element.value !== value) { element.value = value @@ -56,15 +77,8 @@ export function useCommonBindings( watch(accessor.valueRef, () => syncValue()) - onMounted(() => syncValue()) - - const isDisabled = computed(() => accessor.disabled.value) - const isClearable = computed(() => props.clearable ?? config.clearable) - const clearIcon = computed(() => props.clearIcon ?? config.clearIcon) - const clearHidden = computed(() => isDisabled.value || props.readonly || !accessor.valueRef.value) - const isComposing = ref(false) - const handlerInput = (evt: Event) => { + const handleInput = (evt: Event) => { callEmit(props.onInput, evt) if (isComposing.value) { return @@ -80,30 +94,19 @@ export function useCommonBindings( } } - const handlerCompositionStart = (evt: CompositionEvent) => { + const handleCompositionStart = (evt: CompositionEvent) => { isComposing.value = true callEmit(props.onCompositionStart, evt) } - const handlerCompositionEnd = (evt: CompositionEvent) => { + const handleCompositionEnd = (evt: CompositionEvent) => { callEmit(props.onCompositionEnd, evt) if (isComposing.value) { isComposing.value = false - handlerInput(evt) + handleInput(evt) } } - const isFocused = ref(false) - const handlerFocus = (evt: FocusEvent) => { - isFocused.value = true - callEmit(props.onFocus, evt) - } - const handlerBlur = (evt: FocusEvent) => { - isFocused.value = false - callEmit(props.onBlur, evt) - accessor.markAsBlurred() - } - - const handlerClear = (evt: MouseEvent) => { + const handleClear = (evt: MouseEvent) => { callEmit(props.onClear, evt) accessor.setValue('') } @@ -112,19 +115,20 @@ export function useCommonBindings( elementRef, accessor, isDisabled, + clearable, clearIcon, - clearHidden, - isClearable, + clearVisible, isFocused, focus, blur, - handlerInput, - handlerCompositionStart, - handlerCompositionEnd, - handlerFocus, - handlerBlur, - handlerClear, + handleInput, + handleCompositionStart, + handleCompositionEnd, + handleFocus, + handleBlur, + handleClear, + syncValue, } } diff --git a/packages/components/input/style/index.less b/packages/components/input/style/index.less deleted file mode 100644 index 05c0b3644..000000000 --- a/packages/components/input/style/index.less +++ /dev/null @@ -1,171 +0,0 @@ -@import '../../style/mixins/borderless.less'; -@import '../../style/mixins/placeholder.less'; -@import '../../style/mixins/reset.less'; -@import './mixin.less'; - -.@{input-prefix} { - .reset-component(); - - display: inline-flex; - width: 100%; - line-height: @input-line-height; - background-color: @input-background-color; - - &-wrapper { - position: relative; - display: inline-flex; - align-items: center; - width: 100%; - border: @input-border-width @input-border-style @input-border-color; - border-radius: @input-border-radius; - transition: all @input-transition-duration @input-transition-function; - - &:hover { - border-color: @input-hover-color; - } - - .@{input-prefix}-inner { - .input-inner(); - } - - .@{input-prefix}-suffix, - .@{input-prefix}-prefix { - display: inline-flex; - text-align: center; - align-items: center; - color: @input-placeholder-color; - transition: color @input-transition-duration; - cursor: pointer; - - &:hover { - color: @input-color-secondary; - } - } - - .@{input-prefix}-prefix { - margin-right: @input-wrapper-inner-margin; - } - - .@{input-prefix}-suffix { - margin-left: @input-wrapper-inner-margin; - - &-hidden { - visibility: hidden; - } - } - } - - &-addon { - display: inline-block; - text-align: center; - background-color: @input-addon-background-color; - border: @input-border-width @input-border-style @input-border-color; - border-radius: @input-border-radius; - white-space: nowrap; - - &:first-child { - border-right: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - - .@{idux-prefix}-select { - - &-selector { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - } - } - - &:last-child { - border-left: 0; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - - .@{idux-prefix}-select { - - &-selector { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } - } - - .@{idux-prefix}-select { - - &-selector { - background-color: inherit; - border: @input-border-width @input-border-style transparent; - box-shadow: none; - } - } - } - - &-with-addon-after { - .@{input-prefix}-wrapper { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - } - - &-with-addon-before { - .@{input-prefix}-wrapper { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } - - &-sm { - .input-size(@input-font-size-sm; @input-padding-vertical-sm; @input-padding-horizontal-sm; @input-height-sm); - } - - &-md { - .input-size(@input-font-size-md; @input-padding-vertical-md; @input-padding-horizontal-md; @input-height-md); - } - - &-lg { - .input-size(@input-font-size-lg; @input-padding-vertical-lg; @input-padding-horizontal-lg; @input-height-lg); - } - - &-focused { - .@{input-prefix}-wrapper { - border-color: @input-active-color; - box-shadow: @input-active-box-shadow; - } - } - - &-disabled { - color: @input-disabled-color; - background-color: @input-disabled-background-color; - cursor: not-allowed; - opacity: 1; - - .@{input-prefix}-wrapper { - - &:hover { - border-color: @input-border-color; - } - - .@{input-prefix}-suffix, - .@{input-prefix}-prefix { - cursor: not-allowed; - - &:hover { - color: @input-disabled-color; - } - } - } - } - - &-borderless { - - &, - &:hover, - &-focused, - &-disabled { - .@{input-prefix}-wrapper { - .borderless(); - } - } - } -} diff --git a/packages/components/input/style/mixin.less b/packages/components/input/style/mixin.less deleted file mode 100644 index 3b9faa4cd..000000000 --- a/packages/components/input/style/mixin.less +++ /dev/null @@ -1,28 +0,0 @@ -.input-size(@font-size; @padding-vertical; @padding-horizontal; @height) { - font-size: @font-size; - .@{input-prefix}-addon, - .@{input-prefix}-wrapper { - padding: @padding-vertical @padding-horizontal; - } - - .@{input-prefix}-addon { - height: @height; - } - - .@{input-prefix}-addon .@{idux-prefix}-select { - margin: -@padding-vertical -@padding-horizontal; - } -} - -.input-inner() { - display: inline-block; - width: 100%; - min-width: 0; - outline: 0; - - &[disabled] { - cursor: not-allowed; - } - - .placeholder(@input-placeholder-color); -} diff --git a/packages/components/input/style/themes/default.less b/packages/components/input/style/themes/default.less index 2d493bb0a..498793af1 100644 --- a/packages/components/input/style/themes/default.less +++ b/packages/components/input/style/themes/default.less @@ -1,4 +1 @@ -@import '../../../style/themes/default.less'; -@import '../../../form/style/themes/default.variable.less'; @import './default.variable.less'; -@import '../index.less'; diff --git a/packages/components/input/style/themes/default.ts b/packages/components/input/style/themes/default.ts index 8aaddc579..53c684bb1 100644 --- a/packages/components/input/style/themes/default.ts +++ b/packages/components/input/style/themes/default.ts @@ -1,5 +1,5 @@ // style dependencies import '@idux/components/style/core/default' -import '@idux/components/icon/style/themes/default' +import '@idux/components/_private/input/style/themes/default' import './default.less' diff --git a/packages/components/input/style/themes/default.variable.less b/packages/components/input/style/themes/default.variable.less index 579e7952e..5be7463c7 100644 --- a/packages/components/input/style/themes/default.variable.less +++ b/packages/components/input/style/themes/default.variable.less @@ -1,36 +1 @@ -@import '../../../form/style/themes/default.variable.less'; - -@input-font-size-sm: @form-font-size-sm; -@input-font-size-md: @form-font-size-md; -@input-font-size-lg: @form-font-size-lg; -@input-line-height: @form-line-height; -@input-height-sm: @form-height-sm; -@input-height-md: @form-height-md; -@input-height-lg: @form-height-lg; -@input-padding-horizontal-sm: @form-padding-horizontal-sm; -@input-padding-horizontal-md: @form-padding-horizontal-md; -@input-padding-horizontal-lg: @form-padding-horizontal-lg; -@input-padding-vertical-sm: @form-padding-vertical-sm; -@input-padding-vertical-md: @form-padding-vertical-md; -@input-padding-vertical-lg: @form-padding-vertical-lg; - -@input-border-width: @form-border-width; -@input-border-style: @form-border-style; -@input-border-color: @form-border-color; -@input-border-radius: @border-radius-md; - -@input-color: @form-color; -@input-color-secondary: @form-color-secondary; -@input-background-color: @form-background-color; -@input-placeholder-color: @form-placeholder-color; -@input-hover-color: @form-hover-color; -@input-active-color: @form-active-color; -@input-active-box-shadow: @form-active-box-shadow; -@input-disabled-color: @form-disabled-color; -@input-disabled-background-color: @form-disabled-background-color; - -@input-transition-duration: @form-transition-duration; -@input-transition-function: @form-transition-function; - -@input-addon-background-color: @background-color-base; -@input-wrapper-inner-margin: @spacing-xs; +@import '../../../style/themes/default.less'; diff --git a/packages/components/style/variable/prefix.less b/packages/components/style/variable/prefix.less index 9cff687b7..d37fa98e8 100644 --- a/packages/components/style/variable/prefix.less +++ b/packages/components/style/variable/prefix.less @@ -89,6 +89,6 @@ // Private @collapse-transition-prefix: ~'@{idux-prefix}-collapse-transition'; @date-panel-prefix: ~'@{idux-prefix}-date-panel'; -@time-panel-prefix: ~'@{idux-prefix}-time-panel'; @mask-prefix: ~'@{idux-prefix}-mask'; @overlay-prefix: ~'@{idux-prefix}-overlay'; +@time-panel-prefix: ~'@{idux-prefix}-time-panel'; diff --git a/packages/components/textarea/src/Textarea.tsx b/packages/components/textarea/src/Textarea.tsx index cced64faa..20bc8e674 100644 --- a/packages/components/textarea/src/Textarea.tsx +++ b/packages/components/textarea/src/Textarea.tsx @@ -10,11 +10,11 @@ import type { FormAccessor } from '@idux/cdk/forms' import type { TextareaConfig } from '@idux/components/config' import type { Ref, Slot, StyleValue } from 'vue' -import { computed, defineComponent, normalizeClass } from 'vue' +import { computed, defineComponent, normalizeClass, onMounted } from 'vue' import { useGlobalConfig } from '@idux/components/config' import { IxIcon } from '@idux/components/icon' -import { ɵUseCommonBindings } from '@idux/components/input' +import { ɵUseInput } from '@idux/components/input' import { textareaProps } from './types' import { useAutoRows } from './useAutoRows' @@ -33,24 +33,29 @@ export default defineComponent({ accessor, isDisabled, + clearable, clearIcon, - clearHidden, - isClearable, + clearVisible, isFocused, focus, blur, - handlerInput, - handlerCompositionStart, - handlerCompositionEnd, - handlerFocus, - handlerBlur, - handlerClear, - } = ɵUseCommonBindings(props, config) + handleInput, + handleCompositionStart, + handleCompositionEnd, + handleFocus, + handleBlur, + handleClear, + syncValue, + } = ɵUseInput(props, config) expose({ focus, blur }) + onMounted(() => { + syncValue() + }) + const classes = computed(() => { const { showCount = config.showCount, size = config.size } = props const prefixCls = mergedPrefixCls.value @@ -89,20 +94,13 @@ export default defineComponent({ style={textareaStyle.value} disabled={isDisabled.value} readonly={props.readonly} - onInput={handlerInput} - onCompositionstart={handlerCompositionStart} - onCompositionend={handlerCompositionEnd} - onFocus={handlerFocus} - onBlur={handlerBlur} + onInput={handleInput} + onCompositionstart={handleCompositionStart} + onCompositionend={handleCompositionEnd} + onFocus={handleFocus} + onBlur={handleBlur} /> - {renderSuffix( - isClearable.value, - slots.clearIcon, - clearIcon.value, - clearHidden.value, - handlerClear, - prefixCls, - )} + {renderSuffix(clearable.value, slots.clearIcon, clearIcon.value, clearVisible.value, handleClear, prefixCls)} ) } @@ -132,7 +130,7 @@ function renderSuffix( isClearable: boolean, clearIconSlot: Slot | undefined, clearIcon: string, - clearHidden: boolean, + clearVisible: boolean, onClear: (evt: MouseEvent) => void, prefixCls: string, ) { @@ -141,7 +139,7 @@ function renderSuffix( } let classes = `${prefixCls}-suffix` - if (clearHidden) { + if (!clearVisible) { classes += ` ${prefixCls}-suffix-hidden` } const children = clearIconSlot?.({ onClear }) ?? diff --git a/packages/components/utils/src/useFormElement.ts b/packages/components/utils/src/useFormElement.ts index 3eb7a1ec3..82e506079 100644 --- a/packages/components/utils/src/useFormElement.ts +++ b/packages/components/utils/src/useFormElement.ts @@ -5,9 +5,11 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { Ref } from 'vue' +import type { Ref, WatchStopHandle } from 'vue' -import { ref } from 'vue' +import { onBeforeUnmount, ref, watch } from 'vue' + +import { useSharedFocusMonitor } from '@idux/cdk/a11y' export interface FormElementContext { elementRef: Ref @@ -26,3 +28,41 @@ export function useFormElement(): FormEleme return { elementRef, focus, blur } } + +export interface FormFocusMonitor { + elementRef: Ref + focus: (options?: FocusOptions) => void + blur: () => void +} + +export function useFormFocusMonitor(options: { + handleFocus: (evt: FocusEvent) => void + handleBlur: (evt: FocusEvent) => void +}): FormFocusMonitor { + const focusMonitor = useSharedFocusMonitor() + const elementRef = ref() + + let watchStopHandle: WatchStopHandle | undefined + + watch(elementRef, (currElement, prevElement) => { + watchStopHandle?.() + focusMonitor.stopMonitoring(prevElement) + + watchStopHandle = watch(focusMonitor.monitor(currElement, false), evt => { + const { origin, event } = evt + if (event) { + origin ? options.handleFocus(event) : options.handleBlur(event) + } + }) + }) + + onBeforeUnmount(() => { + watchStopHandle?.() + focusMonitor.stopMonitoring(elementRef.value) + }) + + const focus = (options?: FocusOptions) => focusMonitor.focusVia(elementRef.value, 'program', options) + const blur = () => focusMonitor.blurVia(elementRef.value) + + return { elementRef, focus, blur } +} diff --git a/scripts/gen/generate.ts b/scripts/gen/generate.ts index fb4dac191..825c15f3d 100644 --- a/scripts/gen/generate.ts +++ b/scripts/gen/generate.ts @@ -184,7 +184,7 @@ class Generate { const themesTemplate = getThemesTemplate(this.isPrivate) const themesIndexTemplate = getThemesIndexTemplate(category) - const lessTemplate = getLessTemplate(`${isPro ? 'pro-' : ''}${kebabCase(name)}`) + const lessTemplate = getLessTemplate(`${isPro ? 'pro-' : ''}${kebabCase(name)}`, this.isPrivate) const indexTemplate = getIndexTemplate(compName) diff --git a/scripts/gen/template.ts b/scripts/gen/template.ts index d25214320..fb40e6153 100644 --- a/scripts/gen/template.ts +++ b/scripts/gen/template.ts @@ -4,8 +4,8 @@ export function getThemesTemplate(isPrivate: boolean): string { ` } -export function getLessTemplate(compName: string): string { - return `@import '../../style/mixins/reset.less'; +export function getLessTemplate(compName: string, isPrivate: boolean): string { + return `@import '${isPrivate ? '../../../' : '../../'}style/mixins/reset.less'; .@{${compName}-prefix} { .reset-component();