@vectara/vectara-ui
Version:
Vectara's design system, codified as a React and Sass component library
95 lines (94 loc) • 6.02 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import React, { useCallback, useMemo, useRef, useState } from "react";
import classNames from "classnames";
import { VuiFlexItem } from "../flex/FlexItem";
import { VuiSpinner } from "../spinner/Spinner";
import { VuiText } from "../typography/Text";
import { VuiTableContent } from "../table/TableContent";
import { buildAndFlattenSpans } from "./buildAndFlattenSpans";
import { VuiSpansRow } from "./SpansRow";
import { VuiSpansHeaderCell } from "./SpansHeaderCell";
import { VuiSpansLoadingRow } from "./SpansLoadingRow";
const DEFAULT_INDENT_SIZE = 16;
const extractId = (row, idField) => {
return typeof idField === "function" ? idField(row) : row[idField];
};
const extractParentId = (row, parentField) => {
if (typeof parentField === "function")
return parentField(row);
const value = row[parentField];
return value === undefined || value === null ? null : value;
};
export const VuiSpans = ({ idField, parentField, columns, rows, expandedIds, onExpandedIdsChange, onExpand, isLoadingChildren, isLoading, content, className, rowDecorator, indentSize = DEFAULT_INDENT_SIZE, isHeaderSticky, fluid, "data-testid": dataTestId }) => {
const [internalLoadingIds, setInternalLoadingIds] = useState(new Set());
// Tracks pending fetches so we ignore stale resolutions (e.g. user collapses
// mid-fetch, then re-expands — the original promise should not clear loading
// state for the new attempt).
const fetchTokensRef = useRef(new Map());
const getId = useCallback((row) => extractId(row, idField), [idField]);
const getParentId = useCallback((row) => extractParentId(row, parentField), [parentField]);
const flatSpans = useMemo(() => buildAndFlattenSpans(rows, expandedIds, getId, getParentId), [rows, expandedIds, getId, getParentId]);
const columnCount = columns.length;
const handleToggle = useCallback((row, id, hasChildren, hasLoadedChildren) => {
var _a;
const isCurrentlyExpanded = expandedIds.has(id);
const nextExpandedIds = new Set(expandedIds);
if (isCurrentlyExpanded) {
nextExpandedIds.delete(id);
onExpandedIdsChange(nextExpandedIds);
return;
}
nextExpandedIds.add(id);
onExpandedIdsChange(nextExpandedIds);
// Only fire the consumer fetch when the row claims to have children
// (`hasChildren`) but none are present in the rows list yet.
const needsFetch = hasChildren && !hasLoadedChildren && Boolean(onExpand);
if (!needsFetch)
return;
const pendingFetchTokens = fetchTokensRef.current;
const currentFetchToken = ((_a = pendingFetchTokens.get(id)) !== null && _a !== void 0 ? _a : 0) + 1;
pendingFetchTokens.set(id, currentFetchToken);
setInternalLoadingIds((prev) => {
const nextLoadingIds = new Set(prev);
nextLoadingIds.add(id);
return nextLoadingIds;
});
const clearLoading = () => {
// Only clear if our token is still the latest — otherwise a newer
// fetch is in flight and owns the loading state.
if (pendingFetchTokens.get(id) !== currentFetchToken)
return;
setInternalLoadingIds((prev) => {
const nextLoadingIds = new Set(prev);
nextLoadingIds.delete(id);
return nextLoadingIds;
});
};
Promise.resolve(onExpand(row)).finally(clearLoading);
}, [expandedIds, onExpandedIdsChange, onExpand]);
const classes = classNames("vuiSpans", className, {
"vuiSpans--fluid": fluid
});
let tbodyContent;
if (content) {
tbodyContent = _jsx(VuiTableContent, Object.assign({ colSpan: columnCount }, { children: content }));
}
else if (isLoading) {
tbodyContent = (_jsxs(VuiTableContent, Object.assign({ colSpan: columnCount }, { children: [_jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiSpinner, { size: "xs" }) })), _jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiText, { children: _jsx("p", { children: "Loading" }) }) }))] })));
}
else {
tbodyContent = flatSpans.map((flat, rowIndex) => {
const { row, id, depth, hasChildren, hasLoadedChildren, posInSet, setSize } = flat;
const isExpanded = expandedIds.has(id);
const externalLoading = isLoadingChildren ? isLoadingChildren(row) : false;
const isLoadingRow = internalLoadingIds.has(id) || externalLoading;
const showLoadingRow = isExpanded && hasChildren && !hasLoadedChildren && isLoadingRow;
return (_jsxs(React.Fragment, { children: [_jsx(VuiSpansRow, { row: row, rowIndex: rowIndex, columns: columns, id: id, depth: depth, indentSize: indentSize, posInSet: posInSet, setSize: setSize, hasChildren: hasChildren, isExpanded: isExpanded, onToggle: () => handleToggle(row, id, hasChildren, hasLoadedChildren), rowDecorator: rowDecorator }), showLoadingRow && (_jsx(VuiSpansLoadingRow, { colSpan: columnCount, depth: depth + 1, indentSize: indentSize }, `${id}__loading`))] }, id));
});
}
return (_jsx("div", Object.assign({ className: "vuiSpansWrapper", "data-testid": dataTestId }, { children: _jsxs("table", Object.assign({ className: classes, role: "treegrid" }, { children: [_jsx("thead", Object.assign({ className: isHeaderSticky ? "vuiSpansStickyHeader" : undefined }, { children: _jsx("tr", Object.assign({ role: "row" }, { children: columns.map((column) => {
const { name, width } = column;
const styles = width ? { width } : undefined;
return (_jsx("th", Object.assign({ role: "columnheader", style: styles }, { children: _jsx(VuiSpansHeaderCell, { column: column }) }), name));
}) })) })), _jsx("tbody", { children: tbodyContent })] })) })));
};