@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
394 lines • 12.5 kB
JavaScript
import React, { PureComponent } from 'react';
import cls from 'classnames';
import PropTypes from 'prop-types';
import { cssClasses, strings } from '@douyinfe/semi-foundation/lib/es/cascader/constants';
import isEnterPress from '@douyinfe/semi-foundation/lib/es/utils/isEnterPress';
import ConfigContext from '../configProvider/context';
import LocaleConsumer from '../locale/localeConsumer';
import { IconChevronRight, IconTick } from '@douyinfe/semi-icons';
import Spin from '../spin';
import Checkbox from '../checkbox';
import { FixedSizeList as List } from 'react-window';
import VirtualRow from './virtualRow';
const prefixcls = cssClasses.PREFIX_OPTION;
export default class Item extends PureComponent {
constructor() {
var _this;
super(...arguments);
_this = this;
this.onClick = (e, item) => {
const {
onItemClick
} = this.props;
if (item.data.disabled || 'disabled' in item && item.disabled) {
return;
}
onItemClick(e, item);
};
/**
* A11y: simulate item click
*/
this.handleItemEnterPress = (keyboardEvent, item) => {
if (isEnterPress(keyboardEvent)) {
this.onClick(keyboardEvent, item);
}
};
this.onHover = (e, item) => {
const {
showNext,
onItemHover
} = this.props;
if (item.data.disabled) {
return;
}
if (showNext === strings.SHOW_NEXT_BY_HOVER) {
onItemHover(e, item);
}
};
this.onCheckboxChange = (e, item) => {
const {
onItemCheckboxClick
} = this.props;
// Prevent Checkbox's click event bubbling to trigger the li click event
e.stopPropagation();
if (e.nativeEvent && typeof e.nativeEvent.stopImmediatePropagation === 'function') {
e.nativeEvent.stopImmediatePropagation();
}
onItemCheckboxClick(item);
};
this.getItemStatus = key => {
const {
activeKeys,
selectedKeys,
loadedKeys,
loadingKeys
} = this.props;
const state = {
active: false,
selected: false,
loading: false
};
if (activeKeys.has(key)) {
state.active = true;
}
if (selectedKeys.has(key)) {
state.selected = true;
}
if (loadingKeys.has(key) && !loadedKeys.has(key)) {
state.loading = true;
}
return state;
};
this.renderIcon = function (type) {
let haveMarginLeft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
const finalCls = style => {
return style + (haveMarginLeft ? ` ${prefixcls}-icon-left` : '');
};
switch (type) {
case 'child':
const {
expandIcon
} = _this.props;
if (expandIcon) {
return expandIcon;
}
return /*#__PURE__*/React.createElement(IconChevronRight, {
className: finalCls(`${prefixcls}-icon ${prefixcls}-icon-expand`)
});
case 'tick':
return /*#__PURE__*/React.createElement(IconTick, {
className: finalCls(`${prefixcls}-icon ${prefixcls}-icon-active`)
});
case 'loading':
return /*#__PURE__*/React.createElement(Spin, {
wrapperClassName: finalCls(`${prefixcls}-spin-icon`)
});
case 'empty':
return /*#__PURE__*/React.createElement("span", {
"aria-hidden": true,
className: finalCls(`${prefixcls}-icon ${prefixcls}-icon-empty`)
});
default:
return null;
}
};
this.highlight = searchText => {
const content = [];
const {
keyword,
separator
} = this.props;
searchText.forEach((item, idx) => {
if (typeof item === 'string' && keyword) {
// Keep historical DOM structure (span) and only make match case-insensitive.
const lowerItem = item.toLowerCase();
const lowerKeyword = keyword.toLowerCase();
let searchFrom = 0;
let keyIndex = 0;
while (true) {
const matchIndex = lowerItem.indexOf(lowerKeyword, searchFrom);
if (matchIndex === -1) {
const rest = item.slice(searchFrom);
if (rest) {
content.push(rest);
}
break;
}
const before = item.slice(searchFrom, matchIndex);
if (before) {
content.push(before);
}
content.push(/*#__PURE__*/React.createElement("span", {
className: `${prefixcls}-label-highlight`,
key: `${idx}-${matchIndex}-${keyIndex}`
}, item.slice(matchIndex, matchIndex + keyword.length)));
searchFrom = matchIndex + keyword.length;
keyIndex++;
}
} else {
content.push(item);
}
if (idx !== searchText.length - 1) {
content.push(separator);
}
});
return content;
};
this.renderFlattenOptionItem = (data, index, style) => {
var _a;
const {
multiple,
selectedKeys,
checkedKeys,
halfCheckedKeys,
keyword,
filterRender,
virtualize
} = this.props;
const {
searchText,
key,
disabled,
pathData
} = data;
const selected = selectedKeys.has(key);
const className = cls(prefixcls, {
[`${prefixcls}-flatten`]: true && !filterRender,
[`${prefixcls}-disabled`]: disabled,
[`${prefixcls}-select`]: selected && !multiple
});
const onClick = e => {
this.onClick(e, data);
};
const onKeyPress = e => this.handleItemEnterPress(e, data);
const onCheck = e => this.onCheckboxChange(e, data);
if (filterRender) {
const props = {
className,
inputValue: keyword,
disabled,
data: pathData,
checkStatus: {
checked: checkedKeys.has(data.key),
halfChecked: halfCheckedKeys.has(data.key)
},
selected,
onClick,
onCheck
};
const item = filterRender(props);
const otherProps = virtualize ? {
key,
style: Object.assign(Object.assign({}, (_a = item.props.style) !== null && _a !== void 0 ? _a : {}), style)
} : {
key
};
return /*#__PURE__*/React.cloneElement(item, otherProps);
}
return /*#__PURE__*/React.createElement("li", {
role: 'menuitem',
className: className,
style: style,
key: key,
onClick: onClick,
onKeyPress: onKeyPress
}, /*#__PURE__*/React.createElement("span", {
className: `${prefixcls}-label`
}, !multiple && this.renderIcon('empty'), multiple && (/*#__PURE__*/React.createElement(Checkbox, {
onChange: onCheck,
disabled: disabled,
indeterminate: halfCheckedKeys.has(data.key),
checked: checkedKeys.has(data.key),
className: `${prefixcls}-label-checkbox`
})), this.highlight(searchText)));
};
this.renderFlattenOption = data => {
const {
virtualize
} = this.props;
const content = /*#__PURE__*/React.createElement("ul", {
className: `${prefixcls}-list`,
key: 'flatten-list'
}, virtualize ? this.renderVirtualizeList(data) : data.map(item => this.renderFlattenOptionItem(item)));
return content;
};
this.renderVirtualizeList = visibleOptions => {
var _a;
const {
direction
} = this.context;
const {
virtualize
} = this.props;
return /*#__PURE__*/React.createElement(List, {
height: virtualize.height,
itemCount: visibleOptions.length,
itemSize: virtualize.itemSize,
itemData: {
visibleOptions,
renderOption: this.renderFlattenOptionItem
},
width: (_a = virtualize.width) !== null && _a !== void 0 ? _a : '100%',
style: {
direction
}
}, VirtualRow);
};
}
renderItem(renderData) {
let content = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
const {
multiple,
checkedKeys,
halfCheckedKeys
} = this.props;
let showChildItem;
const ind = content.length;
content.push(/*#__PURE__*/React.createElement("ul", {
role: 'menu',
className: `${prefixcls}-list`,
key: renderData[0].key,
onScroll: e => this.props.onListScroll(e, ind)
}, renderData.map(item => {
const {
data,
key,
parentKey
} = item;
const {
children,
label,
disabled,
isLeaf
} = data;
const {
active,
selected,
loading
} = this.getItemStatus(key);
const hasChild = Boolean(children) && children.length;
const showExpand = hasChild || this.props.loadData && !isLeaf;
if (active && hasChild) {
showChildItem = item;
}
const className = cls(prefixcls, {
[`${prefixcls}-active`]: active && !selected,
[`${prefixcls}-select`]: selected && !multiple,
[`${prefixcls}-disabled`]: disabled
});
const otherAriaProps = parentKey ? {
['aria-owns']: `cascaderItem-${parentKey}`
} : {};
return /*#__PURE__*/React.createElement("li", Object.assign({
role: 'menuitem',
id: `cascaderItem-${key}`,
"aria-expanded": active,
"aria-haspopup": Boolean(showExpand),
"aria-disabled": disabled
}, otherAriaProps, {
className: className,
key: key,
onClick: e => {
this.onClick(e, item);
},
onKeyPress: e => this.handleItemEnterPress(e, item),
onMouseEnter: e => {
this.onHover(e, item);
}
}), /*#__PURE__*/React.createElement("span", {
className: `${prefixcls}-label`
}, selected && !multiple && this.renderIcon('tick'), !selected && !multiple && this.renderIcon('empty'), multiple && (/*#__PURE__*/React.createElement(Checkbox, {
onChange: e => this.onCheckboxChange(e, item),
disabled: disabled,
indeterminate: halfCheckedKeys.has(item.key),
checked: checkedKeys.has(item.key),
className: `${prefixcls}-label-checkbox`
})), /*#__PURE__*/React.createElement("span", null, label)), showExpand ? this.renderIcon(loading ? 'loading' : 'child', true) : null);
})));
if (showChildItem) {
content.concat(this.renderItem(showChildItem.children, content));
}
return content;
}
renderEmpty() {
const {
emptyContent
} = this.props;
if (emptyContent === null) {
return null;
}
return /*#__PURE__*/React.createElement(LocaleConsumer, {
componentName: "Cascader"
}, locale => (/*#__PURE__*/React.createElement("ul", {
className: `${prefixcls} ${prefixcls}-empty`,
key: 'empty-list'
}, /*#__PURE__*/React.createElement("span", {
className: `${prefixcls}-label`,
"x-semi-prop": "emptyContent"
}, emptyContent || locale.emptyText))));
}
render() {
const {
data,
searchable
} = this.props;
const {
direction
} = this.context;
const isEmpty = !data || !data.length;
let content;
const listsCls = cls({
[`${prefixcls}-lists`]: true,
[`${prefixcls}-lists-rtl`]: direction === 'rtl',
[`${prefixcls}-lists-empty`]: isEmpty
});
if (isEmpty) {
content = this.renderEmpty();
} else {
content = searchable ? this.renderFlattenOption(data) : this.renderItem(data);
}
return /*#__PURE__*/React.createElement("div", {
className: listsCls
}, content);
}
}
Item.contextType = ConfigContext;
Item.propTypes = {
data: PropTypes.array,
emptyContent: PropTypes.node,
searchable: PropTypes.bool,
onItemClick: PropTypes.func,
onItemHover: PropTypes.func,
multiple: PropTypes.bool,
showNext: PropTypes.oneOf([strings.SHOW_NEXT_BY_CLICK, strings.SHOW_NEXT_BY_HOVER]),
checkedKeys: PropTypes.object,
halfCheckedKeys: PropTypes.object,
onItemCheckboxClick: PropTypes.func,
separator: PropTypes.string,
keyword: PropTypes.string,
virtualize: PropTypes.object,
expandIcon: PropTypes.node
};
Item.defaultProps = {
empty: false
};