mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
LicenseDialog: make license list keyboard accessible
This commit is contained in:
+12
-11
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<
|
||||
|
||||
Reference in New Issue
Block a user