grafana/public/app/features/search/hooks/useSearchKeyboardSelection.ts
Ryan McKinley 402b5ce4c6
SearchV2: support keyboard navigation (#49650)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
2022-06-13 09:10:19 -07:00

101 lines
3.1 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { Observable, Subject } from 'rxjs';
import { Field, locationUtil } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { QueryResponse } from '../service';
export function useKeyNavigationListener() {
const eventsRef = useRef(new Subject<React.KeyboardEvent>());
return {
keyboardEvents: eventsRef.current,
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {
switch (e.code) {
case 'ArrowDown':
case 'ArrowUp':
case 'ArrowLeft':
case 'ArrowRight':
case 'Enter':
eventsRef.current.next(e);
default:
// ignore
}
},
};
}
interface ItemSelection {
x: number;
y: number;
}
export function useSearchKeyboardNavigation(
keyboardEvents: Observable<React.KeyboardEvent>,
numColumns: number,
response: QueryResponse
): ItemSelection {
const highlightIndexRef = useRef<ItemSelection>({ x: 0, y: -1 });
const [highlightIndex, setHighlightIndex] = useState<ItemSelection>({ x: 0, y: -1 });
const urlsRef = useRef<Field>();
// Clear selection when the search results change
useEffect(() => {
urlsRef.current = response.view.fields.url;
highlightIndexRef.current.x = 0;
highlightIndexRef.current.y = -1;
setHighlightIndex({ ...highlightIndexRef.current });
}, [response]);
useEffect(() => {
const sub = keyboardEvents.subscribe({
next: (keyEvent) => {
switch (keyEvent?.code) {
case 'ArrowDown': {
highlightIndexRef.current.y++;
setHighlightIndex({ ...highlightIndexRef.current });
break;
}
case 'ArrowUp':
highlightIndexRef.current.y = Math.max(0, highlightIndexRef.current.y - 1);
setHighlightIndex({ ...highlightIndexRef.current });
break;
case 'ArrowRight': {
if (numColumns > 0) {
highlightIndexRef.current.x = Math.min(numColumns, highlightIndexRef.current.x + 1);
setHighlightIndex({ ...highlightIndexRef.current });
}
break;
}
case 'ArrowLeft': {
if (numColumns > 0) {
highlightIndexRef.current.x = Math.max(0, highlightIndexRef.current.x - 1);
setHighlightIndex({ ...highlightIndexRef.current });
}
break;
}
case 'Enter':
if (!urlsRef.current) {
break;
}
const idx = highlightIndexRef.current.x * numColumns + highlightIndexRef.current.y;
if (idx < 0) {
highlightIndexRef.current.x = 0;
highlightIndexRef.current.y = 0;
setHighlightIndex({ ...highlightIndexRef.current });
break;
}
const url = urlsRef.current.values?.get(idx) as string;
if (url) {
locationService.push(locationUtil.stripBaseFromUrl(url));
}
}
},
});
return () => sub.unsubscribe();
}, [keyboardEvents, numColumns]);
return highlightIndex;
}