@wix/design-system
Version:
@wix/design-system
90 lines • 4.87 kB
JavaScript
import React, { useCallback, useMemo, useState } from 'react';
import classNames from 'classnames';
import Item from './Item';
import { flattenVisible, getDepth } from './utils';
// Resolve childrenStyle.marginLeft to a numeric per-depth indent for the
// absolute layout. Accepts both numbers and "{n}px" strings.
function parseIndentPx(childrenStyle) {
const ml = childrenStyle && childrenStyle.marginLeft;
if (typeof ml === 'number' && Number.isFinite(ml)) {
return ml;
}
if (typeof ml === 'string') {
const n = parseInt(ml, 10);
return Number.isFinite(n) ? n : 0;
}
return 0;
}
const VirtualContainer = React.forwardRef(function VirtualContainer({ items, childrenProperty, childrenStyle, renderAction, isRenderDraggingChildren, theme, itemHeight, viewportHeight, offscreenRowCount = 5, pinnedItemId, }, containerRef) {
const [scrollTop, setScrollTop] = useState(0);
// Memoizing the row stream is what makes scrolling cheap. The action-row
// entries store truthiness only (no rendered React node), so this cache
// stays valid across drag-state changes — action-row styling depends on
// isDragging, but action-row *existence* depends only on stable props
// (actions/addItemLabel/maxDepth). We re-invoke renderAction at render
// time per visible action row to pick up live styling.
const rows = useMemo(() => flattenVisible({ items, renderAction, childrenProperty }), [items, renderAction, childrenProperty]);
const indentPx = parseIndentPx(childrenStyle);
const totalHeight = rows.length * itemHeight;
const firstWindowIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - offscreenRowCount);
const lastWindowIndex = Math.min(rows.length, Math.ceil((scrollTop + viewportHeight) / itemHeight) + offscreenRowCount);
const handleScroll = useCallback(e => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
const containerClass = classNames('nestable-container', 'nestable-top-container', theme && theme.topContainer);
const visible = [];
for (let i = firstWindowIndex; i < lastWindowIndex; i++) {
visible.push({ row: rows[i], absoluteIndex: i });
}
// Pin the focused/keyboard-moved row in the slice so its keyboard handlers
// survive scroll. Without this, arrow-key reorder across viewport edge
// unmounts the focused Item and onKeyUp dies.
if (pinnedItemId) {
const alreadyVisible = visible.some(v => v.row.kind === 'item' && v.row.item.id === pinnedItemId);
if (!alreadyVisible) {
const pinnedIdx = rows.findIndex(r => r.kind === 'item' && r.item.id === pinnedItemId);
if (pinnedIdx !== -1) {
visible.push({ row: rows[pinnedIdx], absoluteIndex: pinnedIdx });
}
}
}
return (React.createElement("div", { ref: containerRef, className: containerClass, onScroll: handleScroll, style: {
height: viewportHeight,
overflowY: 'auto',
position: 'relative',
} },
React.createElement("div", { style: { height: totalHeight, position: 'relative' } }, visible.map(({ row, absoluteIndex }) => {
const top = absoluteIndex * itemHeight;
const left = (row.kind === 'item' ? row.treeDepth - 1 : row.depth - 1) *
indentPx;
const positionStyle = {
position: 'absolute',
top,
left,
right: 0,
minHeight: itemHeight,
};
if (row.kind === 'item') {
const itemForRender = row.isParentLocked
? { ...row.item, isParentLocked: true, draggable: false }
: row.item;
return (React.createElement("div", { key: `item-${row.item.id}`, style: positionStyle },
React.createElement(Item, { id: row.item.id, item: itemForRender, index: row.index, isVeryLastItem: row.isVeryLastItem, siblings: row.siblings, isRenderDraggingChildren: isRenderDraggingChildren, position: row.position, depth: getDepth(row.item, childrenProperty), theme: theme })));
}
// Re-invoke renderAction at render time to pick up live styling
// (depends on state.isDragging in the consumer).
const actionNode = renderAction({
siblings: row.siblings,
item: row.parentItem,
isRootAction: row.isRootAction,
veryLastItem: row.veryLastItem,
depth: row.depth,
});
if (!actionNode) {
return null;
}
return (React.createElement("div", { key: `action-${row.parentItem.id}`, style: positionStyle }, actionNode));
}))));
});
export default VirtualContainer;
//# sourceMappingURL=VirtualContainer.js.map