feat: update vittual-list

pull/2930/head^2
tanjinzhou 2020-09-30 16:21:04 +08:00
parent b9f9dad4c7
commit f6752899e2
9 changed files with 640 additions and 3 deletions

@ -1 +1 @@
Subproject commit 955716e4e9533bc628c651d6ba6c8d1eb9b21a9d
Subproject commit 79d49c0ff31a4f505ccd5bc3ad238c08f9925212

View File

@ -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;
}

View File

@ -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;
},
};

View File

@ -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;
},
};

View File

@ -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 ? (
<div
aria-label={typeof mergedLabel === 'string' ? mergedLabel : null}
{...attrs}
key={index}
role="option"
id={`${props.id}_list_${index}`}
aria-selected={props.values.has(value)}
>
{value}
</div>
) : null;
}
return {
renderItem,
listRef,
state,
onListMouseDown,
itemPrefixCls,
setActive,
onSelectValue,
onKeydown: event => {
const { which } = event;
switch (which) {
// >>> Arrow keys
case KeyCode.UP:
case KeyCode.DOWN: {
let offset = 0;
if (which === KeyCode.UP) {
offset = -1;
} else if (which === KeyCode.DOWN) {
offset = 1;
}
if (offset !== 0) {
const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
scrollIntoView(nextActiveIndex);
setActive(nextActiveIndex, true);
}
break;
}
// >>> Select
case KeyCode.ENTER: {
// value
const item = props.flattenOptions[state.activeIndex];
if (item && !item.data.disabled) {
onSelectValue(item.data.value);
} else {
onSelectValue(undefined);
}
if (props.open) {
event.preventDefault();
}
break;
}
// >>> Close
case KeyCode.ESC: {
props.onToggleOpen(false);
}
}
},
onKeyup: () => {},
scrollTo: index => {
scrollIntoView(index);
},
};
},
render() {
const { renderItem, listRef, onListMouseDown, itemPrefixCls, setActive, onSelectValue } = this;
const {
id,
childrenAsData,
values,
height,
itemHeight,
flattenOptions,
menuItemSelectedIcon,
notFoundContent,
virtual,
onScroll,
onMouseenter,
} = this.$props;
const { activeIndex } = this.state;
// ========================== Render ==========================
if (flattenOptions.length === 0) {
return (
<div
role="listbox"
id={`${id}_list`}
class={`${itemPrefixCls}-empty`}
onMousedown={onListMouseDown}
>
{notFoundContent}
</div>
);
}
return (
<>
<div role="listbox" id={`${id}_list`} style={{ height: 0, width: 0, overflow: 'hidden' }}>
{renderItem(activeIndex - 1)}
{renderItem(activeIndex)}
{renderItem(activeIndex + 1)}
</div>
<List
itemKey="key"
ref={listRef}
data={flattenOptions}
height={height}
itemHeight={itemHeight}
fullHeight={false}
onMousedown={onListMouseDown}
onScroll={onScroll}
virtual={virtual}
onMouseenter={onMouseenter}
>
{({ group, groupOption, data }, itemIndex) => {
const { label, key } = data;
// Group
if (group) {
return (
<div class={classNames(itemPrefixCls, `${itemPrefixCls}-group`)}>
{label !== undefined ? label : key}
</div>
);
}
const {
disabled,
value,
title,
children,
style,
class: cls,
className,
...otherProps
} = data;
// Option
const selected = values.has(value);
const optionPrefixCls = `${itemPrefixCls}-option`;
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, cls, className, {
[`${optionPrefixCls}-grouped`]: groupOption,
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
[`${optionPrefixCls}-disabled`]: disabled,
[`${optionPrefixCls}-selected`]: selected,
});
const mergedLabel = childrenAsData ? children : label;
const iconVisible =
!menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
const content = mergedLabel || value;
// https://github.com/ant-design/ant-design/issues/26717
let optionTitle =
typeof content === 'string' || typeof content === 'number'
? content.toString()
: undefined;
if (title !== undefined) {
optionTitle = title;
}
return (
<div
{...otherProps}
aria-selected={selected}
class={optionClassName}
title={optionTitle}
onMousemove={() => {
if (activeIndex === itemIndex || disabled) {
return;
}
setActive(itemIndex);
}}
onClick={() => {
if (!disabled) {
onSelectValue(value);
}
}}
style={style}
>
<div class={`${optionPrefixCls}-content`}>{content}</div>
{isValidElement(menuItemSelectedIcon) || selected}
{iconVisible && (
<TransBtn
class={`${itemPrefixCls}-option-state`}
customizeIcon={menuItemSelectedIcon}
customizeIconProps={{ isSelected: selected }}
>
{selected ? '✓' : null}
</TransBtn>
)}
</div>
);
}}
</List>
</>
);
},
};
export default OptionList;

View File

