From f6752899e22c9308c553f05854d6d62020936129 Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Wed, 30 Sep 2020 16:21:04 +0800 Subject: [PATCH] feat: update vittual-list --- antdv-demo | 2 +- components/_util/pickAttrs.js | 69 +++++ components/vc-select2/OptGroup.jsx | 11 + components/vc-select2/Option.jsx | 14 + components/vc-select2/OptionList.jsx | 348 ++++++++++++++++++++++++ components/vc-select2/SelectTrigger.jsx | 144 ++++++++++ components/vc-select2/TransBtn.jsx | 41 +++ components/vc-select2/index.js | 9 + components/vc-virtual-list/List.jsx | 5 +- 9 files changed, 640 insertions(+), 3 deletions(-) create mode 100644 components/_util/pickAttrs.js create mode 100644 components/vc-select2/OptGroup.jsx create mode 100644 components/vc-select2/Option.jsx create mode 100644 components/vc-select2/OptionList.jsx create mode 100644 components/vc-select2/SelectTrigger.jsx create mode 100644 components/vc-select2/TransBtn.jsx create mode 100644 components/vc-select2/index.js diff --git a/antdv-demo b/antdv-demo index 955716e4e..79d49c0ff 160000 --- a/antdv-demo +++ b/antdv-demo @@ -1 +1 @@ -Subproject commit 955716e4e9533bc628c651d6ba6c8d1eb9b21a9d +Subproject commit 79d49c0ff31a4f505ccd5bc3ad238c08f9925212 diff --git a/components/_util/pickAttrs.js b/components/_util/pickAttrs.js new file mode 100644 index 000000000..30a241893 --- /dev/null +++ b/components/_util/pickAttrs.js @@ -0,0 +1,69 @@ +const attributes = `accept acceptCharset accessKey action allowFullScreen allowTransparency + alt async autoComplete autoFocus autoPlay capture cellPadding cellSpacing challenge + charSet checked classID className colSpan cols content contentEditable contextMenu + controls coords crossOrigin data dateTime default defer dir disabled download draggable + encType form formAction formEncType formMethod formNoValidate formTarget frameBorder + headers height hidden high href hrefLang htmlFor httpEquiv icon id inputMode integrity + is keyParams keyType kind label lang list loop low manifest marginHeight marginWidth max maxLength media + mediaGroup method min minLength multiple muted name noValidate nonce open + optimum pattern placeholder poster preload radioGroup readOnly rel required + reversed role rowSpan rows sandbox scope scoped scrolling seamless selected + shape size sizes span spellCheck src srcDoc srcLang srcSet start step style + summary tabIndex target title type useMap value width wmode wrap`; + +const eventsName = `onCopy onCut onPaste onCompositionEnd onCompositionStart onCompositionUpdate onKeyDown + onKeyPress onKeyUp onFocus onBlur onChange onInput onSubmit onClick onContextMenu onDoubleClick + onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown + onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp onSelect onTouchCancel + onTouchEnd onTouchMove onTouchStart onScroll onWheel onAbort onCanPlay onCanPlayThrough + onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata + onLoadStart onPause onPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting onLoad onError`; + +const propList = `${attributes} ${eventsName}`.split(/[\s\n]+/); + +/* eslint-enable max-len */ +const ariaPrefix = 'aria-'; +const dataPrefix = 'data-'; + +function match(key, prefix) { + return key.indexOf(prefix) === 0; +} + +/** + * Picker props from exist props with filter + * @param props Passed props + * @param ariaOnly boolean | { aria?: boolean; data?: boolean; attr?: boolean; } filter config + */ +export default function pickAttrs(props, ariaOnly = false) { + let mergedConfig; + if (ariaOnly === false) { + mergedConfig = { + aria: true, + data: true, + attr: true, + }; + } else if (ariaOnly === true) { + mergedConfig = { + aria: true, + }; + } else { + mergedConfig = { + ...ariaOnly, + }; + } + + const attrs = {}; + Object.keys(props).forEach(key => { + if ( + // Aria + (mergedConfig.aria && (key === 'role' || match(key, ariaPrefix))) || + // Data + (mergedConfig.data && match(key, dataPrefix)) || + // Attr + (mergedConfig.attr && propList.includes(key)) + ) { + attrs[key] = props[key]; + } + }); + return attrs; +} diff --git a/components/vc-select2/OptGroup.jsx b/components/vc-select2/OptGroup.jsx new file mode 100644 index 000000000..1293870d5 --- /dev/null +++ b/components/vc-select2/OptGroup.jsx @@ -0,0 +1,11 @@ +import PropTypes from '../_util/vue-types'; +export default { + props: { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }, + isSelectOptGroup: true, + render() { + return null; + }, +}; diff --git a/components/vc-select2/Option.jsx b/components/vc-select2/Option.jsx new file mode 100644 index 000000000..6704e4c42 --- /dev/null +++ b/components/vc-select2/Option.jsx @@ -0,0 +1,14 @@ +import PropTypes from '../_util/vue-types'; + +export default { + props: { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + disabled: PropTypes.bool, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }, + isSelectOption: true, + render() { + return null; + }, +}; diff --git a/components/vc-select2/OptionList.jsx b/components/vc-select2/OptionList.jsx new file mode 100644 index 000000000..d526affc3 --- /dev/null +++ b/components/vc-select2/OptionList.jsx @@ -0,0 +1,348 @@ +import TransBtn from './TransBtn'; +import PropTypes from '../_util/vue-types'; +import KeyCode from '../_util/KeyCode'; +import classNames from '../_util/classNames'; +import pickAttrs from '../_util/pickAttrs'; +import { isValidElement } from '../_util/props-util'; +import createRef from '../_util/createRef'; +import { computed, reactive, watch } from 'vue'; +import List from '../vc-virtual-list/List'; + +const OptionListProps = { + prefixCls: PropTypes.string, + id: PropTypes.string, + options: PropTypes.array, + flattenOptions: PropTypes.array, + height: PropTypes.number, + itemHeight: PropTypes.number, + values: PropTypes.any, + multiple: PropTypes.bool, + open: PropTypes.bool, + defaultActiveFirstOption: PropTypes.bool, + notFoundContent: PropTypes.any, + menuItemSelectedIcon: PropTypes.any, + childrenAsData: PropTypes.bool, + searchValue: PropTypes.string, + virtual: PropTypes.bool, + + onSelect: PropTypes.func, + onToggleOpen: PropTypes.func, + /** Tell Select that some value is now active to make accessibility work */ + onActiveValue: PropTypes.func, + onScroll: PropTypes.func, + + /** Tell Select that mouse enter the popup to force re-render */ + onMouseenter: PropTypes.func, +}; + +/** + * Using virtual list of option display. + * Will fallback to dom if use customize render. + */ +const OptionList = { + props: OptionListProps, + name: 'OptionList', + inheritAttrs: false, + setup(props) { + const itemPrefixCls = computed(() => `${props.prefixCls}-item`); + + // =========================== List =========================== + const listRef = createRef(); + + const onListMouseDown = event => { + event.preventDefault(); + }; + + const scrollIntoView = index => { + if (listRef.current) { + listRef.current.scrollTo({ index }); + } + }; + + // ========================== Active ========================== + const getEnabledActiveIndex = (index, offset = 1) => { + const len = props.flattenOptions.length; + + for (let i = 0; i < len; i += 1) { + const current = (index + i * offset + len) % len; + + const { group, data } = props.flattenOptions[current]; + if (!group && !data.disabled) { + return current; + } + } + + return -1; + }; + const state = reactive({ + activeIndex: getEnabledActiveIndex(0), + }); + + const setActive = (index, fromKeyboard = false) => { + state.activeIndex = index; + const info = { source: fromKeyboard ? 'keyboard' : 'mouse' }; + + // Trigger active event + const flattenItem = props.flattenOptions[index]; + if (!flattenItem) { + props.onActiveValue(null, -1, info); + return; + } + + props.onActiveValue(flattenItem.data.value, index, info); + }; + + // Auto active first item when list length or searchValue changed + + watch([props.flattenOptions.length, props.searchValue], () => { + setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1); + }); + // Auto scroll to item position in single mode + watch(props.open, () => { + /** + * React will skip `onChange` when component update. + * `setActive` function will call root accessibility state update which makes re-render. + * So we need to delay to let Input component trigger onChange first. + */ + const timeoutId = setTimeout(() => { + if (!props.multiple && props.open && props.values.size === 1) { + const value = Array.from(props.values)[0]; + const index = props.flattenOptions.findIndex(({ data }) => data.value === value); + setActive(index); + scrollIntoView(index); + } + }); + + return () => clearTimeout(timeoutId); + }); + + // ========================== Values ========================== + const onSelectValue = value => { + if (value !== undefined) { + props.onSelect(value, { selected: !props.values.has(value) }); + } + + // Single mode should always close by select + if (!props.multiple) { + props.onToggleOpen(false); + } + }; + + function renderItem(index) { + const item = props.flattenOptions[index]; + if (!item) return null; + + const itemData = item.data || {}; + const { value, label, children } = itemData; + const attrs = pickAttrs(itemData, true); + const mergedLabel = props.childrenAsData ? children : label; + return item ? ( +