mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
4.53k stars 1.32k forks source link

[TreeView] Missing exports: TreeItemContent and TreeViewContext #10242

Closed tujger closed 1 year ago

tujger commented 1 year ago

Duplicates

Latest version

Summary 💡

TreeItemContent and TreeViewContext were available in alpha.142, and are not available in x-tree-view. Please return these exports. Thank you!

Examples 🌈

No response

Motivation 🔦

No response

Order ID 💳 (optional)

No response

flaviendelangle commented 1 year ago

Indeed for TreeVIewContent, I'll add it on the next release, thanks for the feedback!

For TreeViewContext, could you give me a working example using it? For me it was not exported, and since I'm doing major breaking changes on it, knowing if it's public or private is important :+1:

tujger commented 1 year ago

We're overriding TreeItem with customization, so both components are necessary for. Then please provide the public version of it - I hope it's possible. There are fields we use:

    const {
            icons: contextIcons = {},
            focus,
            isExpanded,
            isFocused,
            isSelected,
            isDisabled,
            multiSelect,
            disabledItemsFocusable,
            mapFirstChar,
            unMapFirstChar,
            registerNode,
            unregisterNode,
            treeId,
        } = React.useContext(TreeViewContext)

Thank you!

flaviendelangle commented 1 year ago

Could you provide a working reproduction, this Codesandbox template might be a good starting point.

Because as far as I know the context is not public in the lab.

tujger commented 1 year ago

It's not easy, the component is implemented along many other components within our package. It tries to represent foldable table rows based on TreeItem.

https://github.com/mui/mui-x/assets/12112326/20f528d6-3f3f-4e4b-94e7-669b660f180a

flaviendelangle commented 1 year ago

In that case, could you show me how you import the context ?

tujger commented 1 year ago
import TreeViewContext from '@mui/lab/TreeView/TreeViewContext';

(it was working...)

const ComponentTreeItem = React.forwardRef(function TreeItem(inProps, ref) {
    const { branch, level, header, path, label: labelsGiven, labels } = inProps;
    const props = useThemeProps({ props: inProps, name: 'MuiTreeItem' });
    const {
        children,
        className,
        collapseIcon,
        ContentComponent = TreeItemContent,
        ContentProps,
        endIcon,
        expandIcon,
        disabled: disabledProp,
        icon,
        id: idProp,
        label,
        nodeId,
        onClick,
        onMouseDown,
        TransitionComponent,
        TransitionProps,
        ...other
    } = props;
    const {
        icons: contextIcons = {},
        focus,
        isExpanded,
        isFocused,
        isSelected,
        isDisabled,
        multiSelect,
        disabledItemsFocusable,
        mapFirstChar,
        unMapFirstChar,
        registerNode,
        unregisterNode,
        treeId,
    } = React.useContext(TreeViewContext);
    let id = null;

    if (idProp != null) {
        id = idProp;
    } else if (treeId && nodeId) {
        id = `${treeId}-${nodeId}`;
    }

    const [treeitemElement, setTreeitemElement] = React.useState(null);
    const contentRef = React.useRef(null);
    const handleRef = useForkRef(setTreeitemElement, ref);

    const descendant = React.useMemo(
        () => ({
            element: treeitemElement,
            id: nodeId,
        }),
        [nodeId, treeitemElement],
    );

    const { index, parentId } = useDescendant(descendant);

    const expandable = Boolean(Array.isArray(children) ? children.length : children);
    const expanded = isExpanded ? isExpanded(nodeId) : false;
    const focused = isFocused ? isFocused(nodeId) : false;
    const selected = isSelected ? isSelected(nodeId) : false;
    const disabled = isDisabled ? isDisabled(nodeId) : false;

    const ownerState = {
        ...props,
        expanded,
        focused,
        selected,
        disabled,
    };

    const classes = useUtilityClasses(ownerState);

    let displayIcon;
    let expansionIcon;

    if (expandable) {
        if (!expanded) {
            expansionIcon = expandIcon || contextIcons.defaultExpandIcon;
        } else {
            expansionIcon = collapseIcon || contextIcons.defaultCollapseIcon;
        }
    }

    if (expandable) {
        displayIcon = contextIcons.defaultParentIcon;
    } else {
        displayIcon = endIcon || contextIcons.defaultEndIcon;
    }

    React.useEffect(() => {
        // On the first render a node's index will be -1. We want to wait for the real index.
        if (registerNode && unregisterNode && index !== -1) {
            registerNode({
                id: nodeId,
                idAttribute: id,
                index,
                parentId,
                expandable,
                disabled: disabledProp,
            });

            return () => {
                unregisterNode(nodeId);
            };
        }

        return undefined;
    }, [registerNode, unregisterNode, parentId, index, nodeId, expandable, disabledProp, id]);

    React.useEffect(() => {
        if (mapFirstChar && unMapFirstChar && label) {
            mapFirstChar(nodeId, contentRef.current?.textContent.substring(0, 1).toLowerCase());

            return () => {
                unMapFirstChar(nodeId);
            };
        }
        return undefined;
    }, [mapFirstChar, unMapFirstChar, nodeId, label]);

    let ariaSelected;
    if (multiSelect) {
        ariaSelected = selected;
    } else if (selected) {
        /* single-selection trees unset aria-selected on un-selected items.
         *
         * If the tree does not support multiple selection, aria-selected
         * is set to true for the selected node and it is not present on any other node in the tree.
         * Source: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
         */
        ariaSelected = true;
    }

    function handleFocus(event) {
        // DOM focus stays on the tree which manages focus with aria-activedescendant
        if (event.target === event.currentTarget) {
            ownerDocument(event.target).getElementById(treeId).focus({ preventScroll: true });
        }

        const unfocusable = !disabledItemsFocusable && disabled;
        if (!focused && event.currentTarget === event.target && !unfocusable) {
            focus(event, nodeId);
        }
    }

    const row = React.useMemo(() => {//TODO: works for now
        if (path.length > 1 && typeof path !== 'string') {
            const level = path.length - 1;
            const keyRow = [...Array(header.length).keys()].map((item, index) => index === level ? label : "")
            return { ...keyRow }
        } else if (typeof path === 'string') {
            const keyRow = [...Array(header.length).keys()].map((item, index) => index === branch.length - 1 ? label : "")
            return { ...keyRow }
        } else {
            const keyRow = [...Array(header.length).keys()].map((item, index) => index === 0 ? label : "")
            return { ...keyRow }
        }
    }, [path.length, header])

    const isLeaf = branch instanceof Array || false;
    if (isLeaf) {
        return <TableRow
            className={selected ? "SELECTED_LEAF" : ""}
            role="treeitem"
            aria-expanded={expandable ? expanded : null}
            aria-selected={ariaSelected}
            aria-disabled={disabled || null}
            ref={handleRef}
            id={id}
            tabIndex={-1}
            {...other}
            ownerState={ownerState}
            onFocus={handleFocus}
            {...props}
            onClick={onClick}
            row={labels}
        >
            {labelsGiven}
        </TableRow>
    }

    return (<>
        <TableRow
            // className={clsx(classes.root, className)}
            role="treeitem"
            aria-expanded={expandable ? expanded : null}
            aria-selected={ariaSelected}
            aria-disabled={disabled || null}
            ref={handleRef}
            id={id}
            tabIndex={-1}
            {...other}
            ownerState={ownerState}
            onFocus={handleFocus}
        >
            <TreeTableItemContent
                as={ContentComponent}
                ref={contentRef}
                classes={{
                    root: classes.content,
                    expanded: classes.expanded,
                    selected: classes.selected,
                    focused: classes.focused,
                    disabled: classes.disabled,
                    iconContainer: classes.iconContainer,
                    label: classes.label,
                }}
                label={label}
                nodeId={nodeId}
                onClick={onClick}
                onMouseDown={onMouseDown}
                icon={icon}
                expansionIcon={expansionIcon}
                displayIcon={displayIcon}
                ownerState={ownerState}
                {...ContentProps}
                row={row}
            />
        </TableRow>
        {expanded && <DescendantProvider id={nodeId}>
            {children}
        </DescendantProvider>}
    </>
    );
});
flaviendelangle commented 1 year ago
import TreeViewContext from '@mui/lab/TreeView/TreeViewContext';

