useSearchKeyboardSelection.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { useEffect, useRef, useState } from 'react';
  2. import { Observable, Subject } from 'rxjs';
  3. import { Field, locationUtil } from '@grafana/data';
  4. import { locationService } from '@grafana/runtime';
  5. import { QueryResponse } from '../service';
  6. export function useKeyNavigationListener() {
  7. const eventsRef = useRef(new Subject<React.KeyboardEvent>());
  8. return {
  9. keyboardEvents: eventsRef.current,
  10. onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {
  11. switch (e.code) {
  12. case 'ArrowDown':
  13. case 'ArrowUp':
  14. case 'ArrowLeft':
  15. case 'ArrowRight':
  16. case 'Enter':
  17. eventsRef.current.next(e);
  18. default:
  19. // ignore
  20. }
  21. },
  22. };
  23. }
  24. interface ItemSelection {
  25. x: number;
  26. y: number;
  27. }
  28. export function useSearchKeyboardNavigation(
  29. keyboardEvents: Observable<React.KeyboardEvent>,
  30. numColumns: number,
  31. response: QueryResponse
  32. ): ItemSelection {
  33. const highlightIndexRef = useRef<ItemSelection>({ x: 0, y: -1 });
  34. const [highlightIndex, setHighlightIndex] = useState<ItemSelection>({ x: 0, y: -1 });
  35. const urlsRef = useRef<Field>();
  36. // Clear selection when the search results change
  37. useEffect(() => {
  38. urlsRef.current = response.view.fields.url;
  39. highlightIndexRef.current.x = 0;
  40. highlightIndexRef.current.y = -1;
  41. setHighlightIndex({ ...highlightIndexRef.current });
  42. }, [response]);
  43. useEffect(() => {
  44. const sub = keyboardEvents.subscribe({
  45. next: (keyEvent) => {
  46. switch (keyEvent?.code) {
  47. case 'ArrowDown': {
  48. highlightIndexRef.current.y++;
  49. setHighlightIndex({ ...highlightIndexRef.current });
  50. break;
  51. }
  52. case 'ArrowUp':
  53. highlightIndexRef.current.y = Math.max(0, highlightIndexRef.current.y - 1);
  54. setHighlightIndex({ ...highlightIndexRef.current });
  55. break;
  56. case 'ArrowRight': {
  57. if (numColumns > 0) {
  58. highlightIndexRef.current.x = Math.min(numColumns, highlightIndexRef.current.x + 1);
  59. setHighlightIndex({ ...highlightIndexRef.current });
  60. }
  61. break;
  62. }
  63. case 'ArrowLeft': {
  64. if (numColumns > 0) {
  65. highlightIndexRef.current.x = Math.max(0, highlightIndexRef.current.x - 1);
  66. setHighlightIndex({ ...highlightIndexRef.current });
  67. }
  68. break;
  69. }
  70. case 'Enter':
  71. if (!urlsRef.current) {
  72. break;
  73. }
  74. const idx = highlightIndexRef.current.x * numColumns + highlightIndexRef.current.y;
  75. if (idx < 0) {
  76. highlightIndexRef.current.x = 0;
  77. highlightIndexRef.current.y = 0;
  78. setHighlightIndex({ ...highlightIndexRef.current });
  79. break;
  80. }
  81. const url = urlsRef.current.values?.get(idx) as string;
  82. if (url) {
  83. locationService.push(locationUtil.stripBaseFromUrl(url));
  84. }
  85. }
  86. },
  87. });
  88. return () => sub.unsubscribe();
  89. }, [keyboardEvents, numColumns]);
  90. return highlightIndex;
  91. }