From 6be0ca396f45d636150b65b8979c3a1d52486610 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Fri, 16 Jun 2023 17:30:49 +0200 Subject: [PATCH] DS Picker: first item is always active when filtering (#70071) * Select always the first item by default when filtering * Avoid re-render when updating the selected item state --- .../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]; }