These imports are not public and can break with some bundlers. We only guarantee the imports of depth 0 and 1.

But you can do the same thing:

import { TreeViewContext } from "@mui/x-tree-view/TreeView/TreeViewContext";

See this example

tujger commented 1 year ago

I tried. But it fails on start now reporting the unresolved import. Despite it was not with '@mui/lab'.

flaviendelangle commented 1 year ago

Did you change for a named import like in my code example?

tujger commented 1 year ago

Ow, I missed {}. Yes, this way works, even for both components. Thank you! But as you said the internal API can be changed, so now TreeViewContext doesn't bring treeId, and logic is broken :)

flaviendelangle commented 1 year ago

The treeId should still be defined https://github.com/mui/mui-x/blob/51f831e31772b1eda05c62e8c4df292624669bac/packages/x-tree-view/src/TreeView/TreeView.tsx#L856

tujger commented 1 year ago

You're right! Actually, everything is fixed changing:

import TreeView from "@mui/lab/TreeView" -> import {TreeView} from "@mui/x-tree-view/TreeView";
import TreeItem from '@mui/lab/TreeItem' -> import {TreeItem} from '@mui/x-tree-view/TreeItem';

and so on. Thank you!

tujger commented 1 year ago

Sorry, it's me again..

But you can do the same thing:

import { TreeViewContext } from "@mui/x-tree-view/TreeView/TreeViewContext";

See this example

I set the following, and it was working for several weeks:

import {DescendantProvider, useDescendant} from "@mui/x-tree-view/TreeView/descendants";
import {TreeViewContext} from "@mui/x-tree-view/TreeView/TreeViewContext";

Suddenly, it's started reporting:

image

Returning to previous versions (even whole package.json) doesn't help.

Also, there is the error on the reference you've provided.

LukasTy commented 1 year ago

As mentioned by @flaviendelangle - you are trying to use an import deeper than 2nd level. At that point, you assume the responsibility and risk of using an API that we do not guarantee the stability of, especially during pre-release stages.

With the latest release, there have been a lot of changes to the TreeView internals, and the mentioned export can be accessed via: @mui/x-tree-view/internals/TreeViewProvider/TreeViewContext as of 6.0.0-alpha.4.


Once again, please be aware that you are using undocumented API, and you assume the risk of adjusting to any breaking changes that happen to it.