UNPKG

@vectara/vectara-ui

Version:

Vectara's design system, codified as a React and Sass component library

95 lines (94 loc) 6.02 kB
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 })] })) }))); };