PanelChrome: Wrap in a div to allow for hover headers (#114037)

wrap PanelChrome in a div that's outside of `overflow: hidden;` to allow for hover headers
This commit is contained in:
Ashley Harrison
2025-11-19 12:52:09 +00:00
committed by GitHub
parent cf24e0bbd8
commit 3f008acde2
@@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
import { CSSProperties, ReactElement, ReactNode, useId, useState } from 'react';
import { CSSProperties, PropsWithChildren, ReactElement, ReactNode, useId, useState } from 'react';
import * as React from 'react';
import { useMeasure, useToggle } from 'react-use';
@@ -110,6 +110,11 @@ interface HoverHeader {
hoverHeaderOffset?: number;
}
const MaybeWrap = ({ children }: PropsWithChildren<{}>) => {
const styles = useStyles2(getStyles);
return getFeatureToggle('preventPanelChromeOverflow') ? <div className={styles.container}>{children}</div> : children;
};
/**
* @internal
*/
@@ -336,104 +341,106 @@ export function PanelChrome({
const hasHeaderContent = title || description || titleItems || menu || dragClass || actions;
return (
// tabIndex={0} is needed for keyboard accessibility in the plot area
<section
className={cx(
styles.container,
isPanelTransparent && styles.transparentContainer,
isSelected && 'dashboard-selected-element',
!isSelected && isSelectable && selectableHighlight && 'dashboard-selectable-element'
)}
style={containerStyles}
aria-labelledby={!!title ? panelTitleId : undefined}
data-testid={testid}
tabIndex={0} // eslint-disable-line jsx-a11y/no-noninteractive-tabindex
onFocus={onFocus}
onMouseMove={onMouseMove}
onMouseEnter={onMouseEnter}
ref={ref}
>
<div className={styles.loadingBarContainer}>
{loadingState === LoadingState.Loading ? (
<LoadingBar
width={loadingBarWidth}
ariaLabel={t('grafana-ui.panel-chrome.ariaLabel-panel-loading', 'Panel loading bar')}
/>
) : null}
</div>
{hoverHeader && (
<>
{hasHeaderContent && (
<HoverWidget
menu={menu}
title={typeof title === 'string' ? title : undefined}
dragClass={dragClass}
offset={hoverHeaderOffset}
onOpenMenu={onOpenMenu}
>
{headerContent}
</HoverWidget>
)}
{statusMessage && (
<div className={styles.errorContainerFloating}>
<PanelStatus
message={statusMessage}
onClick={statusMessageOnClick}
ariaLabel={t('grafana-ui.panel-chrome.ariaLabel-panel-status', 'Panel status')}
/>
</div>
)}
</>
)}
{hasHeader && (
<div
className={cx(styles.headerContainer, dragClass)}
style={headerStyles}
data-testid={selectors.components.Panels.Panel.headerContainer}
onPointerDown={onPointerDown}
onMouseEnter={isSelectable ? onHeaderEnter : undefined}
onMouseLeave={isSelectable ? onHeaderLeave : undefined}
onPointerUp={onPointerUp}
>
{statusMessage && (
<div className={dragClassCancel}>
<PanelStatus
message={statusMessage}
onClick={statusMessageOnClick}
ariaLabel={t('grafana-ui.panel-chrome.ariaLabel-panel-status', 'Panel status')}
/>
</div>
)}
{headerContent}
{menu && (
<PanelMenu
menu={menu}
title={typeof title === 'string' ? title : undefined}
placement="bottom-end"
menuButtonClass={cx(styles.menuItem, dragClassCancel, showOnHoverClass)}
onOpenMenu={onOpenMenu}
<MaybeWrap>
{/* tabIndex={0} is needed for keyboard accessibility in the plot area */}
<section
className={cx(
styles.panel,
isPanelTransparent && styles.panelTransparent,
isSelected && 'dashboard-selected-element',
!isSelected && isSelectable && selectableHighlight && 'dashboard-selectable-element'
)}
style={containerStyles}
aria-labelledby={!!title ? panelTitleId : undefined}
data-testid={testid}
tabIndex={0} // eslint-disable-line jsx-a11y/no-noninteractive-tabindex
onFocus={onFocus}
onMouseMove={onMouseMove}
onMouseEnter={onMouseEnter}
ref={ref}
>
<div className={styles.loadingBarContainer}>
{loadingState === LoadingState.Loading ? (
<LoadingBar
width={loadingBarWidth}
ariaLabel={t('grafana-ui.panel-chrome.ariaLabel-panel-loading', 'Panel loading bar')}
/>
)}
) : null}
</div>
)}
{!collapsed && (
<div
id={panelContentId}
data-testid={selectors.components.Panels.Panel.content}
className={cx(styles.content, height === undefined && styles.containNone)}
style={contentStyle}
onPointerDown={onContentPointerDown}
>
{typeof children === 'function' ? children(innerWidth, innerHeight) : children}
</div>
)}
</section>
{hoverHeader && (
<>
{hasHeaderContent && (
<HoverWidget
menu={menu}
title={typeof title === 'string' ? title : undefined}
dragClass={dragClass}
offset={hoverHeaderOffset}
onOpenMenu={onOpenMenu}
>
{headerContent}
</HoverWidget>
)}
{statusMessage && (
<div className={styles.errorContainerFloating}>
<PanelStatus
message={statusMessage}
onClick={statusMessageOnClick}
ariaLabel={t('grafana-ui.panel-chrome.ariaLabel-panel-status', 'Panel status')}
/>
</div>
)}
</>
)}
{hasHeader && (
<div
className={cx(styles.headerContainer, dragClass)}
style={headerStyles}
data-testid={selectors.components.Panels.Panel.headerContainer}
onPointerDown={onPointerDown}
onMouseEnter={isSelectable ? onHeaderEnter : undefined}
onMouseLeave={isSelectable ? onHeaderLeave : undefined}
onPointerUp={onPointerUp}
>
{statusMessage && (
<div className={dragClassCancel}>
<PanelStatus
message={statusMessage}
onClick={statusMessageOnClick}
ariaLabel={t('grafana-ui.panel-chrome.ariaLabel-panel-status', 'Panel status')}
/>
</div>
)}
{headerContent}
{menu && (
<PanelMenu
menu={menu}
title={typeof title === 'string' ? title : undefined}
placement="bottom-end"
menuButtonClass={cx(styles.menuItem, dragClassCancel, showOnHoverClass)}
onOpenMenu={onOpenMenu}
/>
)}
</div>
)}
{!collapsed && (
<div
id={panelContentId}
data-testid={selectors.components.Panels.Panel.content}
className={cx(styles.content, height === undefined && styles.containNone)}
style={contentStyle}
onPointerDown={onContentPointerDown}
>
{typeof children === 'function' ? children(innerWidth, innerHeight) : children}
</div>
)}
</section>
</MaybeWrap>
);
}
@@ -494,10 +501,13 @@ const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
position: 'relative',
}),
panel: css({
label: 'panel-container',
backgroundColor: background,
border: `1px solid ${borderColor}`,
position: 'relative',
position: getFeatureToggle('preventPanelChromeOverflow') ? 'unset' : 'relative',
borderRadius: theme.shape.radius.default,
height: '100%',
display: 'flex',
@@ -534,7 +544,7 @@ const getStyles = (theme: GrafanaTheme2) => {
},
},
}),
transparentContainer: css({
panelTransparent: css({
label: 'panel-transparent-container',
backgroundColor: 'transparent',
border: '1px solid transparent',