Skip to content

Commit

Permalink
fix(Select): optimize keyboard control behavior in virtual scroll mode (
Browse files Browse the repository at this point in the history
#3542)

* fix(Select): optimize keyboard control behavior in virtual scroll mode

* chore: fix if

* chore: update snapshot
  • Loading branch information
uyarn authored Oct 31, 2023
1 parent 07ca038 commit dd83124
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 102 deletions.
1 change: 1 addition & 0 deletions src/select/_example/virtual-scroll.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<t-select
v-model="value"
:options="options"
filterable
placeholder="请选择"
style="width: 300px"
:scroll="{ type: 'virtual' }"
Expand Down
144 changes: 144 additions & 0 deletions src/select/hooks/useKeyboardControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { ref, watch, ComputedRef, Ref } from 'vue';
import { usePrefixClass } from '../../hooks/useConfig';

import { getNewMultipleValue } from '../helper';

import type { SelectOption, TdOptionProps, SelectValue } from '../type';
import type { ChangeHandler } from '../../hooks/useVModel';
import type { PopupVisibleChangeContext } from '../../popup';

export type useKeyboardControlType = {
displayOptions: ComputedRef<SelectOption[]>;
optionsList: ComputedRef<TdOptionProps[]>;
innerPopupVisible: Ref<boolean>;
setInnerPopupVisible: ChangeHandler<boolean, [context: PopupVisibleChangeContext]>;
selectPanelRef: Ref<{ isVirtual: boolean; innerRef: HTMLDivElement }>;
isFilterable: ComputedRef<boolean>;
getSelectedOptions: (selectValue?: SelectValue[] | SelectValue) => TdOptionProps[];
setInnerValue: Function;
innerValue: Ref<SelectValue[]>;
popupContentRef: ComputedRef<HTMLElement>;
multiple: boolean;
max: number;
};

// 统一处理键盘控制的hooks
export default function useKeyboardControl({
displayOptions,
optionsList,
innerPopupVisible,
setInnerPopupVisible,
selectPanelRef,
isFilterable,
getSelectedOptions,
setInnerValue,
innerValue,
popupContentRef,
multiple,
max,
}: useKeyboardControlType) {
const hoverIndex = ref(-1);
const virtualFilteredOptions = ref([]); // 处理虚拟滚动下选项过滤通过键盘选择的问题
const classPrefix = usePrefixClass();

const handleKeyDown = (e: KeyboardEvent) => {
const optionsListLength = displayOptions.value.length;
let newIndex = hoverIndex.value;
switch (e.code) {
case 'ArrowUp':
e.preventDefault();
if (hoverIndex.value === -1) {
newIndex = 0;
} else if (hoverIndex.value === 0 || hoverIndex.value > displayOptions.value.length - 1) {
newIndex = optionsListLength - 1;
} else {
newIndex--;
}
if (optionsList.value[newIndex]?.disabled) {
newIndex--;
}
hoverIndex.value = newIndex;
break;
case 'ArrowDown':
e.preventDefault();
if (hoverIndex.value === -1 || hoverIndex.value >= optionsListLength - 1) {
newIndex = 0;
} else {
newIndex++;
}
if (optionsList.value[newIndex]?.disabled) {
newIndex++;
}
hoverIndex.value = newIndex;
break;
case 'Enter':
if (hoverIndex.value === -1) break;
if (!innerPopupVisible.value) {
setInnerPopupVisible(true, { e });
break;
}
const filteredOptions =
selectPanelRef.value.isVirtual && isFilterable.value && virtualFilteredOptions.value.length
? virtualFilteredOptions.value
: optionsList.value;

if (!multiple) {
const selectedOptions = getSelectedOptions(filteredOptions[hoverIndex.value].value);
setInnerValue(filteredOptions[hoverIndex.value].value, {
option: selectedOptions?.[0],
selectedOptions: getSelectedOptions(filteredOptions[hoverIndex.value].value),
trigger: 'check',
e,
});
setInnerPopupVisible(false, { e });
} else {
if (hoverIndex.value === -1) return;
const optionValue = filteredOptions[hoverIndex.value]?.value;

if (!optionValue) return;
const newValue = getNewMultipleValue(innerValue.value, optionValue);

if (newValue.value.length > max) return; // 如果已选达到最大值 则不处理
const selectedOptions = getSelectedOptions(newValue.value);
setInnerValue(newValue.value, {
option: selectedOptions.find((v) => v.value == optionValue),
selectedOptions,
trigger: newValue.isCheck ? 'check' : 'uncheck',
e,
});
}
break;
case 'Escape':
setInnerPopupVisible(false, { e });
break;
}
};

watch(innerPopupVisible, (value) => {
if (value) {
// 展开重新恢复初始值
hoverIndex.value = -1;
virtualFilteredOptions.value = [];
}
});

// 处理键盘操作滚动 超出视图时继续自动滚动到键盘所在元素
watch(hoverIndex, (index) => {
const optionHeight = selectPanelRef.value?.innerRef?.querySelector(
`.${classPrefix.value}-select-option`,
).clientHeight;

const scrollHeight = optionHeight * index;

popupContentRef.value.scrollTo({
top: scrollHeight,
behavior: 'smooth',
});
});

return {
hoverIndex,
handleKeyDown,
virtualFilteredOptions,
};
}
8 changes: 3 additions & 5 deletions src/select/select-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { computed, defineComponent, inject, PropType, Slots, ref } from 'vue';
import { computed, defineComponent, inject, Slots, ref } from 'vue';
import omit from 'lodash/omit';
import { Styles } from '../common';

Expand All @@ -25,10 +25,6 @@ export default defineComponent({
multiple: TdSelectProps.multiple,
filterable: TdSelectProps.filterable,
filter: TdSelectProps.filter,
options: {
type: Array as PropType<SelectOption[]>,
default: (): SelectOption[] => [],
},
scroll: TdSelectProps.scroll,
size: TdSelectProps.size,
},
Expand Down Expand Up @@ -110,6 +106,8 @@ export default defineComponent({

expose({
innerRef,
visibleData,
isVirtual,
});

const renderPanel = (options: SelectOption[], extraStyle?: Styles) => (
Expand Down
Loading

0 comments on commit dd83124

Please sign in to comment.