@ -0,0 +1,144 @@
import Trigger from '../vc-trigger';
import PropTypes from '../_util/vue-types';
import { getSlot } from '../_util/props-util';
import classNames from '../_util/classNames';
import createRef from '../_util/createRef';
const getBuiltInPlacements = dropdownMatchSelectWidth => {
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
const adjustX = typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
return {
bottomLeft: {
points: ['tl', 'bl'],
offset: [0, 4],
overflow: {
adjustX,
adjustY: 1,
},
},
bottomRight: {
points: ['tr', 'br'],
offset: [0, 4],
overflow: {
adjustX,
adjustY: 1,
},
},
topLeft: {
points: ['bl', 'tl'],
offset: [0, -4],
overflow: {
adjustX,
adjustY: 1,
},
},
topRight: {
points: ['br', 'tr'],
offset: [0, -4],
overflow: {
adjustX,
adjustY: 1,
},
},
};
};
export default {
name: 'SelectTrigger',
inheritAttrs: false,
props: {
// onPopupFocus: PropTypes.func,
// onPopupScroll: PropTypes.func,
dropdownAlign: PropTypes.object,
visible: PropTypes.bool,
disabled: PropTypes.bool,
dropdownClassName: PropTypes.string,
dropdownStyle: PropTypes.object,
empty: PropTypes.bool,
prefixCls: PropTypes.string,
popupClassName: PropTypes.string,
// children: PropTypes.any,
animation: PropTypes.string,
transitionName: PropTypes.string,
getPopupContainer: PropTypes.func,
dropdownRender: PropTypes.func,
containerWidth: PropTypes.number,
dropdownMatchSelectWidth: PropTypes.oneOfType([Number, Boolean]).def(true),
popupElement: PropTypes.any,
direction: PropTypes.string,
getTriggerDOMNode: PropTypes.func,
},
created() {
this.popupRef = createRef();
},
methods: {
getDropdownTransitionName() {
const props = this.$props;
let transitionName = props.transitionName;
if (!transitionName && props.animation) {
transitionName = `${this.getDropdownPrefixCls()}-${props.animation}`;
}
return transitionName;
},
getPopupElement() {
return this.popupRef.current;
},
},
render() {
const { empty, ...props } = { ...this.$props, ...this.$attrs };
const {
visible,
dropdownAlign,
prefixCls,
popupElement,
dropdownClassName,
dropdownStyle,
dropdownMatchSelectWidth,
containerWidth,
dropdownRender,
} = props;
const dropdownPrefixCls = `${prefixCls}-dropdown`;
let popupNode = popupElement;
if (dropdownRender) {
popupNode = dropdownRender({ menuNode: popupElement, props });
}
const builtInPlacements = getBuiltInPlacements(dropdownMatchSelectWidth);
const popupStyle = { minWidth: containerWidth, ...dropdownStyle };
if (typeof dropdownMatchSelectWidth === 'number') {
popupStyle.width = `${dropdownMatchSelectWidth}px`;
} else if (dropdownMatchSelectWidth) {
popupStyle.width = `${containerWidth}px`;
}
return (
<Trigger
{...props}
showAction={[]}
hideAction={[]}
popupPlacement={this.direction === 'rtl' ? 'bottomRight' : 'bottomLeft'}
popupPlacement="bottomLeft"
builtinPlacements={builtInPlacements}
prefixCls={dropdownPrefixCls}
popupTransitionName={this.getDropdownTransitionName()}
onPopupVisibleChange={props.onDropdownVisibleChange}
popup={<div ref={this.popupRef}>{popupNode}</div>}
popupAlign={dropdownAlign}
popupVisible={visible}
getPopupContainer={props.getPopupContainer}
popupClassName={classNames(dropdownClassName, {
[`${dropdownPrefixCls}-empty`]: empty,
})}
popupStyle={popupStyle}
getTriggerDOMNode={this.getTriggerDOMNode}
>
{getSlot(this)[0]}
</Trigger>
);
},
};

View File

@ -0,0 +1,41 @@
const TransBtn = (
_,
{ attrs: { class: className, customizeIcon, customizeIconProps, onMousedown, onClick }, slots },
) => {
let icon;
if (typeof customizeIcon === 'function') {
icon = customizeIcon(customizeIconProps);
} else {
icon = customizeIcon;
}
return (
<span
class={className}
onMousedown={event => {
event.preventDefault();
if (onMousedown) {
onMousedown(event);
}
}}
style={{
userSelect: 'none',
WebkitUserSelect: 'none',
}}
unselectable="on"
onClick={onClick}
aria-hidden
>
{icon !== undefined ? (
icon
) : (
<span class={className.split(/\s+/).map(cls => `${cls}-icon`)}>{slots?.default()}</span>
)}
</span>
);
};
TransBtn.inheritAttrs = false;
export default TransBtn;

View File

@ -0,0 +1,9 @@
// based on vc-select 9.2.2
import Select from './Select';
import Option from './Option';
import { SelectPropTypes } from './PropTypes';
import OptGroup from './OptGroup';
Select.Option = Option;
Select.OptGroup = OptGroup;
export { Select, Option, OptGroup, SelectPropTypes };
export default Select;

View File

@ -282,7 +282,6 @@ const List = {
};
},
render() {
const { style, class: className } = this.$attrs;
const {
prefixCls = 'rc-virtual-list',
height,
@ -295,8 +294,10 @@ const List = {
component: Component = 'div',
onScroll,
children,
style,
class: className,
...restProps
} = this.$props;
} = { ...this.$props, ...this.$attrs };
const mergedClassName = classNames(prefixCls, className);
const { scrollTop, mergedData } = this.state;
const { scrollHeight, offset, start, end } = this.calRes;