-
-
+
+
+
+
+
+
+
);
};
diff --git a/src/explorer/explorer.scss b/src/explorer/explorer.scss
new file mode 100644
index 00000000..87a3a224
--- /dev/null
+++ b/src/explorer/explorer.scss
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+@import '../variables.scss';
+
+.pb-explorer-file-tree {
+ // to allow for focus outline
+ padding: 6px;
+}
+
+// reveal file action buttons on hover
+.#{$ns}-tree-node-content:not(:hover) .pb-explorer-file-action-button-group {
+ display: none;
+}
diff --git a/src/explorer/i18n.en.json b/src/explorer/i18n.en.json
index bca57c69..c89fa76a 100644
--- a/src/explorer/i18n.en.json
+++ b/src/explorer/i18n.en.json
@@ -5,6 +5,22 @@
"importTooltip": "Import a file",
"addNewTooltip": "Create a new file"
},
+ "tree": {
+ "label": "File Explorer",
+ "liveDescriptor": {
+ "intro": {
+ "accessibilityGuide": "Accessibility guide for tree {treeLabel}.",
+ "navigation": "Navigate the tree with the arrow keys. Start typing the name of a file to search for a file. Additional keybindings are available:",
+ "keybindings": {
+ "primaryAction": "{key} to open the file in the code editor",
+ "rename": "{key} to start renaming the focused file",
+ "export": "{key} to export the focused file",
+ "delete": "{key} to delete the focused file"
+ }
+ },
+ "searching": "Searching."
+ }
+ },
"treeItem": {
"deleteTooltip": "Delete {fileName}",
"exportTooltip": "Export {fileName}",
diff --git a/src/explorer/i18n.ts b/src/explorer/i18n.ts
index 8c7028dc..24c39c8a 100644
--- a/src/explorer/i18n.ts
+++ b/src/explorer/i18n.ts
@@ -7,6 +7,14 @@ export enum ExplorerStringId {
HeaderExportAllTooltip = 'explorer.header.exportAllTooltip',
HeaderImportTooltip = 'explorer.header.importTooltip',
HeaderAddNewTooltip = 'explorer.header.addNewTooltip',
+ TreeLabel = 'explorer.tree.label',
+ TreeLiveDescriptorIntroAccessibilityGuide = 'explorer.tree.liveDescriptor.intro.accessibilityGuide',
+ TreeLiveDescriptorIntroNavigation = 'explorer.tree.liveDescriptor.intro.navigation',
+ TreeLiveDescriptorIntroKeybindingsPrimaryAction = 'explorer.tree.liveDescriptor.intro.keybindings.primaryAction',
+ TreeLiveDescriptorIntroKeybindingsRename = 'explorer.tree.liveDescriptor.intro.keybindings.rename',
+ TreeLiveDescriptorIntroKeybindingsExport = 'explorer.tree.liveDescriptor.intro.keybindings.export',
+ TreeLiveDescriptorIntroKeybindingsDelete = 'explorer.tree.liveDescriptor.intro.keybindings.delete',
+ TreeLiveDescriptorSearching = 'explorer.tree.liveDescriptor.searching',
TreeItemDeleteTooltip = 'explorer.treeItem.deleteTooltip',
TreeItemExportTooltip = 'explorer.treeItem.exportTooltip',
TreeItemRenameTooltip = 'explorer.treeItem.renameTooltip',
diff --git a/src/index.scss b/src/index.scss
index 3bd3dd14..843a57a0 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -5,6 +5,7 @@
@import '~normalize.css';
@import '~@blueprintjs/core/src/blueprint.scss';
@import '~@blueprintjs/popover2/src/blueprint-popover2.scss';
+@import '~react-complex-tree/lib/style.css';
:root {
--pb-vh: 100vh;
@@ -105,3 +106,9 @@ a.#{$ns}-button {
.#{$ns}-input-group .#{$ns}-icon {
margin: 7px;
}
+
+// don't take up so much space when there is no caret
+.#{$ns}-tree-node-caret-none {
+ // $pt-grid-size / 2 matches padding on .#{$ns}-tree-node-conent
+ min-width: $pt-grid-size / 2;
+}
diff --git a/src/utils/tree-renderer.tsx b/src/utils/tree-renderer.tsx
new file mode 100644
index 00000000..2c4aea35
--- /dev/null
+++ b/src/utils/tree-renderer.tsx
@@ -0,0 +1,197 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+/***/
+// react-complex-tree renderers for bluetprintjs integration
+// based on https://github.com/lukasbach/react-complex-tree/blob/239fb0c5f49f3c24e307142fb3d7e828440c3f55/packages/blueprintjs-renderers/src/renderers.tsx
+// Copyright (c) 2021 Lukas Bach
+
+import {
+ Button,
+ Classes,
+ Colors,
+ FocusStyleManager,
+ Icon,
+ IconName,
+ InputGroup,
+ MaybeElement,
+} from '@blueprintjs/core';
+import React, { createContext } from 'react';
+import { TreeItem, TreeRenderProps } from 'react-complex-tree';
+
+/** Combines class names into a string. */
+const cx = (...classNames: Array
): string =>
+ classNames.filter((cn) => !!cn).join(' ');
+
+/** Node item data similar to blueprintsjs TreeNodeInfo */
+export type TreeItemData = {
+ readonly label: string;
+ readonly icon?: IconName | MaybeElement;
+ readonly secondaryLabel?: string | MaybeElement;
+};
+
+/**
+ * Tree item context that can be used to get a reference to the tree item in
+ * elements passed to TreeItemData.
+ */
+export const TreeItemContext = createContext>({
+ index: '',
+ data: {
+ label: '',
+ },
+});
+
+export const renderers: Omit<
+ Required>,
+ 'renderDraggingItem' | 'renderDraggingItemTitle' | 'renderLiveDescriptorContainer'
+> = {
+ renderTreeContainer: (props) => (
+
+ {props.children}
+
+ ),
+
+ renderItemsContainer: (props) => (
+
+ ),
+
+ renderItem: (props) => (
+
+
+
+ {props.item.hasChildren ? (
+ props.arrow
+ ) : (
+
+ )}
+
+ {props.title}
+ {props.item.data.secondaryLabel && (
+
+ {props.item.data.secondaryLabel}
+
+ )}
+
+ {props.context.isExpanded && props.children}
+
+
+ ),
+
+ renderItemArrow: (props) => (
+
+ ),
+
+ renderItemTitle: ({ title, context, info }) => {
+ if (!info.isSearching || !context.isSearchMatching || !info.search) {
+ return {title};
+ } else {
+ const startIndex = title.toLowerCase().indexOf(info.search.toLowerCase());
+ return (
+
+ {startIndex > 0 && {title.slice(0, startIndex)}}
+
+ {title.slice(startIndex, startIndex + info.search.length)}
+
+ {startIndex + info.search.length < title.length && (
+
+ {title.slice(startIndex + info.search.length, title.length)}
+
+ )}
+
+ );
+ }
+ },
+
+ renderDragBetweenLine: ({ draggingPosition, lineProps }) => (
+
+ ),
+
+ renderRenameInput: (props) => (
+
+ ),
+
+ renderSearchInput: (props) => (
+
+
+
+ ),
+
+ renderDepthOffset: 1,
+};
diff --git a/test/index.tsx b/test/index.tsx
index 05a79e91..df25d243 100644
--- a/test/index.tsx
+++ b/test/index.tsx
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
+import { HotkeysProvider } from '@blueprintjs/core';
import { I18nContext, I18nManager } from '@shopify/react-i18n';
import { RenderResult, render } from '@testing-library/react';
import React, { ReactElement } from 'react';
@@ -137,7 +138,9 @@ export const testRender = (
const result = render(
- {component}
+
+ {component}
+
,
);
diff --git a/yarn.lock b/yarn.lock
index b2820bf7..cad480e9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2152,6 +2152,7 @@ __metadata:
prettier: ^2.5.1
prop-types: ^15.8.1
react: ^16.13.1
+ react-complex-tree: ^1.1.4
react-dom: ^16.13.1
react-dropzone: ^12.0.4
react-monaco-editor: ^0.47.0
@@ -13638,6 +13639,13 @@ __metadata:
languageName: node
linkType: hard
+"react-complex-tree@npm:^1.1.4":
+ version: 1.1.4
+ resolution: "react-complex-tree@npm:1.1.4"
+ checksum: 11ed3d99f58e4bb594be7b64e599609e57e1ee244733c799fee595d096ce5596f9a884a51a222dde41a44fb4f105b253c13b26c59af60a36542ce34a619ce66e
+ languageName: node
+ linkType: hard
+
"react-dev-utils@npm:^11.0.3":
version: 11.0.4
resolution: "react-dev-utils@npm:11.0.4"