UNPKG

@gravity-ui/uikit

Version:

Gravity UI base styling and components

372 lines (371 loc) 16.7 kB
'use client'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import * as React from 'react'; import isEqual from "lodash/isEqual.js"; import isObject from "lodash/isObject.js"; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import AutoSizer from 'react-virtualized-auto-sizer'; import { VariableSizeList } from 'react-window'; import { TextInput } from "../controls/index.js"; import { MobileContext } from "../mobile/index.js"; import { useDirection } from "../theme/index.js"; import { block } from "../utils/cn.js"; import { getUniqId } from "../utils/common.js"; import { ListLoadingIndicator } from "./ListLoadingIndicator.js"; import { ListItem, SimpleContainer, defaultRenderItem } from "./components/index.js"; import { listNavigationIgnoredKeys } from "./constants.js"; import "./List.css"; const b = block('list'); const DEFAULT_ITEM_HEIGHT = 28; const DEFAULT_PAGE_SIZE = 10; export const listDefaultProps = { items: [], itemClassName: '', filterable: true, sortable: false, virtualized: true, deactivateOnLeave: true, }; const reorder = (list, startIndex, endIndex) => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); return result; }; const ListContainer = React.forwardRef((props, ref) => { return _jsx(VariableSizeList, { ref: ref, ...props, direction: useDirection() }); }); ListContainer.displayName = 'ListContainer'; export class List extends React.Component { static defaultProps = listDefaultProps; static moveListElement(list, oldIndex, newIndex) { if (oldIndex !== newIndex) { const [item] = list.splice(oldIndex, 1); list.splice(newIndex, 0, item); } return list; } static findNextIndex(list, index, step) { const dataLength = list.length; let currentIndex = (index + dataLength) % dataLength; for (let i = 0; i < dataLength; i += 1) { if (list[currentIndex] && !list[currentIndex].disabled) { return currentIndex; } currentIndex = (currentIndex + dataLength + step) % dataLength; } return undefined; } state = { items: this.props.items, activeItem: this.props.activeItemIndex, filter: '', }; refFilter = React.createRef(); refContainer = React.createRef(); blurTimer = null; loadingItem = { value: '__LIST_ITEM_LOADING__', disabled: false }; uniqId = getUniqId(); componentDidMount() { this.activateItem(this.props.activeItemIndex, true); } componentDidUpdate(prevProps, prevState) { if (!isEqual(this.props.items, prevProps.items)) { const filter = this.getFilter(); const internalFiltering = filter && !this.props.onFilterUpdate; if (internalFiltering) { this.onUpdateFilterInternal(filter); } else { this.setState({ items: this.props.items }); } } if (this.props.activeItemIndex !== prevProps.activeItemIndex) { this.activateItem(this.props.activeItemIndex); } if (this.props.onChangeActive && this.state.activeItem !== prevState.activeItem) { this.props.onChangeActive(this.state.activeItem); } } componentWillUnmount() { this.blurTimer = null; } render() { const { id, emptyPlaceholder, virtualized, className, itemsClassName, qa, role = 'list', } = this.props; const { items } = this.state; return (_jsx(MobileContext.Consumer, { children: ({ mobile }) => ( // The event handler should only be used to capture bubbled events // eslint-disable-next-line jsx-a11y/no-static-element-interactions _jsxs("div", { className: b({ mobile }, className), "data-qa": qa, tabIndex: -1, onFocus: this.handleFocus, onBlur: this.handleBlur, onKeyDown: this.onKeyDown, children: [this.renderFilter(), _jsxs("div", { id: id, className: b('items', { virtualized }, itemsClassName), style: this.getItemsStyle(), onMouseLeave: this.onMouseLeave, role: role, children: [this.renderItems(), items.length === 0 && Boolean(emptyPlaceholder) && (_jsx("div", { className: b('empty-placeholder'), children: emptyPlaceholder }))] })] })) })); } getItems() { return this.state.items; } getItemsWithLoading() { if (this.props.sortable) { return this.getItems(); } return this.props.loading ? [...this.state.items, this.loadingItem] : this.getItems(); } getActiveItem() { return typeof this.state.activeItem === 'number' ? this.state.activeItem : null; } activateItem(index, scrollTo = true) { this.setState({ activeItem: index }, () => { if (typeof index === 'number' && scrollTo) { this.scrollToIndex(index); } }); } onKeyDown = (event) => { const { activeItem, pageSize } = this.state; if (listNavigationIgnoredKeys.includes(event.key)) { return; } const isInputTarget = event.target instanceof HTMLInputElement; switch (event.key) { case 'ArrowDown': { this.handleKeyMove(event, 1, -1); break; } case 'ArrowUp': { this.handleKeyMove(event, -1); break; } case 'PageDown': { this.handleKeyMove(event, pageSize ?? DEFAULT_PAGE_SIZE); break; } case 'PageUp': { this.handleKeyMove(event, -(pageSize ?? DEFAULT_PAGE_SIZE)); break; } case 'Home': { // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ // ... if the combobox is editable, returns focus to the combobox and places the cursor on the first character (c) if (isInputTarget) { return; } this.handleKeyMove(event, this.state.items.length - (activeItem || 0)); break; } case 'End': { // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ // ... if the combobox is editable, returns focus to the combobox and places the cursor after the last character (c) if (isInputTarget) { return; } this.handleKeyMove(event, -(activeItem || 0) - 1); break; } case 'Enter': { if (typeof activeItem === 'number' && this.props.onItemClick) { this.props.onItemClick(this.state.items[activeItem], activeItem, true, event); } break; } default: { if (this.refFilter.current) { this.refFilter.current.focus(); } } } }; renderItemContent = (item, isItemActive, itemIndex) => { const { onLoadMore } = this.props; if (isObject(item) && 'value' in item && item.value === this.loadingItem.value) { return _jsx(ListLoadingIndicator, { onIntersect: itemIndex === 0 ? undefined : onLoadMore }); } return this.props.renderItem ? this.props.renderItem(item, isItemActive, itemIndex) : defaultRenderItem(item); }; renderItem = ({ index, style, height, provided, isDragging, }) => { const { sortHandleAlign, role } = this.props; const { items, activeItem } = this.state; const item = this.getItemsWithLoading()[index]; const sortable = this.props.sortable && items.length > 1 && !this.getFilter(); const active = index === activeItem || index === this.props.activeItemIndex; const selected = Array.isArray(this.props.selectedItemIndex) ? this.props.selectedItemIndex.includes(index) : index === this.props.selectedItemIndex; return (_jsx(ListItem, { style: style, height: height, itemIndex: index, item: item, sortable: sortable, sortHandleAlign: sortHandleAlign, renderItem: this.renderItemContent, itemClassName: this.props.itemClassName, active: active, selected: selected, onActivate: this.onItemActivate, onClick: this.props.onItemClick, role: role === 'listbox' ? 'option' : 'listitem', listId: this.props.id ?? this.uniqId, provided: provided, isDragging: isDragging }, index)); }; renderVirtualizedItem = ({ index, style, }) => { return (_jsx(Draggable, { draggableId: String(index), index: index, children: (provided) => this.renderItem({ index, style, provided }) }, `item-key-${index}`)); }; renderFilter() { const { size, filterable, filter = this.state.filter, filterPlaceholder, filterClassName = '', autoFocus, } = this.props; if (!filterable) { return null; } return (_jsx("div", { className: b('filter', filterClassName), children: _jsx(TextInput, { controlRef: this.refFilter, size: size, placeholder: filterPlaceholder, value: filter, hasClear: true, onUpdate: this.onFilterUpdate, autoFocus: autoFocus }) })); } renderSimpleContainer() { const { sortable } = this.props; const items = this.getItemsWithLoading(); if (sortable) { return (_jsx(DragDropContext, { onDragStart: this.onSortStart, onDragEnd: this.onSortEnd, children: _jsx(Droppable, { droppableId: "droppable", renderClone: (provided, snapshot, rubric) => { return this.renderItem({ index: rubric.source.index, provided, isDragging: snapshot.isDragging, }); }, children: (droppableProvided) => (_jsx(SimpleContainer, { ref: this.refContainer, itemCount: items.length, provided: droppableProvided, onScrollToItem: this.props.onScrollToItem, children: items.map((_item, index) => { return (_jsx(Draggable, { draggableId: String(index), index: index, children: (provided, snapshot) => { return this.renderItem({ index, isDragging: snapshot.isDragging, provided, height: this.getItemHeight(index), }); } }, `item-key-${index}`)); }) })) }) })); } return (_jsx(SimpleContainer, { itemCount: items.length, ref: this.refContainer, onScrollToItem: this.props.onScrollToItem, children: items.map((_item, index) => this.renderItem({ index, height: this.getItemHeight(index) })) })); } renderVirtualizedContainer() { // Otherwise, react-window will not update the list items const items = [...this.getItemsWithLoading()]; if (this.props.sortable) { return (_jsx(DragDropContext, { onDragStart: this.onSortStart, onDragEnd: this.onSortEnd, children: _jsx(Droppable, { droppableId: "droppable", mode: "virtual", renderClone: (provided, snapshot, rubric) => { return this.renderItem({ index: rubric.source.index, provided, isDragging: snapshot.isDragging, }); }, children: (droppableProvided) => (_jsx(AutoSizer, { children: ({ width, height }) => (_jsx(ListContainer, { ref: this.refContainer, outerRef: droppableProvided.innerRef, width: width, height: height, itemSize: this.getVirtualizedItemHeight, itemData: items, itemCount: items.length, overscanCount: 10, onItemsRendered: this.onItemsRendered, // this property used to rerender items in viewport // must be last, typescript skips checks for all props behind ts-ignore/ts-expect-error // @ts-expect-error activeItem: this.state.activeItem, children: this.renderVirtualizedItem })) })) }) })); } return (_jsx(AutoSizer, { children: ({ width, height }) => (_jsx(ListContainer, { ref: this.refContainer, width: width, height: height, itemSize: this.getVirtualizedItemHeight, itemData: items, itemCount: items.length, overscanCount: 10, onItemsRendered: this.onItemsRendered, // this property used to rerender items in viewport // must be last, typescript skips checks for all props behind ts-ignore/ts-expect-error // @ts-expect-error activeItem: this.state.activeItem, children: this.renderItem })) })); } renderItems() { if (this.props.virtualized) { return this.renderVirtualizedContainer(); } else { return this.renderSimpleContainer(); } } filterItem = (filter) => (item) => { return String(item).includes(filter); }; getFilter() { const { filter = this.state.filter } = this.props; return filter; } getItemsStyle() { let { itemsHeight } = this.props; if (typeof itemsHeight === 'function') { itemsHeight = itemsHeight(this.state.items); } return itemsHeight ? { height: itemsHeight } : undefined; } scrollToIndex = (index) => { const container = this.refContainer.current; if (container) { container.scrollToItem(index); } }; deactivate = () => { if (!this.blurTimer) { return; } this.blurTimer = null; if (this.props.deactivateOnLeave) { this.setState({ activeItem: undefined }); } }; handleKeyMove(event, step, defaultItemIndex = 0) { const { activeItem = defaultItemIndex } = this.state; event.preventDefault(); const items = this.getItemsWithLoading(); this.activateItem(List.findNextIndex(items, activeItem + step, Math.sign(step))); } handleFocus = () => { if (this.blurTimer) { clearTimeout(this.blurTimer); this.blurTimer = null; } }; handleBlur = () => { if (!this.blurTimer) { this.blurTimer = setTimeout(this.deactivate, 50); } }; onUpdateFilterInternal = (value) => { const { items, filterItem = this.filterItem, onFilterEnd } = this.props; this.setState({ filter: value, items: value ? items.filter(filterItem(value)) : items, }, () => { if (onFilterEnd) { onFilterEnd({ items: this.state.items }); } }); }; onFilterUpdate = (value) => { if (this.props.onFilterUpdate) { this.props.onFilterUpdate(value); } else { this.onUpdateFilterInternal(value); } }; onItemsRendered = ({ visibleStartIndex, visibleStopIndex, }) => { this.setState({ pageSize: visibleStopIndex - visibleStartIndex, }); }; onItemActivate = (index) => { if (!this.state.sorting) { this.activateItem(index, false); } }; onMouseLeave = () => { this.handleBlur(); }; onSortStart = () => { this.setState({ sorting: true }); }; onSortEnd = (result) => { if (!result.destination) { this.setState({ sorting: false }); return; } if (result.source.index === result.destination.index) { this.setState({ sorting: false }); return; } const oldIndex = result.source.index; const newIndex = result.destination.index; if (this.props.onSortEnd) { this.props.onSortEnd({ oldIndex, newIndex }); } const nextItems = reorder(this.getItems(), oldIndex, newIndex); this.setState({ activeItem: newIndex, items: nextItems, sorting: false, }); }; getItemHeight = (index) => { const { itemHeight } = this.props; if (typeof itemHeight === 'function') { const { items } = this.state; return itemHeight(items[index], index); } return itemHeight; }; getVirtualizedItemHeight = (index) => { return this.getItemHeight(index) || DEFAULT_ITEM_HEIGHT; }; } //# sourceMappingURL=List.js.map