iannbing / react-simple-tree-menu

A simple React tree menu component
MIT License
137 stars 48 forks source link

Choosing multiple tree items #188

Open ScriptyChris opened 3 years ago

ScriptyChris commented 3 years ago

Is it possible to select (and deselect) multiple tree items? I can't see any "multiple" alike option/param/prop to be set in neither of internal components config in documentation.

I haven't dug into this library code yet, but if such feature is not currently implemented, would it conceptually be as simple as changing activeKey, initialActiveKey and similar props that track active tree items from string to array along with adjusting logic accordingly?

I did a workaround, which in my opinion is dirty: component that uses <TreeMenu /> keeps track of selected tree items and highlights them outside of library's components control. Is it possible to be done in a simpler or cleaner way?

Demo: https://codepen.io/ScriptyChris/pen/jOMZmdV?editors=1010

Code:

function CategoriesTree() {
  const categoriesTreeRef = createRef();
  const treeMenuRef = createRef();
  const activeTreeNodes = new Map();

  const getCategoriesTree = () => {    
    const treeData = JSON.parse(`[{"key":"first-level-node-1","index":0,"label":"first level node 1"},{"key":"first-level-node-2","index":1,"label":"first level node 2","nodes":[{"key":"second-level-node-1","index":0,"label":"second level node 1"},{"key":"second-level-node-2","index":1,"label":"second level node 2"},{"key":"second-level-node-3","index":2,"label":"second level node 3"},{"key":"second-level-node-4","index":3,"label":"second level node 4"},{"key":"second-level-node-5","index":4,"label":"second level node 5"},{"key":"second-level-node-6","index":5,"label":"second level node 6"}]},{"key":"first-level-node-3","index":2,"label":"first level node 3"},{"key":"first-level-node-4","index":3,"label":"first level node 4"},{"key":"first-level-node-5","index":4,"label":"first level node 5"}]`);

    return (
      <TreeMenu
        data={treeData}
        onClickItem={(clickedItem) => toggleActiveTreeNode(clickedItem.level, clickedItem.index, clickedItem.label)}
        ref={treeMenuRef}
        />
    );
  };

  const toggleActiveTreeNode = (nodeLevel, nodeIndex, nodeLabel) => {
    const currentNodeKey = `${nodeLevel}-${nodeIndex}`;
    const isActiveTreeNode = activeTreeNodes.has(currentNodeKey);

    if (isActiveTreeNode) {
      activeTreeNodes.delete(currentNodeKey);
    } else {
      activeTreeNodes.set(currentNodeKey, nodeLabel);
    }

    // This is a dirty workaround, because 3rd-party TreeMenu component doesn't seem to support multi selection.
    [[currentNodeKey], ...activeTreeNodes].forEach(([key], iteration) => {
      const isCurrentNodeKey = iteration === 0;
      const [level, index] = key.split('-');
      const treeNodeLevelSelector = `.rstm-tree-item-level${level}`;

      const treeNodeDOM = categoriesTreeRef.current.querySelectorAll(treeNodeLevelSelector)[index];

      // "Force" DOM actions execution on elements controlled by React.
      requestAnimationFrame(() => {
        treeNodeDOM.classList.toggle('rstm-tree-item--active', !isCurrentNodeKey);
        treeNodeDOM.setAttribute('aria-pressed', !isCurrentNodeKey);
      });
    });
  };

  return (
    /*
      Attribute [ref] is used on whole component wrapper, because
      both useRef() hook and React.createRef() method don't seem to
      give reference to nested functional component's DOM elements, such as used TreeMenu.
    */
    <div ref={categoriesTreeRef}>
      {getCategoriesTree()}
    </div>
  );
}
iannbing commented 3 years ago

@ScriptyChris I would suggest create your own UI components instead of using the built-in UI components.

For example:

// note that you shouldn't use `activeKey` any more, and you need to create your own state
const [activeKeys, setActiveKeys] = useState([]);
const toggleItem = (key) => {
   setActiveKeys(keys => {
      if(keys.includes(key)) return keys.filter(activeKey => activeKey !== key);
      return [...keys, key];
   })
}

...

<TreeMenu
  data={treeData}
  onClickItem={({ key, label, ...props }) => {
    toggleItem(key);
  }}
    {({ items }) => (
          <ul>
            {items.map(props => (
              <li 
                   onClick={props.onClick}
                   style={{
                       backgroundColor: activeKeys.includes(props.key) ? 'orange' : 'transparent'
                   }}>{props.label}</li>
            ))}
          </ul>
    )}
</TreeMenu>

Note: I didn't actually run the above code but just demonstrate how it might work 🙂

ScriptyChris commented 3 years ago

Thanks for answer, @iannbing. I assume that above approach is cleaner workaround than i did, but it's still workaround, since this library doesn't support multi selection?

iannbing commented 3 years ago

Yes, multi-selection is not a built-in feature. But, this component is fully customizable (and this is a perfect example).

The activeKey prop was designed for supporting keyboard browsing. To support multi-selection, it will require a separate state. I'd see this as a feature request 🙂