From edd4faf0647cf1a89f44e0a69234fd05565957aa Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 16 Jun 2023 11:39:24 -0400 Subject: [PATCH] [v10.0.x] DS Picker: first item is not active when filtering (#70256) DS Picker: first item is always active when filtering (#70071) (cherry picked from commit 6be0ca396f45d636150b65b8979c3a1d52486610) Co-authored-by: Ivan Ortega Alba --- .../components/picker/DataSourceList.tsx | 1 - public/app/features/datasources/hooks.ts | 65 +++++++++++++------ 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/public/app/features/datasources/components/picker/DataSourceList.tsx b/public/app/features/datasources/components/picker/DataSourceList.tsx index 69efcf57c4f..a56a8daa1f3 100644 --- a/public/app/features/datasources/components/picker/DataSourceList.tsx +++ b/public/app/features/datasources/components/picker/DataSourceList.tsx @@ -54,7 +54,6 @@ export function DataSourceList(props: DataSourceListProps) { const styles = getStyles(theme, selectedItemCssSelector); const { className, current, onChange, enableKeyboardNavigation, onClickEmptyStateCTA } = props; - // QUESTION: Should we use data from the Redux store as admin DS view does? const dataSources = useDatasources({ alerting: props.alerting, annotations: props.annotations, diff --git a/public/app/features/datasources/hooks.ts b/public/app/features/datasources/hooks.ts index 07c700119e8..0960b8bcc06 100644 --- a/public/app/features/datasources/hooks.ts +++ b/public/app/features/datasources/hooks.ts @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { useLocalStorage } from 'react-use'; import { Observable } from 'rxjs'; @@ -68,7 +68,7 @@ export interface KeybaordNavigatableListProps { */ export function useKeyboardNavigatableList(props: KeybaordNavigatableListProps): [Record, string] { const { keyboardEvents, containerRef } = props; - const [selectedIndex, setSelectedIndex] = useState(0); + const selectedIndex = useRef(0); const attributeName = 'data-role'; const roleName = 'keyboardSelectableItem'; @@ -78,27 +78,29 @@ export function useKeyboardNavigatableList(props: KeybaordNavigatableListProps): const selectedAttributeName = 'data-selectedItem'; const selectedItemCssSelector = `[${selectedAttributeName}="true"]`; - useEffect(() => { - const listItems = containerRef?.current?.querySelectorAll( - querySelectorNavigatableElements - ); + const selectItem = useCallback( + (index: number) => { + const listItems = containerRef?.current?.querySelectorAll( + querySelectorNavigatableElements + ); + const selectedItem = listItems?.item(index % listItems?.length); - const selectedItem = listItems?.item(selectedIndex % listItems?.length); + listItems?.forEach((li) => li.setAttribute(selectedAttributeName, 'false')); - listItems?.forEach((li) => li.setAttribute(selectedAttributeName, 'false')); + if (selectedItem) { + selectedItem.scrollIntoView({ block: 'center' }); + selectedItem.setAttribute(selectedAttributeName, 'true'); + } + }, + [containerRef, querySelectorNavigatableElements] + ); - if (selectedItem) { - selectedItem.scrollIntoView({ block: 'center' }); - selectedItem.setAttribute(selectedAttributeName, 'true'); - } - }, [selectedIndex, containerRef, selectedAttributeName, querySelectorNavigatableElements]); - - const clickSelectedElement = () => { + const clickSelectedElement = useCallback(() => { containerRef?.current ?.querySelector(selectedItemCssSelector) ?.querySelector('button') // This is a bit weird. The main use for this would be to select card items, however the root of the card component does not have the click event handler, instead it's attached to a button inside it. ?.click(); - }; + }, [containerRef, selectedItemCssSelector]); useEffect(() => { if (!keyboardEvents) { @@ -108,12 +110,13 @@ export function useKeyboardNavigatableList(props: KeybaordNavigatableListProps): next: (keyEvent) => { switch (keyEvent?.code) { case 'ArrowDown': { - setSelectedIndex(selectedIndex + 1); + selectItem(++selectedIndex.current); keyEvent.preventDefault(); break; } case 'ArrowUp': - setSelectedIndex(selectedIndex > 0 ? selectedIndex - 1 : selectedIndex); + selectedIndex.current = selectedIndex.current > 0 ? selectedIndex.current - 1 : selectedIndex.current; + selectItem(selectedIndex.current); keyEvent.preventDefault(); break; case 'Enter': @@ -123,7 +126,31 @@ export function useKeyboardNavigatableList(props: KeybaordNavigatableListProps): }, }); return () => sub.unsubscribe(); - }); + }, [keyboardEvents, selectItem, clickSelectedElement]); + + useEffect(() => { + // This observer is used to keep track of the number of items in the list + // that can change dinamically (e.g. when filtering a dropdown list) + const listObserver = new MutationObserver((mutations) => { + const listHasChanged = mutations.some( + (mutation) => + (mutation.addedNodes && mutation.addedNodes.length > 0) || + (mutation.removedNodes && mutation.removedNodes.length > 0) + ); + + listHasChanged && selectItem(0); + }); + + if (containerRef.current) { + listObserver.observe(containerRef.current, { + childList: true, + }); + } + + return () => { + listObserver.disconnect(); + }; + }, [containerRef, querySelectorNavigatableElements, selectItem]); return [navigatableItemProps, selectedItemCssSelector]; }