UNPKG

@itwin/presentation-hierarchies-react

Version:

React components based on `@itwin/presentation-hierarchies`

161 lines 12.1 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import "./TreeNodeRenderer.css"; import cx from "classnames"; import { forwardRef, useCallback, useEffect, useRef } from "react"; import { SvgFilter, SvgFilterHollow, SvgMore, SvgRemove } from "@itwin/itwinui-icons-react"; import { Anchor, ButtonGroup, DropdownMenu, Flex, IconButton, MenuItem, ProgressRadial, Text, TreeNode } from "@itwin/itwinui-react"; import { HierarchyNode } from "@itwin/presentation-hierarchies"; import { MAX_LIMIT_OVERRIDE } from "../internal/Utils.js"; import { isPresentationHierarchyNode } from "../TreeNode.js"; import { useLocalizationContext } from "./LocalizationContext.js"; /** * A component that renders `RenderedTreeNode` using the `TreeNode` component from `@itwin/itwinui-react`. * * @see `TreeRenderer` * @see https://itwinui.bentley.com/docs/tree * @public */ export const TreeNodeRenderer = forwardRef(({ node, expandNode, getIcon, getLabel, getSublabel, onFilterClick, onNodeClick, onNodeKeyDown, isSelected, isDisabled, actionButtonsClassName, getHierarchyLevelDetails, reloadTree, size, filterButtonsVisibility, getActions, ...treeNodeProps }, forwardedRef) => { const { localizedStrings } = useLocalizationContext(); if ("type" in node && node.type === "ChildrenPlaceholder") { return _jsx(PlaceholderNode, { ...treeNodeProps, ref: forwardedRef, size: size }); } if (isPresentationHierarchyNode(node)) { return (_jsx(HierarchyNodeRenderer, { ...treeNodeProps, ref: forwardedRef, node: node, expandNode: expandNode, getIcon: getIcon, getLabel: getLabel, getSublabel: getSublabel, onFilterClick: onFilterClick, onNodeClick: onNodeClick, onNodeKeyDown: onNodeKeyDown, isSelected: isSelected, isDisabled: isDisabled, actionButtonsClassName: actionButtonsClassName, getHierarchyLevelDetails: getHierarchyLevelDetails, filterButtonsVisibility: filterButtonsVisibility, getActions: getActions })); } if (node.type === "ResultSetTooLarge") { const hierarchyLevelDetails = getHierarchyLevelDetails?.(node.parentNodeId); const isFilterable = hierarchyLevelDetails?.hierarchyNode && !HierarchyNode.isGroupingNode(hierarchyLevelDetails.hierarchyNode) && hierarchyLevelDetails.hierarchyNode.supportsFiltering; return (_jsx(ResultSetTooLargeNode, { ...treeNodeProps, ref: forwardedRef, limit: node.resultSetSizeLimit, onOverrideLimit: hierarchyLevelDetails ? (limit) => hierarchyLevelDetails.setSizeLimit(limit) : undefined, onFilterClick: onFilterClick && hierarchyLevelDetails && isFilterable ? () => { onFilterClick(hierarchyLevelDetails); } : undefined })); } if (node.type === "NoFilterMatches") { return (_jsx(TreeNode, { ...treeNodeProps, ref: forwardedRef, label: localizedStrings.noFilteredChildren, isDisabled: true, onExpanded: /* c8 ignore next */ () => { } })); } const onRetry = reloadTree ? () => reloadTree({ parentNodeId: node.parentNodeId, state: "reset" }) : undefined; return (_jsx(TreeNode, { ...treeNodeProps, ref: forwardedRef, label: _jsx(ErrorNodeLabel, { message: node.message, onRetry: onRetry }), isDisabled: true, onExpanded: /* c8 ignore next */ () => { } })); }); TreeNodeRenderer.displayName = "TreeNodeRenderer"; const HierarchyNodeRenderer = forwardRef(({ node, expandNode, getIcon, getLabel, getSublabel, onFilterClick, onNodeClick, onNodeKeyDown, isSelected, isDisabled, actionButtonsClassName, getHierarchyLevelDetails, filterButtonsVisibility, getActions, ...treeNodeProps }, forwardedRef) => { const nodeRef = useRef(null); const ref = useMergedRefs(forwardedRef, nodeRef); return (_jsx(TreeNode, { ...treeNodeProps, ref: ref, isSelected: isSelected, isDisabled: isDisabled, className: cx(treeNodeProps.className, "stateless-tree-node", { filtered: node.isFiltered, selected: isSelected }), onClick: (event) => !isDisabled && onNodeClick?.(node, !isSelected, event), onKeyDown: (event) => { // Ignore if it is called on the element inside, e.g. checkbox or expander if (!isDisabled && event.target === nodeRef.current) { onNodeKeyDown?.(node, !isSelected, event); } }, onExpanded: (_, isExpanded) => { expandNode(node.id, isExpanded); }, icon: getIcon ? getIcon(node) : undefined, label: getLabel ? getLabel(node) : node.label, sublabel: getSublabel ? getSublabel(node) : undefined, // TODO: review if this is needed when horizontal scroll is enabled back after fix for issue: https://github.com/iTwin/iTwinUI/issues/2330 title: node.label, contentProps: { className: "stateless-tree-node-content" }, children: _jsx(TreeNodeActions, { node: node, getHierarchyLevelDetails: getHierarchyLevelDetails, onFilterClick: onFilterClick, filterButtonsVisibility: filterButtonsVisibility, actionButtonsClassName: actionButtonsClassName, getActions: getActions }) })); }); HierarchyNodeRenderer.displayName = "HierarchyNodeRenderer"; const PlaceholderNode = forwardRef(({ size, ...props }, forwardedRef) => { const { localizedStrings } = useLocalizationContext(); return (_jsx(TreeNode, { ...props, ref: forwardedRef, label: localizedStrings.loading, icon: _jsx(ProgressRadial, { size: "x-small", indeterminate: true, title: localizedStrings.loading, className: cx(props.className, { "stateless-tree-node-small-spinner": size === "small" }) }), onExpanded: /* c8 ignore next */ () => { } })); }); PlaceholderNode.displayName = "PlaceholderNode"; const ResultSetTooLargeNode = forwardRef(({ onFilterClick, onOverrideLimit, limit, ...props }, forwardedRef) => { return (_jsx(TreeNode, { ...props, ref: forwardedRef, className: "stateless-tree-node", label: _jsx(ResultSetTooLargeNodeLabel, { limit: limit, onFilterClick: onFilterClick, onOverrideLimit: onOverrideLimit }), onExpanded: /* c8 ignore next */ () => { }, isDisabled: true })); }); ResultSetTooLargeNode.displayName = "ResultSetTooLargeNode"; function ErrorNodeLabel({ message, onRetry }) { const { localizedStrings } = useLocalizationContext(); return (_jsxs(Flex, { flexDirection: "row", gap: "xs", title: message, alignItems: "start", children: [_jsx(Text, { children: message }), onRetry ? _jsx(Anchor, { onClick: onRetry, children: localizedStrings?.retry }) : null] })); } function TreeNodeActions({ node, getHierarchyLevelDetails, onFilterClick, filterButtonsVisibility, actionButtonsClassName, getActions, }) { const { localizedStrings } = useLocalizationContext(); const applyFilterButtonRef = useRef(null); const prevIsFiltered = useRef(node.isFiltered); useEffect(() => { // If the node is filtered, focus the apply filter button if (node.isFiltered && !prevIsFiltered.current) { applyFilterButtonRef.current?.focus(); } prevIsFiltered.current = node.isFiltered; }, [node.isFiltered]); const additionalButtons = getActions ? getActions(node) : []; const isClearFilterVisible = getHierarchyLevelDetails && node.isFiltered; const isFilterVisible = onFilterClick && node.isFilterable && (filterButtonsVisibility !== "hide" || node.isFiltered); const renderAdditionalActions = () => { if (additionalButtons.length === 1) { const button = additionalButtons[0]; return (_jsx(IconButton, { styleType: "borderless", size: "small", label: button.label, onClick: (e) => { e.stopPropagation(); button.onClick(); }, children: button.icon })); } if (additionalButtons.length > 1) { return (_jsx(DropdownMenu, { menuItems: (close) => additionalButtons.map((button, index) => (_jsx(MenuItem, { startIcon: button.icon, onClick: () => { button.onClick(); close(); }, children: button.label }, index))), children: _jsx(IconButton, { styleType: "borderless", size: "small", label: localizedStrings.more, onClick: (e) => e.stopPropagation(), children: _jsx(SvgMore, {}) }) })); } return null; }; return (_jsxs(ButtonGroup, { className: cx("action-buttons", actionButtonsClassName), children: [isClearFilterVisible ? (_jsx(IconButton, { styleType: "borderless", size: "small", label: localizedStrings.clearHierarchyLevelFilter, onClick: (e) => { e.stopPropagation(); getHierarchyLevelDetails(node.id)?.setInstanceFilter(undefined); applyFilterButtonRef.current?.focus(); }, children: _jsx(SvgRemove, {}) })) : null, isFilterVisible ? (_jsx(IconButton, { ref: applyFilterButtonRef, styleType: "borderless", size: "small", label: localizedStrings.filterHierarchyLevel, onClick: (e) => { e.stopPropagation(); const hierarchyLevelDetails = getHierarchyLevelDetails?.(node.id); hierarchyLevelDetails && onFilterClick(hierarchyLevelDetails); }, children: node.isFiltered ? _jsx(SvgFilter, {}) : _jsx(SvgFilterHollow, {}) })) : null, renderAdditionalActions()] })); } function ResultSetTooLargeNodeLabel({ onFilterClick, onOverrideLimit, limit }) { const { localizedStrings } = useLocalizationContext(); const supportsFiltering = !!onFilterClick; const supportsLimitOverride = !!onOverrideLimit && limit < MAX_LIMIT_OVERRIDE; const limitExceededMessage = createLocalizedMessage(supportsFiltering ? localizedStrings.resultLimitExceededWithFiltering : localizedStrings.resultLimitExceeded, limit, onFilterClick); const increaseLimitMessage = supportsLimitOverride ? createLocalizedMessage(supportsFiltering ? localizedStrings.increaseHierarchyLimitWithFiltering : localizedStrings.increaseHierarchyLimit, MAX_LIMIT_OVERRIDE, () => onOverrideLimit(MAX_LIMIT_OVERRIDE)) : { title: "", element: null }; const title = `${limitExceededMessage.title} ${increaseLimitMessage.title}`; return (_jsxs(Flex, { flexDirection: "column", gap: "3xs", title: title, alignItems: "start", children: [limitExceededMessage.element, increaseLimitMessage.element] })); } function createLocalizedMessage(message, limit, onClick) { const limitStr = limit.toLocaleString(undefined, { useGrouping: true }); const messageWithLimit = message.replace("{{limit}}", limitStr); const exp = new RegExp("<link>(.*)</link>"); const match = messageWithLimit.match(exp); if (!match) { return { title: messageWithLimit, element: (_jsx(Text, { as: "span", style: { whiteSpace: "normal" }, children: messageWithLimit })), }; } const [fullText, innerText] = match; const [textBefore, textAfter] = messageWithLimit.split(fullText); return { title: messageWithLimit.replace(fullText, innerText), element: (_jsxs("div", { children: [textBefore ? (_jsx(Text, { as: "span", style: { whiteSpace: "normal" }, children: textBefore })) : null, _jsx(Anchor, { style: { whiteSpace: "normal" }, underline: true, onClick: (e) => { e.stopPropagation(); onClick?.(); }, children: innerText }), textAfter ? (_jsx(Text, { as: "span", style: { whiteSpace: "normal" }, children: textAfter })) : null] })), }; } function useMergedRefs(...refs) { return useCallback((instance) => { refs.forEach((ref) => { if (typeof ref === "function") { ref(instance); } else if (ref) { ref.current = instance; } }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [...refs]); } //# sourceMappingURL=TreeNodeRenderer.js.map