Skip to content

Table - part of tree table node looks clickable but is not #12692

Open

Description

@mcoker

Here's a link to the example - https://www.patternfly.org/components/table#tree-table. There is also an example below that and another in the data-view extension - https://www.patternfly.org/extensions/data-view/table/#tree-table

Right now the .pf-v6-c-table__tree-view-main element that wraps the entire first part of the node row (toggle + checkbox + node text, and all the space around those things) has a cursor pointer, but only the toggle and checkbox are clickable. Clicking the text or area around the toggle/check (including to the left of them on nested rows) doesn't appear to do anything.

If you compare to the checkbox tree view component example, the entire rows in tree view are clickable because on a non-checkbox node, the whole row is wrapped in a <button>, and the whole row on checkbox nodes is wrapped in a <label> pointing to the checkbox.

Should there be parity between the two? cc @andrew-ronaldson

Also should we add a basic/non-checkbox tree table example? I tried removing the checkboxes from the current example and the text field wasn't clickable but I don't know if I did it correctly. Below is the code I used:

import { useState } from 'react';
import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper, TdProps } from '@patternfly/react-table';
import LeafIcon from '@patternfly/react-icons/dist/esm/icons/leaf-icon';
import RhUiFolderFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-fill-icon';
import RhUiFolderOpenFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-folder-open-fill-icon';

interface RepositoriesTreeNode {
  name: string;
  branches: string;
  pullRequests: string;
  workspaces: string;
  children?: RepositoriesTreeNode[];
}

export const TableTree: React.FunctionComponent = () => {
  // In real usage, this data would come from some external source like an API via props.
  const data: RepositoriesTreeNode[] = [
    {
      name: 'Repositories one',
      branches: 'Branch one',
      pullRequests: 'Pull request one',
      workspaces: 'Workplace one',
      children: [
        {
          name: 'Repositories two',
          branches: 'Branch two',
          pullRequests: 'Pull request two',
          workspaces: 'Workplace two',
          children: [
            {
              name: 'Repositories three',
              branches: 'Branch three',
              pullRequests: 'Pull request three',
              workspaces: 'Workplace three'
            },
            {
              name: 'Repositories four',
              branches: 'Branch four',
              pullRequests: 'Pull request four',
              workspaces: 'Workplace four'
            }
          ]
        },
        {
          name: 'Repositories five',
          branches: 'Branch five',
          pullRequests: 'Pull request five',
          workspaces: 'Workplace five'
        },
        {
          name: 'Repositories six',
          branches: 'Branch six',
          pullRequests: 'Pull request six',
          workspaces: 'Workplace six'
        }
      ]
    },
    {
      name: 'Repositories seven',
      branches: 'Branch seven',
      pullRequests: 'Pull request seven',
      workspaces: 'Workplace seven',
      children: [
        {
          name: 'Repositories eight',
          branches: 'Branch eight',
          pullRequests: 'Pull request eight',
          workspaces: 'Workplace eight'
        }
      ]
    },
    {
      name: 'Repositories nine',
      branches: 'Branch nine',
      pullRequests: 'Pull request nine',
      workspaces: 'Workplace nine'
    }
  ];

  const columnNames = {
    name: 'Repositories',
    branches: 'Branches',
    prs: 'Pull requests',
    workspaces: 'Workspaces'
  };

  const [expandedNodeNames, setExpandedNodeNames] = useState<string[]>(['Repositories one']);
  const [expandedDetailsNodeNames, setExpandedDetailsNodeNames] = useState<string[]>([]);

  /** 
    Recursive function which flattens the data into an array of flattened TreeRowWrapper components
    params: 
      - nodes - array of a single level of tree nodes
      - level - number representing how deeply nested the current row is
      - posinset - position of the row relative to this row's siblings
      - currentRowIndex - position of the row relative to the entire table
      - isHidden - defaults to false, true if this row's parent is expanded
  */
  const renderRows = (
    [node, ...remainingNodes]: RepositoriesTreeNode[],
    level = 1,
    posinset = 1,
    rowIndex = 0,
    isHidden = false
  ): React.ReactNode[] => {
    if (!node) {
      return [];
    }
    const isExpanded = expandedNodeNames.includes(node.name);
    const isDetailsExpanded = expandedDetailsNodeNames.includes(node.name);
    let icon = <LeafIcon />;
    if (node.children) {
      icon = isExpanded ? <RhUiFolderOpenFillIcon /> : <RhUiFolderFillIcon />;
    }

    const treeRow: TdProps['treeRow'] = {
      onCollapse: () =>
        setExpandedNodeNames((prevExpanded) => {
          const otherExpandedNodeNames = prevExpanded.filter((name) => name !== node.name);
          return isExpanded ? otherExpandedNodeNames : [...otherExpandedNodeNames, node.name];
        }),
      onToggleRowDetails: () =>
        setExpandedDetailsNodeNames((prevDetailsExpanded) => {
          const otherDetailsExpandedNodeNames = prevDetailsExpanded.filter((name) => name !== node.name);
          return isDetailsExpanded ? otherDetailsExpandedNodeNames : [...otherDetailsExpandedNodeNames, node.name];
        }),
      rowIndex,
      props: {
        isExpanded,
        isDetailsExpanded,
        isHidden,
        'aria-level': level,
        'aria-posinset': posinset,
        'aria-setsize': node.children ? node.children.length : 0,
        icon
      }
    };

    const childRows =
      node.children && node.children.length
        ? renderRows(node.children, level + 1, 1, rowIndex + 1, !isExpanded || isHidden)
        : [];

    return [
      <TreeRowWrapper key={node.name} row={{ props: treeRow.props }}>
        <Td dataLabel={columnNames.name} treeRow={treeRow}>
          {node.name}
        </Td>
        <Td dataLabel={columnNames.branches}>{node.branches}</Td>
        <Td dataLabel={columnNames.prs}>{node.pullRequests}</Td>
        <Td dataLabel={columnNames.workspaces}>{node.workspaces}</Td>
      </TreeRowWrapper>,
      ...childRows,
      ...renderRows(remainingNodes, level, posinset + 1, rowIndex + 1 + childRows.length, isHidden)
    ];
  };

  return (
    <Table isTreeTable aria-label="Tree table">
      <Thead>
        <Tr>
          <Th width={40}>{columnNames.name}</Th>
          <Th>{columnNames.branches}</Th>
          <Th>{columnNames.prs}</Th>
          <Th>{columnNames.workspaces}</Th>
        </Tr>
      </Thead>
      <Tbody>{renderRows(data)}</Tbody>
    </Table>
  );
};

Jira Issue: PF-4745

Activity

  1. changed the title [-]Table - tree table looks clickable but is not[/-] [+]Table - part of tree table node looks clickable but is not[/+] on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions