@gravity-ui/uikit
Version:
Gravity UI base styling and components
377 lines (376 loc) • 17.7 kB
JavaScript
'use client';
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.List = exports.listDefaultProps = void 0;
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const React = tslib_1.__importStar(require("react"));
const isEqual_1 = tslib_1.__importDefault(require("lodash/isEqual.js"));
const isObject_1 = tslib_1.__importDefault(require("lodash/isObject.js"));
const react_beautiful_dnd_1 = require("react-beautiful-dnd");
const react_virtualized_auto_sizer_1 = tslib_1.__importDefault(require("react-virtualized-auto-sizer"));
const react_window_1 = require("react-window");
const controls_1 = require("../controls/index.js");
const mobile_1 = require("../mobile/index.js");
const theme_1 = require("../theme/index.js");
const cn_1 = require("../utils/cn.js");
const common_1 = require("../utils/common.js");
const ListLoadingIndicator_1 = require("./ListLoadingIndicator.js");
const components_1 = require("./components/index.js");
const constants_1 = require("./constants.js");
require("./List.css");
const b = (0, cn_1.block)('list');
const DEFAULT_ITEM_HEIGHT = 28;
const DEFAULT_PAGE_SIZE = 10;
exports.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 (0, jsx_runtime_1.jsx)(react_window_1.VariableSizeList, { ref: ref, ...props, direction: (0, theme_1.useDirection)() });
});
ListContainer.displayName = 'ListContainer';
class List extends React.Component {
static defaultProps = exports.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 = (0, common_1.getUniqId)();
componentDidMount() {
this.activateItem(this.props.activeItemIndex, true);
}
componentDidUpdate(prevProps, prevState) {
if (!(0, isEqual_1.default)(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 ((0, jsx_runtime_1.jsx)(mobile_1.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
(0, jsx_runtime_1.jsxs)("div", { className: b({ mobile }, className), "data-qa": qa, tabIndex: -1, onFocus: this.handleFocus, onBlur: this.handleBlur, onKeyDown: this.onKeyDown, children: [this.renderFilter(), (0, jsx_runtime_1.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) && ((0, jsx_runtime_1.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 (constants_1.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 ((0, isObject_1.default)(item) && 'value' in item && item.value === this.loadingItem.value) {
return (0, jsx_runtime_1.jsx)(ListLoadingIndicator_1.ListLoadingIndicator, { onIntersect: itemIndex === 0 ? undefined : onLoadMore });
}
return this.props.renderItem
? this.props.renderItem(item, isItemActive, itemIndex)
: (0, components_1.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 ((0, jsx_runtime_1.jsx)(components_1.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 ((0, jsx_runtime_1.jsx)(react_beautiful_dnd_1.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 ((0, jsx_runtime_1.jsx)("div", { className: b('filter', filterClassName), children: (0, jsx_runtime_1.jsx)(controls_1.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 ((0, jsx_runtime_1.jsx)(react_beautiful_dnd_1.DragDropContext, { onDragStart: this.onSortStart, onDragEnd: this.onSortEnd, children: (0, jsx_runtime_1.jsx)(react_beautiful_dnd_1.Droppable, { droppableId: "droppable", renderClone: (provided, snapshot, rubric) => {
return this.renderItem({
index: rubric.source.index,
provided,
isDragging: snapshot.isDragging,
});
}, children: (droppableProvided) => ((0, jsx_runtime_1.jsx)(components_1.SimpleContainer, { ref: this.refContainer, itemCount: items.length, provided: droppableProvided, onScrollToItem: this.props.onScrollToItem, children: items.map((_item, index) => {
return ((0, jsx_runtime_1.jsx)(react_beautiful_dnd_1.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 ((0, jsx_runtime_1.jsx)(components_1.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 ((0, jsx_runtime_1.jsx)(react_beautiful_dnd_1.DragDropContext, { onDragStart: this.onSortStart, onDragEnd: this.onSortEnd, children: (0, jsx_runtime_1.jsx)(react_beautiful_dnd_1.Droppable, { droppableId: "droppable", mode: "virtual", renderClone: (provided, snapshot, rubric) => {
return this.renderItem({
index: rubric.source.index,
provided,
isDragging: snapshot.isDragging,
});
}, children: (droppableProvided) => ((0, jsx_runtime_1.jsx)(react_virtualized_auto_sizer_1.default, { children: ({ width, height }) => ((0, jsx_runtime_1.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 ((0, jsx_runtime_1.jsx)(react_virtualized_auto_sizer_1.default, { children: ({ width, height }) => ((0, jsx_runtime_1.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;
};
}
exports.List = List;
//# sourceMappingURL=List.js.map