123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- import React, { PureComponent } from 'react';
- import cls from 'classnames';
- import PropTypes from 'prop-types';
- import { cssClasses, strings } from '@douyinfe/semi-foundation/cascader/constants';
- import isEnterPress from '@douyinfe/semi-foundation/utils/isEnterPress';
- import { includes } from 'lodash';
- import ConfigContext, { ContextValue } from '../configProvider/context';
- import LocaleConsumer from '../locale/localeConsumer';
- import { IconChevronRight, IconTick } from '@douyinfe/semi-icons';
- import { Locale } from '../locale/interface';
- import Spin from '../spin';
- import Checkbox, { CheckboxEvent } from '../checkbox';
- import {
- BasicCascaderData,
- BasicEntity,
- ShowNextType,
- BasicData
- } from '@douyinfe/semi-foundation/cascader/foundation';
- export interface CascaderData extends BasicCascaderData {
- label: React.ReactNode
- }
- export interface Entity extends BasicEntity {
- /* children list */
- children?: Array<Entity>;
- /* treedata */
- data: CascaderData;
- /* parent data */
- parent?: Entity
- }
- export interface Entities {
- [idx: string]: Entity
- }
- export interface Data extends BasicData {
- data: CascaderData;
- searchText: React.ReactNode[]
- }
- export interface CascaderItemProps {
- activeKeys: Set<string>;
- selectedKeys: Set<string>;
- loadedKeys: Set<string>;
- loadingKeys: Set<string>;
- onItemClick: (e: React.MouseEvent | React.KeyboardEvent, item: Entity | Data) => void;
- onItemHover: (e: React.MouseEvent, item: Entity) => void;
- showNext: ShowNextType;
- onItemCheckboxClick: (item: Entity | Data) => void;
- onListScroll: (e: React.UIEvent<HTMLUListElement, UIEvent>, ind: number) => void;
- searchable: boolean;
- keyword: string;
- empty: boolean;
- emptyContent: React.ReactNode;
- loadData: (selectOptions: CascaderData[]) => Promise<void>;
- data: Array<Data | Entity>;
- separator: string;
- multiple: boolean;
- checkedKeys: Set<string>;
- halfCheckedKeys: Set<string>
- }
- const prefixcls = cssClasses.PREFIX_OPTION;
- export default class Item extends PureComponent<CascaderItemProps> {
- static contextType = ConfigContext;
- static 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
- };
- static defaultProps = {
- empty: false,
- };
- context: ContextValue;
- onClick = (e: React.MouseEvent | React.KeyboardEvent, item: Entity | Data) => {
- const { onItemClick } = this.props;
- if (item.data.disabled || ('disabled' in item && item.disabled)) {
- return;
- }
- onItemClick(e, item);
- };
- /**
- * A11y: simulate item click
- */
- handleItemEnterPress = (keyboardEvent: React.KeyboardEvent, item: Entity | Data) => {
- if (isEnterPress(keyboardEvent)) {
- this.onClick(keyboardEvent, item);
- }
- }
- onHover = (e: React.MouseEvent, item: Entity) => {
- const { showNext, onItemHover } = this.props;
- if (item.data.disabled) {
- return;
- }
- if (showNext === strings.SHOW_NEXT_BY_HOVER) {
- onItemHover(e, item);
- }
- };
- onCheckboxChange = (e: CheckboxEvent, item: Entity | Data) => {
- 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);
- };
- getItemStatus = (key: string) => {
- 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;
- };
- renderIcon = (type: string) => {
- switch (type) {
- case 'child':
- return (<IconChevronRight className={`${prefixcls}-icon ${prefixcls}-icon-expand`} />);
- case 'tick':
- return (<IconTick className={`${prefixcls}-icon ${prefixcls}-icon-active`} />);
- case 'loading':
- return <Spin wrapperClassName={`${prefixcls}-spin-icon`} />;
- case 'empty':
- return (<span aria-hidden={true} className={`${prefixcls}-icon ${prefixcls}-icon-empty`} />);
- default:
- return null;
- }
- };
- highlight = (searchText: React.ReactNode[]) => {
- const content: React.ReactNode[] = [];
- const { keyword, separator } = this.props;
- searchText.forEach((item, idx) => {
- if (typeof item === 'string' && includes(item, keyword)) {
- item.split(keyword).forEach((node, index) => {
- if (index > 0) {
- content.push(
- <span className={`${prefixcls}-label-highlight`} key={`${index}-${idx}`}>
- {keyword}
- </span>
- );
- }
- content.push(node);
- });
- } else {
- content.push(item);
- }
- if (idx !== searchText.length - 1) {
- content.push(separator);
- }
- });
- return content;
- };
- renderFlattenOption = (data: Data[]) => {
- const { multiple, checkedKeys, halfCheckedKeys } = this.props;
- const content = (
- <ul className={`${prefixcls}-list`} key={'flatten-list'}>
- {data.map(item => {
- const { searchText, key, disabled } = item;
- const className = cls(prefixcls, {
- [`${prefixcls}-flatten`]: true,
- [`${prefixcls}-disabled`]: disabled
- });
- return (
- <li
- role='menuitem'
- className={className}
- key={key}
- onClick={e => {
- this.onClick(e, item);
- }}
- onKeyPress={e => this.handleItemEnterPress(e, item)}
- >
- <span className={`${prefixcls}-label`}>
- {!multiple && this.renderIcon('empty')}
- {multiple && (
- <Checkbox
- onChange={(e: CheckboxEvent) => this.onCheckboxChange(e, item)}
- disabled={disabled}
- indeterminate={halfCheckedKeys.has(item.key)}
- checked={checkedKeys.has(item.key)}
- className={`${prefixcls}-label-checkbox`}
- />
- )}
- {this.highlight(searchText)}
- </span>
- </li>
- );
- })}
- </ul>
- );
- return content;
- };
- renderItem(renderData: Array<Entity>, content: Array<React.ReactNode> = []) {
- const { multiple, checkedKeys, halfCheckedKeys } = this.props;
- let showChildItem: Entity;
- const ind = content.length;
- content.push(
- <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 (
- <li
- 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);
- }}
- >
- <span className={`${prefixcls}-label`}>
- {selected && !multiple && this.renderIcon('tick')}
- {!selected && !multiple && this.renderIcon('empty')}
- {multiple && (
- <Checkbox
- onChange={(e: CheckboxEvent) => this.onCheckboxChange(e, item)}
- disabled={disabled}
- indeterminate={halfCheckedKeys.has(item.key)}
- checked={checkedKeys.has(item.key)}
- className={`${prefixcls}-label-checkbox`}
- />
- )}
- <span>{label}</span>
- </span>
- {showExpand ? this.renderIcon(loading ? 'loading' : 'child') : null}
- </li>
- );
- })}
- </ul>
- );
- if (showChildItem) {
- content.concat(this.renderItem(showChildItem.children, content));
- }
- return content;
- }
- renderEmpty() {
- const { emptyContent } = this.props;
- return (
- <LocaleConsumer componentName="Cascader">
- {(locale: Locale['Cascader']) => (
- <ul className={`${prefixcls} ${prefixcls}-empty`} key={'empty-list'}>
- <span className={`${prefixcls}-label`} x-semi-prop="emptyContent">
- {emptyContent || locale.emptyText}
- </span>
- </ul>
- )}
- </LocaleConsumer>
- );
- }
- 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 as Data[]) :
- this.renderItem(data as Entity[]);
- }
- return (
- <div className={listsCls}>
- {content}
- </div>
- );
- }
- }
|