LicenseDialog: make license list keyboard accessible

This commit is contained in:
David Lechner
2022-03-19 21:20:02 -05:00
parent 1802bcf796
commit 7abdcf85e7
4 changed files with 75 additions and 34 deletions
+12 -11
View File
@@ -79,9 +79,13 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
);
};
type FileTreeItemData = TreeItemData & { fileName: string };
type FileTreeItem = TreeItem<FileTreeItemData>;
type ActionButtonGroupProps = {
/** The name of the file (displayed to user) */
item: TreeItem<TreeItemData>;
item: TreeItem;
};
const FileActionButtonGroup: React.VoidFunctionComponent<ActionButtonGroupProps> = ({
@@ -236,7 +240,7 @@ const FileTree: React.VFC = () => {
obj[index] = {
index,
data: {
label: fileName,
fileName,
icon: 'document',
secondaryLabel: (
<TreeItemContext.Consumer>
@@ -251,19 +255,16 @@ const FileTree: React.VFC = () => {
{
[rootItemIndex]: {
index: rootItemIndex,
data: { label: '/' },
data: { fileName: '/' },
hasChildren: true,
children: debouncedFileNames.map((n) => `/${n}`),
},
} as Record<TreeItemIndex, TreeItem<TreeItemData>>,
} as Record<TreeItemIndex, FileTreeItem>,
),
[debouncedFileNames],
);
const getItemTitle = useCallback(
(item: TreeItem<TreeItemData>) => item.data.label,
[],
);
const getItemTitle = useCallback((item: FileTreeItem) => item.data.fileName, []);
const [renameFileName, setRenameFileName] = useState('');
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
@@ -328,7 +329,7 @@ const FileTree: React.VFC = () => {
);
// override default renderRenameInput since we have a separate rename dialog
const renderRenameInput = useCallback<typeof renderers.renderRenameInput>(
const renderRenameInput = useCallback(
({ item }) => (
<span className={[Classes.TREE_NODE_LABEL, Classes.TEXT_MUTED].join(' ')}>
{getItemTitle(item)}
@@ -338,7 +339,7 @@ const FileTree: React.VFC = () => {
);
const handleStartRenamingItem = useCallback(
(item: TreeItem<TreeItemData>) => {
(item: FileTreeItem) => {
// we are ignoring most of the props since we are opening a dialog
// instead of using an inline input and button
setRenameFileName(getItemTitle(item));
@@ -380,7 +381,7 @@ const FileTree: React.VFC = () => {
);
return (
<ControlledTreeEnvironment<TreeItemData>
<ControlledTreeEnvironment<FileTreeItemData>
{...renderers}
renderTreeContainer={renderTreeContainer}
renderRenameInput={renderRenameInput}
+59 -18
View File
@@ -10,20 +10,25 @@ import {
Dialog,
NonIdealState,
Spinner,
Tree,
TreeEventHandler,
TreeNodeInfo,
} from '@blueprintjs/core';
import { useI18n } from '@shopify/react-i18n';
import React, { useCallback, useMemo, useState } from 'react';
import {
ControlledTreeEnvironment,
Tree,
TreeItem,
TreeItemIndex,
TreeViewState,
} from 'react-complex-tree';
import { useFetch } from 'usehooks-ts';
import { appName } from '../app/constants';
import { TreeItemData, renderers } from '../utils/tree-renderer';
import { LicenseStringId } from './i18n';
import en from './i18n.en.json';
import './license.scss';
interface LicenseInfo {
interface LicenseInfo extends TreeItemData {
readonly name: string;
readonly version: string;
readonly author: string | undefined;
@@ -42,27 +47,52 @@ const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = ({
}) => {
const [i18n] = useI18n({ id: 'license', translations: { en }, fallback: en });
const { data, error } = useFetch<LicenseList>('static/oss-licenses.json');
const [selectedNode, setSelectedNode] = useState<string | undefined>(undefined);
const [focusedItem, setFocusedItem] = useState<TreeItemIndex>();
const [activeItem, setActiveItem] = useState<TreeItemIndex>();
const contents = useMemo(() => {
if (!data) {
return undefined;
}
return data.map<TreeNodeInfo<LicenseInfo>>((info, i) => ({
id: i,
label: info.name,
isSelected: info.name === selectedNode,
nodeData: info,
}));
}, [data, selectedNode]);
return data.reduce(
(obj, info, i) => {
obj[i] = {
index: i,
data: info,
};
const handleNodeClick = useCallback<TreeEventHandler<LicenseInfo>>(
(e) => {
setSelectedNode(e.nodeData?.name);
onItemClick(e.nodeData);
return obj;
},
{
root: {
index: 'root',
data: {} as LicenseInfo,
hasChildren: true,
children: data.map((_info, i) => i),
},
} as Record<TreeItemIndex, TreeItem<LicenseInfo>>,
);
}, [data]);
const handlePrimaryAction = useCallback(
(item: TreeItem<LicenseInfo>) => {
setActiveItem(item.index);
onItemClick(item.data);
},
[setSelectedNode, onItemClick],
[onItemClick],
);
const viewState = useMemo<TreeViewState>(
() => ({
'pb-license-list': {
focusedItem,
// REVISIT: it would be nice if there was an active item separate
// from using selected items.
selectedItems: activeItem === undefined ? undefined : [activeItem],
},
}),
[focusedItem, activeItem],
);
return (
@@ -76,7 +106,18 @@ const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = ({
)}
</NonIdealState>
) : (
<Tree contents={contents} onNodeClick={handleNodeClick} />
<ControlledTreeEnvironment<LicenseInfo>
{...renderers}
items={contents}
getItemTitle={(item) => item.data.name}
viewState={viewState}
canRename={false}
showLiveDescription={false}
onFocusItem={(item) => setFocusedItem(item.index)}
onPrimaryAction={handlePrimaryAction}
>
<Tree treeId="pb-license-list" rootItem="root" />
</ControlledTreeEnvironment>
)}
</div>
);
+3 -1
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
// Custom styling for the LicenseDialog control.
@@ -25,6 +25,8 @@
width: 25%;
flex-flow: column;
overflow: auto;
// to allow for focus outline
padding: 6px;
}
.pb-license-info {
+1 -4
View File
@@ -25,7 +25,6 @@ const cx = (...classNames: Array<string | undefined | false>): string =>
/** Node item data similar to blueprintsjs TreeNodeInfo */
export type TreeItemData = {
readonly label: string;
readonly icon?: IconName | MaybeElement;
readonly secondaryLabel?: string | MaybeElement;
};
@@ -36,9 +35,7 @@ export type TreeItemData = {
*/
export const TreeItemContext = createContext<TreeItem<TreeItemData>>({
index: '<default>',
data: {
label: '<default>',
},
data: {},
});
export const renderers: Omit<