explorer: render file toolbar on hover

This improves performance by only rendering the toolbar for each file
when it is actually shown.
This commit is contained in:
David Lechner
2022-06-15 12:03:29 -05:00
committed by David Lechner
parent d891d349f6
commit a835934028
4 changed files with 75 additions and 58 deletions
+25 -13
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { cleanup } from '@testing-library/react';
import { cleanup, waitFor } from '@testing-library/react';
import React from 'react';
import { testRender, uuid } from '../../test';
import { FileMetadata } from '../fileStorage';
@@ -99,9 +99,12 @@ describe('tree item', () => {
jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]);
const [user, explorer, dispatch] = testRender(<Explorer />);
// NB: this button is intentionally not accessible (by role) since
// there is a keyboard shortcut.
const button = explorer.getByTitle('Duplicate test.file');
const treeItem = explorer.getByRole('treeitem', { name: 'test.file' });
await user.hover(treeItem);
const button = await waitFor(() =>
explorer.getByRole('button', { name: 'Duplicate test.file' }),
);
await user.click(button);
@@ -129,9 +132,12 @@ describe('tree item', () => {
jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]);
const [user, explorer, dispatch] = testRender(<Explorer />);
// NB: this button is intentionally not accessible (by role) since
// there is a keyboard shortcut.
const button = explorer.getByTitle('Rename test.file');
const treeItem = explorer.getByRole('treeitem', { name: 'test.file' });
await user.hover(treeItem);
const button = await waitFor(() =>
explorer.getByRole('button', { name: 'Rename test.file' }),
);
await user.click(button);
@@ -159,9 +165,12 @@ describe('tree item', () => {
jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]);
const [user, explorer, dispatch] = testRender(<Explorer />);
// NB: this button is intentionally not accessible (by role) since
// there is a keyboard shortcut.
const button = explorer.getByTitle('Export test.file');
const treeItem = explorer.getByRole('treeitem', { name: 'test.file' });
await user.hover(treeItem);
const button = await waitFor(() =>
explorer.getByRole('button', { name: 'Export test.file' }),
);
await user.click(button);
@@ -189,9 +198,12 @@ describe('tree item', () => {
jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]);
const [user, explorer, dispatch] = testRender(<Explorer />);
// NB: this button is intentionally not accessible (by role) since
// there is a keyboard shortcut.
const button = explorer.getByTitle('Delete test.file');
const treeItem = explorer.getByRole('treeitem', { name: 'test.file' });
await user.hover(treeItem);
const button = await waitFor(() =>
explorer.getByRole('button', { name: 'Delete test.file' }),
);
await user.click(button);
-1
View File
@@ -111,7 +111,6 @@ const FileActionButtonGroup: React.VoidFunctionComponent<ActionButtonGroupProps>
return (
<ButtonGroup
aria-hidden={true}
className="pb-explorer-file-tree-action-button-group"
minimal={true}
>
-5
View File
@@ -15,11 +15,6 @@
padding: unset;
}
// reveal file action toolbar on hover
.#{bp.$ns}-tree-node-content:not(:hover) &-action-button-group {
display: none;
}
// fix layout since we are nesting toolbar in button group
&-action-toolbar {
display: inline-flex;
+50 -39
View File
@@ -17,6 +17,7 @@ import {
} from '@blueprintjs/core';
import React, { createContext } from 'react';
import { TreeItem, TreeRenderProps } from 'react-complex-tree';
import { useBoolean } from 'usehooks-ts';
/** Combines class names into a string. */
const cx = (...classNames: Array<string | undefined | false>): string =>
@@ -62,48 +63,58 @@ export const renderers: Omit<
</ul>
),
renderItem: (props) => (
<TreeItemContext.Provider value={props.item}>
<li
className={cx(
Classes.TREE_NODE,
// TODO: include Classes.DISABLED if disabled
props.context.isExpanded && Classes.TREE_NODE_EXPANDED,
(props.context.isSelected || props.context.isDraggingOver) &&
Classes.TREE_NODE_SELECTED,
)}
onMouseDown={(e) => e.stopPropagation()}
{...props.context.itemContainerWithChildrenProps}
{...props.context.interactiveElementProps}
>
<div
renderItem: (props) => {
const {
value: isHover,
setTrue: setIsHoverTrue,
setFalse: setIsHoverFalse,
} = useBoolean(false);
return (
<TreeItemContext.Provider value={props.item}>
<li
className={cx(
Classes.TREE_NODE_CONTENT,
`${Classes.TREE_NODE_CONTENT}-${props.depth}`,
Classes.TREE_NODE,
// TODO: include Classes.DISABLED if disabled
props.context.isExpanded && Classes.TREE_NODE_EXPANDED,
(props.context.isSelected || props.context.isDraggingOver) &&
Classes.TREE_NODE_SELECTED,
)}
{...props.context.itemContainerWithoutChildrenProps}
onMouseDown={(e) => e.stopPropagation()}
onMouseEnter={setIsHoverTrue}
onMouseLeave={setIsHoverFalse}
{...props.context.itemContainerWithChildrenProps}
{...props.context.interactiveElementProps}
>
{props.item.hasChildren ? (
props.arrow
) : (
<span className={Classes.TREE_NODE_CARET_NONE} />
)}
<Icon
className={Classes.TREE_NODE_ICON}
icon={props.item.data.icon}
aria-hidden={true}
/>
{props.title}
{props.item.data.secondaryLabel && (
<span className={Classes.TREE_NODE_SECONDARY_LABEL}>
{props.item.data.secondaryLabel}
</span>
)}
</div>
{props.context.isExpanded && props.children}
</li>
</TreeItemContext.Provider>
),
<div
className={cx(
Classes.TREE_NODE_CONTENT,
`${Classes.TREE_NODE_CONTENT}-${props.depth}`,
)}
{...props.context.itemContainerWithoutChildrenProps}
>
{props.item.hasChildren ? (
props.arrow
) : (
<span className={Classes.TREE_NODE_CARET_NONE} />
)}
<Icon
className={Classes.TREE_NODE_ICON}
icon={props.item.data.icon}
aria-hidden={true}
/>
{props.title}
{props.item.data.secondaryLabel && isHover && (
<span className={Classes.TREE_NODE_SECONDARY_LABEL}>
{props.item.data.secondaryLabel}
</span>
)}
</div>
{props.context.isExpanded && props.children}
</li>
</TreeItemContext.Provider>
);
},
renderItemArrow: (props) => (
<Icon