@vectara/vectara-ui 16.3.1 → 16.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/index.d.ts +4 -2
- package/lib/components/index.js +2 -1
- package/lib/components/modal/Modal.js +10 -1
- package/lib/components/spans/Spans.d.ts +23 -0
- package/lib/components/spans/Spans.js +94 -0
- package/lib/components/spans/SpansCell.d.ts +5 -0
- package/lib/components/spans/SpansCell.js +4 -0
- package/lib/components/spans/SpansHeaderCell.d.ts +6 -0
- package/lib/components/spans/SpansHeaderCell.js +5 -0
- package/lib/components/spans/SpansLoadingRow.d.ts +8 -0
- package/lib/components/spans/SpansLoadingRow.js +9 -0
- package/lib/components/spans/SpansRow.d.ts +17 -0
- package/lib/components/spans/SpansRow.js +43 -0
- package/lib/components/spans/_index.scss +110 -0
- package/lib/components/spans/buildAndFlattenSpans.d.ts +14 -0
- package/lib/components/spans/buildAndFlattenSpans.js +69 -0
- package/lib/components/spans/buildAndFlattenSpans.test.d.ts +1 -0
- package/lib/components/spans/buildAndFlattenSpans.test.js +388 -0
- package/lib/components/spans/types.d.ts +5 -0
- package/lib/components/spans/types.js +1 -0
- package/lib/styles/index.css +98 -0
- package/package.json +1 -1
- package/src/docs/pages/drawer/PrimaryDrawer.tsx +14 -0
- package/src/docs/pages/spans/Spans.tsx +170 -0
- package/src/docs/pages/spans/createFakeSpans.ts +118 -0
- package/src/docs/pages/spans/index.tsx +11 -0
- package/src/docs/pages.tsx +2 -1
|
@@ -79,6 +79,8 @@ import { VuiStepsVertical, StepsVertical, StepVerticalStatus } from "./stepsVert
|
|
|
79
79
|
import { SKELETON_COLOR, VuiSkeleton } from "./skeleton/Skeleton";
|
|
80
80
|
import { VuiSummary, VuiSummaryCitation } from "./summary";
|
|
81
81
|
import { VuiTable } from "./table/Table";
|
|
82
|
+
import { VuiSpans } from "./spans/Spans";
|
|
83
|
+
import { SpansRow } from "./spans/types";
|
|
82
84
|
import { VuiTab } from "./tabs/Tab";
|
|
83
85
|
import { VuiTabbedRoutes } from "./tabs/TabbedRoutes";
|
|
84
86
|
import { VuiTabs } from "./tabs/Tabs";
|
|
@@ -95,5 +97,5 @@ import { VuiInfoTooltip } from "./tooltip/InfoTooltip";
|
|
|
95
97
|
import { VuiTopicButton } from "./topicButton/TopicButton";
|
|
96
98
|
import { copyToClipboard } from "../utils/copyToClipboard";
|
|
97
99
|
import { toRgb, toRgba } from "./context/Theme";
|
|
98
|
-
export type { AnchorSide, AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CheckboxConfig, CodeEditorColorConfig, CodeEditorError, CodeLanguage, InfoListItemType, InfoListType, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, OptionListItem, Pagination, RadioButtonConfig, SearchResult, SearchSuggestion, Sections, SectionItem, Stat, StepStatus, StepSize, Steps, StepsVertical, StepVerticalStatus, TabSize, Tree, TreeItem };
|
|
99
|
-
export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
|
|
100
|
+
export type { AnchorSide, AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CheckboxConfig, CodeEditorColorConfig, CodeEditorError, CodeLanguage, InfoListItemType, InfoListType, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, OptionListItem, Pagination, RadioButtonConfig, SearchResult, SearchSuggestion, Sections, SectionItem, SpansRow, Stat, StepStatus, StepSize, Steps, StepsVertical, StepVerticalStatus, TabSize, Tree, TreeItem };
|
|
101
|
+
export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpans, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
|
package/lib/components/index.js
CHANGED
|
@@ -72,6 +72,7 @@ import { VuiStepsVertical } from "./stepsVertical/StepsVertical";
|
|
|
72
72
|
import { SKELETON_COLOR, VuiSkeleton } from "./skeleton/Skeleton";
|
|
73
73
|
import { VuiSummary, VuiSummaryCitation } from "./summary";
|
|
74
74
|
import { VuiTable } from "./table/Table";
|
|
75
|
+
import { VuiSpans } from "./spans/Spans";
|
|
75
76
|
import { VuiTab } from "./tabs/Tab";
|
|
76
77
|
import { VuiTabbedRoutes } from "./tabs/TabbedRoutes";
|
|
77
78
|
import { VuiTabs } from "./tabs/Tabs";
|
|
@@ -88,4 +89,4 @@ import { VuiInfoTooltip } from "./tooltip/InfoTooltip";
|
|
|
88
89
|
import { VuiTopicButton } from "./topicButton/TopicButton";
|
|
89
90
|
import { copyToClipboard } from "../utils/copyToClipboard";
|
|
90
91
|
import { toRgb, toRgba } from "./context/Theme";
|
|
91
|
-
export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
|
|
92
|
+
export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpans, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
|
|
@@ -59,11 +59,20 @@ export const VuiModal = (_a) => {
|
|
|
59
59
|
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
60
60
|
}, 0);
|
|
61
61
|
};
|
|
62
|
+
// Handle outside clicks, but ignore clicks on notifications.
|
|
63
|
+
const handleClickOutside = (event) => {
|
|
64
|
+
const target = event.target;
|
|
65
|
+
// Check if the click is within a notification.
|
|
66
|
+
if (target.closest("[data-awareness='notification']")) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
onCloseDelayed();
|
|
70
|
+
};
|
|
62
71
|
const containerClasses = classNames("vuiModalContainer", {
|
|
63
72
|
"vuiModalContainer-isLoaded": showTransition
|
|
64
73
|
});
|
|
65
74
|
const classes = classNames("vuiModal", `vuiModal--${color}`, `vuiModal--${size}`, className);
|
|
66
|
-
return (_jsx(VuiPortal, { children: (isOpen || isContentVisible || showTransition) && (_jsx(VuiScreenBlock, Object.assign({ type: "modal", isHidden: !isOpen }, { children: _jsx(FocusOn, Object.assign({ onEscapeKey: onCloseDelayed, onClickOutside: canClickOutsideToClose ?
|
|
75
|
+
return (_jsx(VuiPortal, { children: (isOpen || isContentVisible || showTransition) && (_jsx(VuiScreenBlock, Object.assign({ type: "modal", isHidden: !isOpen }, { children: _jsx(FocusOn, Object.assign({ onEscapeKey: onCloseDelayed, onClickOutside: canClickOutsideToClose ? handleClickOutside : undefined,
|
|
67
76
|
// Enable manual focus return to work.
|
|
68
77
|
returnFocus: false,
|
|
69
78
|
// Enable focus on contents when it's open,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Column } from "../table/types";
|
|
3
|
+
import { SpansRow } from "./types";
|
|
4
|
+
type Props<T extends SpansRow> = {
|
|
5
|
+
idField: keyof T | ((row: T) => string);
|
|
6
|
+
parentField: keyof T | ((row: T) => string | null);
|
|
7
|
+
columns: Column<T>[];
|
|
8
|
+
rows: T[];
|
|
9
|
+
expandedIds: Set<string>;
|
|
10
|
+
onExpandedIdsChange: (ids: Set<string>) => void;
|
|
11
|
+
onExpand?: (row: T) => void | Promise<void>;
|
|
12
|
+
isLoadingChildren?: (row: T) => boolean;
|
|
13
|
+
isLoading?: boolean;
|
|
14
|
+
content?: React.ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
rowDecorator?: (row: T, depth: number) => React.HTMLAttributes<HTMLTableRowElement>;
|
|
17
|
+
indentSize?: number;
|
|
18
|
+
isHeaderSticky?: boolean;
|
|
19
|
+
fluid?: boolean;
|
|
20
|
+
"data-testid"?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare const VuiSpans: <T extends SpansRow>({ idField, parentField, columns, rows, expandedIds, onExpandedIdsChange, onExpand, isLoadingChildren, isLoading, content, className, rowDecorator, indentSize, isHeaderSticky, fluid, "data-testid": dataTestId }: Props<T>) => import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import { VuiFlexItem } from "../flex/FlexItem";
|
|
5
|
+
import { VuiSpinner } from "../spinner/Spinner";
|
|
6
|
+
import { VuiText } from "../typography/Text";
|
|
7
|
+
import { VuiTableContent } from "../table/TableContent";
|
|
8
|
+
import { buildAndFlattenSpans } from "./buildAndFlattenSpans";
|
|
9
|
+
import { VuiSpansRow } from "./SpansRow";
|
|
10
|
+
import { VuiSpansHeaderCell } from "./SpansHeaderCell";
|
|
11
|
+
import { VuiSpansLoadingRow } from "./SpansLoadingRow";
|
|
12
|
+
const DEFAULT_INDENT_SIZE = 16;
|
|
13
|
+
const extractId = (row, idField) => {
|
|
14
|
+
return typeof idField === "function" ? idField(row) : row[idField];
|
|
15
|
+
};
|
|
16
|
+
const extractParentId = (row, parentField) => {
|
|
17
|
+
if (typeof parentField === "function")
|
|
18
|
+
return parentField(row);
|
|
19
|
+
const value = row[parentField];
|
|
20
|
+
return value === undefined || value === null ? null : value;
|
|
21
|
+
};
|
|
22
|
+
export const VuiSpans = ({ idField, parentField, columns, rows, expandedIds, onExpandedIdsChange, onExpand, isLoadingChildren, isLoading, content, className, rowDecorator, indentSize = DEFAULT_INDENT_SIZE, isHeaderSticky, fluid, "data-testid": dataTestId }) => {
|
|
23
|
+
const [internalLoadingIds, setInternalLoadingIds] = useState(new Set());
|
|
24
|
+
// Tracks pending fetches so we ignore stale resolutions (e.g. user collapses
|
|
25
|
+
// mid-fetch, then re-expands — the original promise should not clear loading
|
|
26
|
+
// state for the new attempt).
|
|
27
|
+
const fetchTokensRef = useRef(new Map());
|
|
28
|
+
const getId = useCallback((row) => extractId(row, idField), [idField]);
|
|
29
|
+
const getParentId = useCallback((row) => extractParentId(row, parentField), [parentField]);
|
|
30
|
+
const flatSpans = useMemo(() => buildAndFlattenSpans(rows, expandedIds, getId, getParentId), [rows, expandedIds, getId, getParentId]);
|
|
31
|
+
const columnCount = columns.length;
|
|
32
|
+
const handleToggle = useCallback((row, id, hasChildren, hasLoadedChildren) => {
|
|
33
|
+
var _a;
|
|
34
|
+
const isCurrentlyExpanded = expandedIds.has(id);
|
|
35
|
+
const nextExpandedIds = new Set(expandedIds);
|
|
36
|
+
if (isCurrentlyExpanded) {
|
|
37
|
+
nextExpandedIds.delete(id);
|
|
38
|
+
onExpandedIdsChange(nextExpandedIds);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
nextExpandedIds.add(id);
|
|
42
|
+
onExpandedIdsChange(nextExpandedIds);
|
|
43
|
+
// Only fire the consumer fetch when the row claims to have children
|
|
44
|
+
// (`hasChildren`) but none are present in the rows list yet.
|
|
45
|
+
const needsFetch = hasChildren && !hasLoadedChildren && Boolean(onExpand);
|
|
46
|
+
if (!needsFetch)
|
|
47
|
+
return;
|
|
48
|
+
const pendingFetchTokens = fetchTokensRef.current;
|
|
49
|
+
const currentFetchToken = ((_a = pendingFetchTokens.get(id)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
50
|
+
pendingFetchTokens.set(id, currentFetchToken);
|
|
51
|
+
setInternalLoadingIds((prev) => {
|
|
52
|
+
const nextLoadingIds = new Set(prev);
|
|
53
|
+
nextLoadingIds.add(id);
|
|
54
|
+
return nextLoadingIds;
|
|
55
|
+
});
|
|
56
|
+
const clearLoading = () => {
|
|
57
|
+
// Only clear if our token is still the latest — otherwise a newer
|
|
58
|
+
// fetch is in flight and owns the loading state.
|
|
59
|
+
if (pendingFetchTokens.get(id) !== currentFetchToken)
|
|
60
|
+
return;
|
|
61
|
+
setInternalLoadingIds((prev) => {
|
|
62
|
+
const nextLoadingIds = new Set(prev);
|
|
63
|
+
nextLoadingIds.delete(id);
|
|
64
|
+
return nextLoadingIds;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
Promise.resolve(onExpand(row)).finally(clearLoading);
|
|
68
|
+
}, [expandedIds, onExpandedIdsChange, onExpand]);
|
|
69
|
+
const classes = classNames("vuiSpans", className, {
|
|
70
|
+
"vuiSpans--fluid": fluid
|
|
71
|
+
});
|
|
72
|
+
let tbodyContent;
|
|
73
|
+
if (content) {
|
|
74
|
+
tbodyContent = _jsx(VuiTableContent, Object.assign({ colSpan: columnCount }, { children: content }));
|
|
75
|
+
}
|
|
76
|
+
else if (isLoading) {
|
|
77
|
+
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" }) }) }))] })));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
tbodyContent = flatSpans.map((flat, rowIndex) => {
|
|
81
|
+
const { row, id, depth, hasChildren, hasLoadedChildren, posInSet, setSize } = flat;
|
|
82
|
+
const isExpanded = expandedIds.has(id);
|
|
83
|
+
const externalLoading = isLoadingChildren ? isLoadingChildren(row) : false;
|
|
84
|
+
const isLoadingRow = internalLoadingIds.has(id) || externalLoading;
|
|
85
|
+
const showLoadingRow = isExpanded && hasChildren && !hasLoadedChildren && isLoadingRow;
|
|
86
|
+
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));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
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) => {
|
|
90
|
+
const { name, width } = column;
|
|
91
|
+
const styles = width ? { width } : undefined;
|
|
92
|
+
return (_jsx("th", Object.assign({ role: "columnheader", style: styles }, { children: _jsx(VuiSpansHeaderCell, { column: column }) }), name));
|
|
93
|
+
}) })) })), _jsx("tbody", { children: tbodyContent })] })) })));
|
|
94
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { VuiFlexContainer } from "../flex/FlexContainer";
|
|
3
|
+
import { VuiFlexItem } from "../flex/FlexItem";
|
|
4
|
+
import { VuiSpinner } from "../spinner/Spinner";
|
|
5
|
+
import { VuiText } from "../typography/Text";
|
|
6
|
+
import { VuiTextColor } from "../typography/TextColor";
|
|
7
|
+
export const VuiSpansLoadingRow = ({ colSpan, depth, indentSize, message = "Loading…" }) => {
|
|
8
|
+
return (_jsx("tr", Object.assign({ className: "vuiSpansLoadingRow vuiSpansRow--inert" }, { children: _jsx("td", Object.assign({ colSpan: colSpan, className: "vuiSpansLoadingRow__cell" }, { children: _jsx("div", Object.assign({ className: "vuiSpansLoadingRow__inner", style: { paddingLeft: depth * indentSize } }, { children: _jsxs(VuiFlexContainer, Object.assign({ alignItems: "center", spacing: "xs" }, { children: [_jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiSpinner, { size: "xs" }) })), _jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiText, Object.assign({ size: "xs" }, { children: _jsx("p", { children: _jsx(VuiTextColor, Object.assign({ color: "subdued" }, { children: message })) }) })) }))] })) })) })) })));
|
|
9
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Column, Row } from "../table/types";
|
|
2
|
+
type Props<T> = {
|
|
3
|
+
row: T;
|
|
4
|
+
rowIndex: number;
|
|
5
|
+
columns: Column<T>[];
|
|
6
|
+
id: string;
|
|
7
|
+
depth: number;
|
|
8
|
+
indentSize: number;
|
|
9
|
+
posInSet: number;
|
|
10
|
+
setSize: number;
|
|
11
|
+
hasChildren: boolean;
|
|
12
|
+
isExpanded: boolean;
|
|
13
|
+
onToggle: () => void;
|
|
14
|
+
rowDecorator?: (row: T, depth: number) => React.HTMLAttributes<HTMLTableRowElement>;
|
|
15
|
+
};
|
|
16
|
+
export declare const VuiSpansRow: <T extends Row>({ row, rowIndex, columns, id, depth, indentSize, posInSet, setSize, hasChildren, isExpanded, onToggle, rowDecorator }: Props<T>) => import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import classNames from "classnames";
|
|
14
|
+
import { BiChevronDown, BiChevronRight } from "react-icons/bi";
|
|
15
|
+
import { VuiIcon } from "../icon/Icon";
|
|
16
|
+
import { VuiIconButton } from "../button/IconButton";
|
|
17
|
+
import { VuiSpansCell } from "./SpansCell";
|
|
18
|
+
// Cap visual indentation at this depth so the first column doesn't overflow
|
|
19
|
+
// for very deep traces. `aria-level` continues to reflect the true depth.
|
|
20
|
+
const MAX_VISUAL_DEPTH = 12;
|
|
21
|
+
export const VuiSpansRow = ({ row, rowIndex, columns, id, depth, indentSize, posInSet, setSize, hasChildren, isExpanded, onToggle, rowDecorator }) => {
|
|
22
|
+
var _a;
|
|
23
|
+
const decoratorAttrs = (_a = rowDecorator === null || rowDecorator === void 0 ? void 0 : rowDecorator(row, depth)) !== null && _a !== void 0 ? _a : {};
|
|
24
|
+
const { className: decoratorClassName } = decoratorAttrs, restDecoratorAttrs = __rest(decoratorAttrs, ["className"]);
|
|
25
|
+
const rowClassName = classNames("vuiSpansRow", decoratorClassName, {
|
|
26
|
+
"vuiSpansRow-isExpanded": hasChildren && isExpanded,
|
|
27
|
+
"vuiSpansRow--leaf": !hasChildren
|
|
28
|
+
});
|
|
29
|
+
const visualDepth = Math.min(depth, MAX_VISUAL_DEPTH);
|
|
30
|
+
const indentStyle = { paddingLeft: visualDepth * indentSize };
|
|
31
|
+
return (_jsx("tr", Object.assign({ role: "row", "aria-level": depth + 1, "aria-posinset": posInSet, "aria-setsize": setSize, "aria-expanded": hasChildren ? isExpanded : undefined, "data-row-id": id, className: rowClassName }, restDecoratorAttrs, { children: columns.map((column, columnIndex) => {
|
|
32
|
+
const { name, render, className: columnClassName, testId } = column;
|
|
33
|
+
const cellClasses = classNames("vuiSpansCellWrapper", columnClassName, {
|
|
34
|
+
"vuiSpansCellWrapper--first": columnIndex === 0,
|
|
35
|
+
"vuiSpansCellWrapper--truncate": column.truncate
|
|
36
|
+
});
|
|
37
|
+
const cellContent = render ? render(row, rowIndex) : row[name];
|
|
38
|
+
if (columnIndex === 0) {
|
|
39
|
+
return (_jsx("td", Object.assign({ role: "gridcell", className: cellClasses, "data-testid": typeof testId === "function" ? testId(row) : testId }, { children: _jsxs("div", Object.assign({ className: "vuiSpansCell__indent", style: indentStyle }, { children: [_jsx("div", Object.assign({ className: "vuiSpansCell__chevron" }, { children: hasChildren ? (_jsx(VuiIconButton, { icon: _jsx(VuiIcon, { children: isExpanded ? _jsx(BiChevronDown, {}) : _jsx(BiChevronRight, {}) }), size: "xs", color: "neutral", "aria-label": isExpanded ? "Collapse span" : "Expand span", onClick: onToggle, "data-testid": `spanExpandToggle-${id}` })) : (_jsx("span", { className: "vuiSpansCell__chevronPlaceholder", "aria-hidden": "true" })) })), _jsx(VuiSpansCell, { children: cellContent })] })) }), name));
|
|
40
|
+
}
|
|
41
|
+
return (_jsx("td", Object.assign({ role: "gridcell", className: cellClasses, "data-testid": typeof testId === "function" ? testId(row) : testId }, { children: _jsx(VuiSpansCell, { children: cellContent }) }), name));
|
|
42
|
+
}) })));
|
|
43
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
$spansChevronWidth: 24px;
|
|
2
|
+
|
|
3
|
+
.vuiSpansWrapper {
|
|
4
|
+
width: 100%;
|
|
5
|
+
position: relative;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.vuiSpans {
|
|
9
|
+
width: 100%;
|
|
10
|
+
table-layout: fixed;
|
|
11
|
+
|
|
12
|
+
thead {
|
|
13
|
+
border-bottom: 1px solid var(--vui-color-border-medium);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
tbody tr {
|
|
17
|
+
border-bottom: 1px solid var(--vui-color-border-light);
|
|
18
|
+
|
|
19
|
+
&:not(.vuiSpansRow--inert):hover {
|
|
20
|
+
background-color: rgba(var(--vui-color-light-shade-rgb), 0.25);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&:last-child {
|
|
24
|
+
border-bottom: 1px solid var(--vui-color-border-medium);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
th {
|
|
29
|
+
font-size: $fontSizeStandard;
|
|
30
|
+
font-weight: $fontWeightBold;
|
|
31
|
+
padding: $sizeXxs;
|
|
32
|
+
text-align: left;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
td {
|
|
36
|
+
font-size: $fontSizeStandard;
|
|
37
|
+
padding: $sizeXxs;
|
|
38
|
+
vertical-align: middle;
|
|
39
|
+
word-break: break-word;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.vuiSpans--fluid {
|
|
44
|
+
table-layout: auto;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// First column: depth-indent + chevron + content laid out in a row.
|
|
48
|
+
.vuiSpansCell__indent {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: $sizeXxs;
|
|
52
|
+
// padding-left set inline based on depth.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.vuiSpansCell__chevron {
|
|
56
|
+
flex: 0 0 auto;
|
|
57
|
+
width: $spansChevronWidth;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.vuiSpansCell__chevronPlaceholder {
|
|
64
|
+
display: inline-block;
|
|
65
|
+
width: $spansChevronWidth;
|
|
66
|
+
height: 1px; // Reserves horizontal space; height irrelevant.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.vuiSpansCell {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
flex: 1 1 auto;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.vuiSpansCellWrapper--truncate {
|
|
77
|
+
min-width: 0;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
text-overflow: ellipsis;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.vuiSpansHeaderCell {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
padding: $sizeXxs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sticky header — same treatment as VuiTable.
|
|
90
|
+
.vuiSpansStickyHeader {
|
|
91
|
+
position: sticky;
|
|
92
|
+
top: 0;
|
|
93
|
+
background-color: var(--vui-color-empty-shade);
|
|
94
|
+
z-index: 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Loading row injected under a parent during lazy fetch.
|
|
98
|
+
.vuiSpansLoadingRow {
|
|
99
|
+
background-color: rgba(var(--vui-color-light-shade-rgb), 0.15);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.vuiSpansLoadingRow__cell {
|
|
103
|
+
padding: $sizeXxs !important;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.vuiSpansLoadingRow__inner {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
// padding-left set inline based on depth.
|
|
110
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SpansRow } from "./types";
|
|
2
|
+
export type FlatSpan<T> = {
|
|
3
|
+
row: T;
|
|
4
|
+
id: string;
|
|
5
|
+
parentId: string | null;
|
|
6
|
+
depth: number;
|
|
7
|
+
hasChildren: boolean;
|
|
8
|
+
hasLoadedChildren: boolean;
|
|
9
|
+
posInSet: number;
|
|
10
|
+
setSize: number;
|
|
11
|
+
};
|
|
12
|
+
export type IdAccessor<T> = (row: T) => string;
|
|
13
|
+
export type ParentAccessor<T> = (row: T) => string | null;
|
|
14
|
+
export declare const buildAndFlattenSpans: <T extends SpansRow>(rows: T[], expandedIds: Set<string>, getId: IdAccessor<T>, getParentId: ParentAccessor<T>) => FlatSpan<T>[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Flattens a list of rows (linked by `parentId`) into a render-ordered, depth-tagged array,
|
|
2
|
+
// showing only visible nodes based on `expandedIds`. The render layer maps this 1:1 to <tr>s.
|
|
3
|
+
// Orphans (rows with missing parents) are dropped. Cycles are avoided using an ancestor set.
|
|
4
|
+
export const buildAndFlattenSpans = (rows, expandedIds, getId, getParentId) => {
|
|
5
|
+
// First pass: collect every known id so we can detect orphans below.
|
|
6
|
+
const allIds = new Set();
|
|
7
|
+
for (const row of rows) {
|
|
8
|
+
allIds.add(getId(row));
|
|
9
|
+
}
|
|
10
|
+
// Group rows by their parent id, with top-level rows under a sentinel key.
|
|
11
|
+
// The sentinel can't collide with a real id because it contains spaces.
|
|
12
|
+
const ROOT_KEY = " __VuiSpansRoot__ ";
|
|
13
|
+
const childrenByParent = new Map();
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
const parentId = getParentId(row);
|
|
16
|
+
// Drop orphans: parent id set but not present in the rows list.
|
|
17
|
+
if (parentId !== null && !allIds.has(parentId))
|
|
18
|
+
continue;
|
|
19
|
+
const key = parentId === null ? ROOT_KEY : parentId;
|
|
20
|
+
const list = childrenByParent.get(key);
|
|
21
|
+
if (list) {
|
|
22
|
+
list.push(row);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
childrenByParent.set(key, [row]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const result = [];
|
|
29
|
+
// Recursive depth-first walk. `ancestors` carries the chain of ids from the
|
|
30
|
+
// root down to (but not including) the current row, used to detect cycles.
|
|
31
|
+
const walk = (parentKey, parentId, depth, ancestors) => {
|
|
32
|
+
const siblings = childrenByParent.get(parentKey);
|
|
33
|
+
if (!siblings)
|
|
34
|
+
return;
|
|
35
|
+
siblings.forEach((row, index) => {
|
|
36
|
+
const id = getId(row);
|
|
37
|
+
// Break cycle: this id appears earlier in its own ancestor chain.
|
|
38
|
+
if (ancestors.has(id))
|
|
39
|
+
return;
|
|
40
|
+
const ownChildren = childrenByParent.get(id);
|
|
41
|
+
const ownChildCount = ownChildren ? ownChildren.length : 0;
|
|
42
|
+
const hasLoadedChildren = ownChildCount > 0;
|
|
43
|
+
// The chevron should appear if either we already have children to show
|
|
44
|
+
// or the row is hinting that lazily-loadable children exist.
|
|
45
|
+
const hasChildren = row.hasChildren || hasLoadedChildren;
|
|
46
|
+
result.push({
|
|
47
|
+
row,
|
|
48
|
+
id,
|
|
49
|
+
parentId,
|
|
50
|
+
depth,
|
|
51
|
+
hasChildren,
|
|
52
|
+
hasLoadedChildren,
|
|
53
|
+
posInSet: index + 1,
|
|
54
|
+
setSize: siblings.length
|
|
55
|
+
});
|
|
56
|
+
// Only descend if the user has expanded this row AND there's something
|
|
57
|
+
// loaded to descend into. The lazy-only case (`hasChildren` but
|
|
58
|
+
// `!hasLoadedChildren`) is handled by the loading row in the render
|
|
59
|
+
// layer, not here.
|
|
60
|
+
if (hasChildren && expandedIds.has(id) && ownChildCount > 0) {
|
|
61
|
+
const nextAncestors = new Set(ancestors);
|
|
62
|
+
nextAncestors.add(id);
|
|
63
|
+
walk(id, id, depth + 1, nextAncestors);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
walk(ROOT_KEY, null, 0, new Set());
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|