UNPKG

@gravity-ui/uikit

Version:

Gravity UI base styling and components

377 lines (376 loc) 17.7 kB
'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