diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx index 9619330b571..ce94c04375c 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx @@ -512,24 +512,37 @@ The panel can be collapsed/expanded by clicking on the chevron or the title. > _Note: `collapsible` and `hoverHeader` props are mutually exclusive and cannot be used in the same panel._ ```tsx - - {(innerwidth, innerheight) => { - return ( -
- Content -
- ); - }} -
+function Container() { + const [isCollapsed, setCollapsed] = useState(true); + + return ( + setCollapsed(isCollapsed)} + > + {(innerwidth, innerheight) => { + return ( +
+ Content +
+ ); + }} +
+ ); +} ``` diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx index e00bdd78023..a92fb4a676e 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx @@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryFn } from '@storybook/react'; import { merge } from 'lodash'; import React, { CSSProperties, useState, ReactNode } from 'react'; -import { useInterval } from 'react-use'; +import { useInterval, useToggle } from 'react-use'; import { LoadingState } from '@grafana/data'; import { Button, Icon, PanelChrome, PanelChromeProps, RadioButtonGroup } from '@grafana/ui'; @@ -14,6 +14,9 @@ import { Menu } from '../Menu/Menu'; import mdx from './PanelChrome.mdx'; +const PANEL_WIDTH = 400; +const PANEL_HEIGHT = 150; + const meta: Meta = { title: 'Visualizations/PanelChrome', component: PanelChrome, @@ -39,8 +42,8 @@ function getContentStyle(): CSSProperties { function renderPanel(name: string, overrides?: Partial) { const props: PanelChromeProps = { - width: 400, - height: 150, + width: PANEL_WIDTH, + height: PANEL_HEIGHT, children: () => undefined, }; @@ -57,6 +60,33 @@ function renderPanel(name: string, overrides?: Partial) { ); } +function renderCollapsiblePanel(name: string, overrides?: Partial) { + const props: PanelChromeProps = { + width: PANEL_WIDTH, + height: PANEL_HEIGHT, + children: () => undefined, + collapsible: true, + }; + + merge(props, overrides); + + const contentStyle = getContentStyle(); + + const ControlledCollapseComponent = () => { + const [collapsed, toggleCollapsed] = useToggle(false); + + return ( + + {(innerWidth, innerHeight) => { + return
{name}
; + }} +
+ ); + }; + + return ; +} + const menu = ( @@ -215,7 +245,7 @@ export const Examples = () => { />, ], })} - {renderPanel('Collapsible panel', { + {renderCollapsiblePanel('Collapsible panel', { title: 'Default title', collapsible: true, })} diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx index 45e2535f89b..6e050af4541 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx @@ -1,5 +1,6 @@ import { screen, render, fireEvent } from '@testing-library/react'; import React from 'react'; +import { useToggle } from 'react-use'; import { LoadingState } from '@grafana/data'; @@ -18,6 +19,27 @@ const setup = (propOverrides?: Partial) => { return render(); }; +const setupWithToggleCollapsed = (propOverrides?: Partial) => { + const props: PanelChromeProps = { + width: 100, + height: 100, + children: (innerWidth, innerHeight) => { + return
Panel's Content
; + }, + collapsible: true, + }; + + Object.assign(props, propOverrides); + + const ControlledCollapseComponent = () => { + const [collapsed, toggleCollapsed] = useToggle(false); + + return ; + }; + + return render(); +}; + it('renders an empty panel with required props only', () => { setup(); @@ -125,8 +147,25 @@ it('renders streaming indicator in the panel header if loadingState is streaming expect(screen.getByTestId('panel-streaming')).toBeInTheDocument(); }); -it('collapes the panel when user clicks on the chevron or the title', () => { - setup({ collapsible: true, title: 'Default title' }); +it('collapses the controlled panel when user clicks on the chevron or the title', () => { + setupWithToggleCollapsed({ title: 'Default title' }); + + expect(screen.getByText("Panel's Content")).toBeInTheDocument(); + + const button = screen.getByText('Default title'); + // collapse button should have same aria-controls as the panel's content + expect(button.getAttribute('aria-controls')).toBe(button.parentElement?.parentElement?.nextElementSibling?.id); + + fireEvent.click(button); + + expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument(); + // aria-controls should be removed when panel is collapsed + expect(button).not.toHaveAttribute('aria-controlls'); + expect(button.parentElement?.parentElement?.nextElementSibling?.id).toBe(undefined); +}); + +it('collapses the uncontrolled panel when user clicks on the chevron or the title', () => { + setup({ title: 'Default title', collapsible: true }); expect(screen.getByText("Panel's Content")).toBeInTheDocument(); diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 7cfce1b8f0d..95b3780e9ef 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -71,12 +71,19 @@ interface AutoSize extends BaseProps { interface Collapsible { collapsible: boolean; + collapsed?: boolean; + /** + * callback when collapsing or expanding the panel + */ + onToggleCollapse?: (collapsed: boolean) => void; hoverHeader?: never; hoverHeaderOffset?: never; } interface HoverHeader { collapsible?: never; + collapsed?: never; + onToggleCollapse?: never; hoverHeader?: boolean; hoverHeaderOffset?: number; } @@ -111,14 +118,21 @@ export function PanelChrome({ onCancelQuery, onOpenMenu, collapsible = false, + collapsed, + onToggleCollapse, }: PanelChromeProps) { const theme = useTheme2(); const styles = useStyles2(getStyles); const panelContentId = useId(); + const hasHeader = !hoverHeader; + const [isOpen, toggleOpen] = useToggle(true); - const hasHeader = !hoverHeader; + // if collapsed is not defined, then component is uncontrolled and state is managed internally + if (collapsed === undefined) { + collapsed = !isOpen; + } // hover menu is only shown on hover when not on touch devices const showOnHoverClass = 'show-on-hover'; @@ -129,7 +143,7 @@ export function PanelChrome({ padding, theme, headerHeight, - isOpen, + collapsed, height, width ); @@ -139,7 +153,7 @@ export function PanelChrome({ cursor: dragClass ? 'move' : 'auto', }; - const containerStyles: CSSProperties = { width, height: isOpen ? height : headerHeight }; + const containerStyles: CSSProperties = { width, height: !collapsed ? height : headerHeight }; const [ref, { width: loadingBarWidth }] = useMeasure(); /** Old property name now maps to actions */ @@ -154,12 +168,17 @@ export function PanelChrome({