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..84c327191
--- /dev/null
+++ b/packages/components/_private/input/style/themes/default.less
@@ -0,0 +1,4 @@
+@import '../../../../style/themes/default.less';
+@import '../../../../form/style/themes/default.variable.less';
+@import './default.variable.less';
+@import '../index.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/input/style/themes/default.variable.less b/packages/components/_private/input/style/themes/default.variable.less
similarity index 93%
rename from packages/components/input/style/themes/default.variable.less
rename to packages/components/_private/input/style/themes/default.variable.less
index 579e7952e..96010f194 100644
--- a/packages/components/input/style/themes/default.variable.less
+++ b/packages/components/_private/input/style/themes/default.variable.less
@@ -1,5 +1,3 @@
-@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;
@@ -17,7 +15,7 @@
@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-border-radius: @border-radius-sm;
@input-color: @form-color;
@input-color-secondary: @form-color-secondary;
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/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..3556905e5 100644
--- a/packages/components/input-number/style/index.less
+++ b/packages/components/input-number/style/index.less
@@ -1,50 +1,57 @@
@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}-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 +63,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..be04a29fb 100644
--- a/packages/components/input-number/style/themes/default.less
+++ b/packages/components/input-number/style/themes/default.less
@@ -1,5 +1,4 @@
@import '../../../style/themes/default.less';
-@import '../../../form/style/themes/default.variable.less';
-@import '../../../input/style/themes/default.variable.less';
+@import '../../../_private/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..a013538d8 100644
--- a/packages/components/input-number/style/themes/default.variable.less
+++ b/packages/components/input-number/style/themes/default.variable.less
@@ -1,7 +1,3 @@
-@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;
-
@input-number-height-sm: @input-height-sm;
@input-number-height-md: @input-height-md;
@input-number-height-lg: @input-height-lg;
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}
+ >ɵInput>
)
}
},
})
-
-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..5be7463c7 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/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();