From ea450a92f9dcd991a9f77e812f54ddf5cc662cf4 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Sat, 19 Apr 2025 22:38:58 +0200 Subject: [PATCH] Extension Sidebar: Close sidebar with toolbar button (#104208) * Extension Sidebar: Improve closing behavior * Extension Sidebar: Fix title --- .../ExtensionToolbarItem.test.tsx | 23 ++--- .../ExtensionSidebar/ExtensionToolbarItem.tsx | 91 ++++++++++++++----- public/locales/en-US/grafana.json | 6 +- 3 files changed, 85 insertions(+), 35 deletions(-) diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx index 7b6823c56d3..b1a6e35ed2c 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx @@ -95,9 +95,9 @@ describe('ExtensionToolbarItem', () => { it('should render a single button when only one component is available', () => { setup(); - const button = screen.getByTestId('extension-toolbar-button'); + const button = screen.getByTestId('extension-toolbar-button-open'); expect(button).toBeInTheDocument(); - expect(button).toHaveAttribute('aria-label', mockComponent.description); + expect(button).toHaveAttribute('aria-label', `Open ${mockComponent.title}`); expect(screen.getByTestId('is-open')).toHaveTextContent('false'); expect(screen.getByTestId('docked-component-id')).toHaveTextContent(''); }); @@ -105,7 +105,7 @@ describe('ExtensionToolbarItem', () => { it('should toggle the sidebar when clicking a single component button', async () => { setup(); - const button = screen.getByTestId('extension-toolbar-button'); + const button = screen.getByTestId('extension-toolbar-button-open'); await userEvent.click(button); expect(screen.getByTestId('is-open')).toHaveTextContent('true'); @@ -127,7 +127,7 @@ describe('ExtensionToolbarItem', () => { setup(); - const button = screen.getByTestId('extension-toolbar-button'); + const button = screen.getByTestId('extension-toolbar-button-open'); expect(button).toBeInTheDocument(); await userEvent.click(button); @@ -151,7 +151,7 @@ describe('ExtensionToolbarItem', () => { setup(); - const button = screen.getByTestId('extension-toolbar-button'); + const button = screen.getByTestId('extension-toolbar-button-open'); await userEvent.click(button); // Menu items should be visible @@ -176,7 +176,7 @@ describe('ExtensionToolbarItem', () => { setup(); // Open the dropdown - const button = screen.getByTestId('extension-toolbar-button'); + const button = screen.getByTestId('extension-toolbar-button-open'); await userEvent.click(button); // Click a menu item @@ -202,12 +202,13 @@ describe('ExtensionToolbarItem', () => { setup(); - const button = screen.getByTestId('extension-toolbar-button'); - await userEvent.click(button); - await userEvent.click(screen.getByText('Component 1')); + const openButton = screen.getByTestId('extension-toolbar-button-open'); + await userEvent.click(openButton); + const component1 = screen.getByText('Component 1'); + await userEvent.click(component1); - await userEvent.click(button); - await userEvent.click(screen.getByText('Component 1')); + const closeButton = screen.getByTestId('extension-toolbar-button-close'); + await userEvent.click(closeButton); expect(screen.getByTestId('is-open')).toHaveTextContent('false'); }); diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx index 4d8f7031db7..c8c638ea512 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx @@ -1,4 +1,5 @@ import { css, cx } from '@emotion/css'; +import { useCallback } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Dropdown, Menu, ToolbarButton, useTheme2 } from '@grafana/ui'; @@ -6,13 +7,25 @@ import { t } from 'app/core/internationalization'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; -import { getComponentIdFromComponentMeta, useExtensionSidebarContext } from './ExtensionSidebarProvider'; +import { + getComponentIdFromComponentMeta, + getComponentMetaFromComponentId, + useExtensionSidebarContext, +} from './ExtensionSidebarProvider'; export function ExtensionToolbarItem() { const styles = getStyles(useTheme2()); const { availableComponents, dockedComponentId, setDockedComponentId, isOpen, isEnabled } = useExtensionSidebarContext(); + let dockedComponentTitle = ''; + if (dockedComponentId) { + const dockedComponent = getComponentMetaFromComponentId(dockedComponentId); + if (dockedComponent) { + dockedComponentTitle = dockedComponent.componentTitle; + } + } + if (!isEnabled || availableComponents.size === 0) { return null; } @@ -26,22 +39,52 @@ export function ExtensionToolbarItem() { return null; } + // conditionally renders a button to open or close the sidebar + // not using a component to avoid passing refs with the `Dropdown` component + const renderButton = useCallback( + (isOpen: boolean, title?: string, onClick?: () => void) => { + if (isOpen) { + // render button to close the sidebar + return ( + setDockedComponentId(undefined)} + tooltip={t('navigation.extension-sidebar.button-tooltip.close', 'Close {{title}}', { title })} + /> + ); + } + // if a title is provided, use it in the tooltip + let tooltip = t('navigation.extension-sidebar.button-tooltip.open-all', 'Open AI assistants and sidebar apps'); + if (title) { + tooltip = t('navigation.extension-sidebar.button-tooltip.open', 'Open {{title}}', { title }); + } + return ( + + ); + }, + [setDockedComponentId, styles.button, styles.buttonActive] + ); + if (components.length === 1) { return ( <> - { - if (isOpen) { - setDockedComponentId(undefined); - } else { - setDockedComponentId(getComponentIdFromComponentMeta(components[0].pluginId, components[0])); - } - }} - /> + {renderButton(isOpen, components[0].title, () => { + if (isOpen) { + setDockedComponentId(undefined); + } else { + setDockedComponentId(getComponentIdFromComponentMeta(components[0].pluginId, components[0])); + } + })} ); @@ -70,15 +113,17 @@ export function ExtensionToolbarItem() { ); return ( <> - - - + {isOpen && + renderButton(isOpen, dockedComponentTitle, () => { + if (isOpen) { + setDockedComponentId(undefined); + } + })} + {!isOpen && ( + + {renderButton(isOpen)} + + )} ); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f6775796ce5..5edbf69c780 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -6489,7 +6489,11 @@ "aria-label": "Breadcrumbs" }, "extension-sidebar": { - "button-tooltip": "Open AI assistants and sidebar apps" + "button-tooltip": { + "close": "Close {{title}}", + "open": "Open {{title}}", + "open-all": "Open AI assistants and sidebar apps" + } }, "help": { "aria-label": "Help"