Files
grafana/public/app/core/components/NestedFolderPicker/hooks.ts
Ashley Harrison b164fa37e6 NestedFolderPicker: Implement keyboard handling (#71842)
* first attempt at keyboard handling

* rename some things, handle escape

* better

* almost working

* cleaner

* remove aria-label

* add some extra unit tests

* remove onMouseUp

* fix typo

* use a switch instead of if/else

* ensure lsit items are prefixed with an id unique to the picker

* extract keyboard interactions out into custom hook

* wrap handleCloseOverlay in useCallback

* use redux state instead of filtering items
2023-07-19 15:32:55 +01:00

95 lines
2.6 KiB
TypeScript

import React, { useCallback, useEffect, useState } from 'react';
import { DashboardsTreeItem } from 'app/features/browse-dashboards/types';
import { DashboardViewItem } from 'app/features/search/types';
import { getDOMId } from './NestedFolderList';
interface TreeInteractionProps {
tree: DashboardsTreeItem[];
handleCloseOverlay: () => void;
handleFolderSelect: (item: DashboardViewItem) => void;
handleFolderExpand: (uid: string, newOpenState: boolean) => Promise<void>;
idPrefix: string;
search: string;
visible: boolean;
}
export function useTreeInteractions({
tree,
handleCloseOverlay,
handleFolderSelect,
handleFolderExpand,
idPrefix,
search,
visible,
}: TreeInteractionProps) {
const [focusedItemIndex, setFocusedItemIndex] = useState(-1);
useEffect(() => {
if (visible) {
setFocusedItemIndex(-1);
}
}, [visible]);
useEffect(() => {
setFocusedItemIndex(0);
}, [search]);
useEffect(() => {
document
.getElementById(getDOMId(idPrefix, tree[focusedItemIndex]?.item.uid))
?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}, [focusedItemIndex, idPrefix, tree]);
const handleKeyDown = useCallback(
(ev: React.KeyboardEvent<HTMLInputElement>) => {
const foldersAreOpenable = !search;
switch (ev.key) {
// Expand/collapse folder on right/left arrow keys
case 'ArrowRight':
case 'ArrowLeft':
if (foldersAreOpenable) {
ev.preventDefault();
handleFolderExpand(tree[focusedItemIndex].item.uid, ev.key === 'ArrowRight');
}
break;
case 'ArrowUp':
if (focusedItemIndex > 0) {
ev.preventDefault();
setFocusedItemIndex(focusedItemIndex - 1);
}
break;
case 'ArrowDown':
if (focusedItemIndex < tree.length - 1) {
ev.preventDefault();
setFocusedItemIndex(focusedItemIndex + 1);
}
break;
case 'Enter':
ev.preventDefault();
const item = tree[focusedItemIndex].item;
if (item.kind === 'folder') {
handleFolderSelect(item);
}
break;
case 'Tab':
ev.stopPropagation();
handleCloseOverlay();
break;
case 'Escape':
ev.stopPropagation();
ev.preventDefault();
handleCloseOverlay();
break;
}
},
[focusedItemIndex, handleCloseOverlay, handleFolderExpand, handleFolderSelect, search, tree]
);
return {
focusedItemIndex,
handleKeyDown,
};
}