Skip to content

Commit

Permalink
add KeyboardEvents to select; fix table some bugs (#2683)
Browse files Browse the repository at this point in the history
* fix(table): virtual scroll default scroll bar height

* feat(checkbox): support Space checked

* feat(radio): support Space Keyboard to check

* fix(table): e.target.closest might not exist

* test: update snapshots

* feat(select): support keyboard selection

* feat(select): support keyboard

* feat(input): showInput with 0px width

* fix: remove unsued code

* test: update snapshots

* fix(table): drag sort invalid on lazyload

* feat: update common

* feat: update common
  • Loading branch information
chaishi committed Aug 15, 2023
1 parent 24b6ccc commit 87ea127
Show file tree
Hide file tree
Showing 20 changed files with 466 additions and 121 deletions.
6 changes: 5 additions & 1 deletion src/checkbox/hooks/useKeyboardEvent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export const CHECKED_CODE_REG = /(enter|space)/i;

export function useKeyboardEvent(handleChange: (e: Event) => void) {
const keyboardEventListener = (e: KeyboardEvent) => {
if (e.code === 'Enter') {
const isCheckedCode = CHECKED_CODE_REG.test(e.key) || CHECKED_CODE_REG.test(e.code);
if (isCheckedCode) {
e.preventDefault();
const { disabled } = (e.currentTarget as HTMLElement).querySelector('input');
!disabled && handleChange(e);
}
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useElementLazyRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function useElementLazyRender(labelRef: Ref<HTMLElement>, lazyLoad: Ref<b
const showElement = ref(true);

const handleLazyLoad = () => {
if (!lazyLoad.value) return;
if (!lazyLoad.value || !labelRef.value || ioObserver.value) return;
showElement.value = false;
const io = observe(
labelRef.value,
Expand All @@ -23,11 +23,11 @@ export function useElementLazyRender(labelRef: Ref<HTMLElement>, lazyLoad: Ref<b

onMounted(handleLazyLoad);

lazyLoad.value && watch([lazyLoad], handleLazyLoad);
lazyLoad.value && watch([lazyLoad, labelRef], handleLazyLoad);

onBeforeUnmount(() => {
if (!lazyLoad.value) return;
ioObserver.value?.unobserve(labelRef.value);
ioObserver.value?.unobserve?.(labelRef.value);
});

return {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useVirtualScrollNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const useVirtualScroll = (container: Ref<HTMLElement>, params: UseVirtualScrollP
/** 注意测试:数据长度为空;数据长度小于表格高度等情况。即期望只有数据量达到一定程度才允许开启虚拟滚动 */
const visibleData = ref<any[]>([]);
// 用于显示表格列
const translateY = ref(0);
const translateY = ref((params.value.data?.length || 0) * (params.value.scroll?.rowHeight || 50));
// 滚动高度,用于显示滚动条
const scrollHeight = ref(0);
const trScrollTopHeightList = ref<number[]>([]);
Expand Down
21 changes: 10 additions & 11 deletions src/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -525,17 +525,16 @@ export default mixins(
<span class={[`${this.componentName}__prefix`, `${this.componentName}__prefix-icon`]}>{prefixIcon}</span>
) : null}
{labelContent}
{this.showInput && (
<input
attrs={this.inputAttrs}
on={inputEvents}
ref="inputRef"
class={`${this.componentName}__inner`}
value={inputTextValue}
onInput={this.handleInput}
title={this.disabled ? inputTextValue : undefined}
/>
)}
{/* input element must exist, or other select components can not focus by keyboard operation */}
<input
attrs={this.inputAttrs}
on={inputEvents}
ref="inputRef"
class={[`${this.componentName}__inner`, { [`${this.componentName}--soft-hidden`]: !this.showInput }]}
value={inputTextValue}
onInput={this.handleInput}
title={this.disabled ? inputTextValue : undefined}
/>
{this.autoWidth && (
<span ref="inputPreRef" class={`${this.classPrefix}-input__input-pre`}>
{this.preValue || this.tPlaceholder}
Expand Down
5 changes: 4 additions & 1 deletion src/radio/group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { emitEvent } from '../utils/event';
import { getClassPrefixMixins } from '../config-provider/config-receiver';
import mixins from '../utils/mixins';
import { off, on } from '../utils/dom';
import { CHECKED_CODE_REG } from '../checkbox/hooks/useKeyboardEvent';

const classPrefixMixins = getClassPrefixMixins('radio-group');

Expand Down Expand Up @@ -113,7 +114,9 @@ export default mixins(classPrefixMixins).extend({

// 注意:此处会还原区分 数字 和 数字字符串
checkRadioInGroup(e: KeyboardEvent) {
if (/enter/i.test(e.key) || /enter/i.test(e.code)) {
const isCheckedCode = CHECKED_CODE_REG.test(e.key) || CHECKED_CODE_REG.test(e.code);
if (isCheckedCode) {
e.preventDefault();
const inputNode = (e.target as HTMLElement).querySelector('input');
const data = inputNode.dataset;
if (inputNode.checked && data.allowUncheck) {
Expand Down
4 changes: 4 additions & 0 deletions src/select-input/_example/single.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@popup-visible-change="onPopupVisibleChange"
@clear="onClear"
@input-change="onInputChange"
@focus="onFocus"
>
<template #panel>
<ul class="tdesign-demo__select-input-ul-single">
Expand Down Expand Up @@ -61,6 +62,9 @@ export default {
// 过滤功能
console.log(val, context);
},
onFocus(val, context) {
console.log('focus:', val, context);
},
},
};
</script>
Expand Down
49 changes: 45 additions & 4 deletions src/select-input/select-input.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import {
computed, defineComponent, ref, SetupContext, toRefs,
computed,
defineComponent,
onBeforeUnmount,
onMounted,
ref,
SetupContext,
toRefs,
watch,
} from '@vue/composition-api';
import Popup, { PopupVisibleChangeContext } from '../popup';
import props from './props';
Expand Down Expand Up @@ -29,20 +36,27 @@ export default defineComponent({
} = toRefs(props);

const {
commonInputProps, singleInputValue, onInnerClear, renderSelectSingle,
} = useSingle(props, context);
const { multipleInputValue, renderSelectMultiple } = useMultiple(props, context);
isSingleFocus, commonInputProps, singleInputValue, onInnerClear, renderSelectSingle,
} = useSingle(
props,
context,
);
const { multipleInputValue, isMultipleFocus, renderSelectMultiple } = useMultiple(props, context);
const { tOverlayInnerStyle, innerPopupVisible, onInnerPopupVisibleChange } = useOverlayInnerStyle(props, {
afterHidePopup: onInnerBlur,
});

const isFocus = computed(() => (props.multiple ? isMultipleFocus.value : isSingleFocus.value));

// SelectInput.blur is not equal to Input or TagInput, example: click popup panel.
// if trigger blur on click popup panel, filter data of tree select can not be checked.
function onInnerBlur(ctx: PopupVisibleChangeContext) {
const inputValue = props.multiple ? multipleInputValue.value : singleInputValue.value;
const params: Parameters<TdSelectInputProps['onBlur']>[1] = { e: ctx.e, inputValue };
props.onBlur?.(props.value, params);
context.emit('blur', props.value, params);
isSingleFocus.value = false;
isMultipleFocus.value = false;
}

const classes = computed(() => [
Expand All @@ -55,6 +69,33 @@ export default defineComponent({
},
]);

const addKeyboardEventListener = (e: KeyboardEvent) => {
if (/(ArrowDown|ArrowUp)/.test(e.code || e.key)) {
const ctx: PopupVisibleChangeContext = { ...context, trigger: 'trigger-element-focus' };
props.onPopupVisibleChange?.(true, ctx);
context.emit('popup-visible-change', true, ctx);
}
};

watch([isFocus], ([isFocus]) => {
if (popupVisible.value) return;
if (isFocus) {
selectInputRef.value.addEventListener('keydown', addKeyboardEventListener);
} else {
selectInputRef.value.removeEventListener('keydown', addKeyboardEventListener);
}
});

onMounted(() => {
if (!popupVisible.value && isFocus) {
selectInputRef.value.addEventListener('keydown', addKeyboardEventListener);
}
});

onBeforeUnmount(() => {
selectInputRef.value.removeEventListener('keydown', addKeyboardEventListener);
});

return {
selectInputRef,
innerPopupVisible,
Expand Down
51 changes: 30 additions & 21 deletions src/select-input/useMultiple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import isObject from 'lodash/isObject';
import Vue from 'vue';
import { TdSelectInputProps, SelectInputKeys } from './type';
import { SelectInputCommonProperties } from './interface';
import { InputValue } from '../input';
import TagInput, { TagInputValue, InputValueChangeContext, TagInputProps } from '../tag-input';
import TagInput, { TagInputValue, TagInputProps } from '../tag-input';
import Loading from '../loading';
import useDefaultValue from '../hooks/useDefaultValue';
import { usePrefixClass } from '../hooks/useConfig';
Expand All @@ -27,6 +26,7 @@ export default function useMultiple(props: TdSelectInputProps, context: SetupCon
const { inputValue } = toRefs(props);
const classPrefix = usePrefixClass();
const tagInputRef = ref();
const isMultipleFocus = ref(props.autofocus);
const [tInputValue, setTInputValue] = useDefaultValue(
inputValue,
props.defaultInputValue,
Expand All @@ -52,6 +52,30 @@ export default function useMultiple(props: TdSelectInputProps, context: SetupCon
props.onTagChange?.(val, ctx);
context.emit('tag-change', val, ctx);
};

/**
* 筛选器统一特性:
* 1. 筛选器按下回车时不清空输入框;
* 2. SelectInput 的失焦不等于 TagInput。如点击下拉面板时,TagInput 失去焦点,但 SelectInput 依旧保持聚焦,允许继续选择。
*/
const onInputChange: TagInputProps['onInputChange'] = (val, ctx) => {
if (ctx.trigger === 'enter' || ctx.trigger === 'blur') return;
setTInputValue(val, { trigger: ctx.trigger, e: ctx.e });
};

const onFocus: TagInputProps['onFocus'] = (val, ctx) => {
isMultipleFocus.value = true;
const params = { ...ctx, tagInputValue: val };
props.onFocus?.(props.value, params);
context.emit('focus', props.value, params);
};

const onEnter: TagInputProps['onEnter'] = (val, ctx) => {
const params = { ...ctx, tagInputValue: val };
props.onEnter?.(props.value, params);
context.emit('focus', props.value, params);
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const renderSelectMultiple = (p: RenderSelectMultipleParams, h: Vue.CreateElement) => {
const tagInputProps = {
Expand Down Expand Up @@ -88,29 +112,13 @@ export default function useMultiple(props: TdSelectInputProps, context: SetupCon
scopedSlots={slots}
props={tagInputProps}
on={{
'input-change': (val: InputValue, ctx: InputValueChangeContext) => {
/**
* 筛选器统一特性:
* 1. 筛选器按下回车时不清空输入框;
* 2. SelectInput 的失焦不等于 TagInput。如点击下拉面板时,TagInput 失去焦点,但 SelectInput 依旧保持聚焦,允许继续选择。
*/
if (ctx.trigger === 'enter' || ctx.trigger === 'blur') return;
setTInputValue(val, { trigger: ctx.trigger, e: ctx.e });
},
'input-change': onInputChange,
...newListeners,
change: onTagInputChange,
clear: p.onInnerClear,
// [Important Info]: SelectInput.blur is not equal to TagInput, example: click popup panel
focus: (val: TagInputValue, ctx: { inputValue: InputValue; e: FocusEvent }) => {
const params = { ...ctx, tagInputValue: val };
props.onFocus?.(props.value, params);
context.emit('focus', props.value, params);
},
enter: (val: TagInputValue, ctx: { e: KeyboardEvent; inputValue: InputValue }) => {
const params = { ...ctx, tagInputValue: val };
props.onEnter?.(props.value, params);
context.emit('focus', props.value, params);
},
focus: onFocus,
enter: onEnter,
}}
/>
);
Expand All @@ -121,6 +129,7 @@ export default function useMultiple(props: TdSelectInputProps, context: SetupCon
tPlaceholder,
tagInputRef,
multipleInputValue: tInputValue,
isMultipleFocus,
renderSelectMultiple,
};
}
60 changes: 34 additions & 26 deletions src/select-input/useSingle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import isObject from 'lodash/isObject';
import pick from 'lodash/pick';
import { SelectInputCommonProperties } from './interface';
import { TdSelectInputProps } from './type';

import Input, { InputValue } from '../input';
import Input, { InputProps, InputValue } from '../input';
import Loading from '../loading';

import { useTNodeJSX } from '../hooks/tnode';
import { usePrefixClass } from '../hooks/useConfig';
import useDefaultValue from '../hooks/useDefaultValue';
Expand Down Expand Up @@ -39,7 +37,6 @@ function getInputValue(value: TdSelectInputProps['value'], keys: TdSelectInputPr

export default function useSingle(props: TdSelectInputProps, context: SetupContext) {
const instance = getCurrentInstance();

const { value, keys, inputValue: propsInputValue } = toRefs(props);
const classPrefix = usePrefixClass();

Expand All @@ -51,6 +48,7 @@ export default function useSingle(props: TdSelectInputProps, context: SetupConte
'input-change',
);

const isSingleFocus = ref(props.autofocus);
const inputRef = ref();
const renderTNode = useTNodeJSX();

Expand All @@ -69,6 +67,32 @@ export default function useSingle(props: TdSelectInputProps, context: SetupConte
}
};

const onEnter: InputProps['onEnter'] = (val, context) => {
props.onEnter?.(value.value, { ...context, inputValue: val });
instance.emit('enter', value.value, { ...context, inputValue: val });
};

const onFocus: InputProps['onFocus'] = (val, context) => {
isSingleFocus.value = true;
props.onFocus?.(value.value, { ...context, inputValue: val });
instance.emit('focus', value.value, { ...context, tagInputValue: val });
};

const onPaste: InputProps['onPaste'] = (context) => {
props.onPaste?.(context);
instance.emit('paste', context);
};

const onMouseenter: InputProps['onMouseenter'] = (context) => {
props.onMouseenter?.(context);
instance.emit('mouseenter', context);
};

const onMouseleave: InputProps['onMouseleave'] = (context: { e: MouseEvent }) => {
props.onMouseleave?.(context);
instance.emit('mouseenter', context);
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const renderSelectSingle = (h: Vue.CreateElement, popupVisible: boolean) => {
const singleValueDisplay = renderTNode('valueDisplay');
Expand Down Expand Up @@ -100,29 +124,12 @@ export default function useSingle(props: TdSelectInputProps, context: SetupConte
scopedSlots={slots}
onChange={onInnerInputChange}
onClear={onInnerClear}
onEnter={(val: InputValue, context: { e: KeyboardEvent }) => {
props.onEnter?.(value.value, { ...context, inputValue: val });
instance.emit('enter', value.value, { ...context, inputValue: val });
}}
onEnter={onEnter}
// [Important Info]: SelectInput.blur is not equal to Input, example: click popup panel
onFocus={(val: InputValue, context: { e: MouseEvent }) => {
props.onFocus?.(value.value, { ...context, inputValue: val });
instance.emit('focus', value.value, { ...context, tagInputValue: val });
// TO Discuss: focus might not need to change input value. it will caught some curious errors in tree-select
// !popupVisible && setInputValue(getInputValue(value.value, keys.value), { ...context, trigger: 'focus' }); // 聚焦时拿到value
}}
onPaste={(context: { e: ClipboardEvent; pasteValue: string }) => {
props.onPaste?.(context);
instance.emit('paste', context);
}}
onMouseenter={(context: { e: MouseEvent }) => {
props.onMouseenter?.(context);
instance.emit('mouseenter', context);
}}
onMouseleave={(context: { e: MouseEvent }) => {
props.onMouseleave?.(context);
instance.emit('mouseenter', context);
}}
onFocus={onFocus}
onPaste={onPaste}
onMouseenter={onMouseenter}
onMouseleave={onMouseleave}
/>
);
};
Expand All @@ -131,6 +138,7 @@ export default function useSingle(props: TdSelectInputProps, context: SetupConte
inputRef,
commonInputProps,
singleInputValue: inputValue,
isSingleFocus,
onInnerClear,
renderSelectSingle,
};
Expand Down
Loading

0 comments on commit 87ea127

Please sign in to comment.