From 8dd82c34f7136795b40a6fef1de55505d7d5467b Mon Sep 17 00:00:00 2001 From: Luminessa Starlight Date: Wed, 3 Sep 2025 05:22:00 -0400 Subject: [PATCH] Accessibility: Fix overflowing layout on small zoomed screen (#109880) * fixes the layout in a slightly naive way does work in both chrome and firefox * make panel and query sections individually have full viewport height * allow wrapping in dashboard controls, and align time picker section correctly when wrapped * use more fixed minimum widths and allow horizontal scroll for overflow * remove collapsing when sizes are fixed, and fix inverted collapse state logic * use new wrapper for reflow layout media query setup replace the magic numbers with theme breakpoints apply global styles conditionally and locally fix left to right splitter collapse state so it's removed in small size added betterer exception that will be removed in the next commit * moved component definition outside of non-react class so react hook lint rule recognizes it's not a class component (betterer fixes) * remove unused import * nit fix * move disabling useSnapperSplitter logic into the hook simplify reflow hook to only use height, and use a fixed height unrelated to shared width breakpoints * remove global style overrides * prevent scrolling in editor --------- Co-authored-by: Ashley Harrison --- .betterer.results | 3 -- .../panel-edit/PanelEditorRenderer.tsx | 38 ++++++++++++++++--- .../panel-edit/PanelOptionsPane.tsx | 12 ++---- .../splitter/useSnappingSplitter.ts | 24 ++++++++++++ .../panel-edit/useScrollReflowLimit.ts | 15 ++++++++ .../scene/DashboardControls.tsx | 13 +++++-- 6 files changed, 86 insertions(+), 19 deletions(-) create mode 100644 public/app/features/dashboard-scene/panel-edit/useScrollReflowLimit.ts diff --git a/.betterer.results b/.betterer.results index d1b8ef95b28..7f01a7c90a5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1750,9 +1750,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] - ], "public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index 9aee611d4f0..ee2d74b3513 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -15,12 +15,15 @@ import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils'; import { PanelEditor } from './PanelEditor'; import { SaveLibraryVizPanelModal } from './SaveLibraryVizPanelModal'; import { useSnappingSplitter } from './splitter/useSnappingSplitter'; +import { scrollReflowMediaCondition, useScrollReflowLimit } from './useScrollReflowLimit'; export function PanelEditorRenderer({ model }: SceneComponentProps) { const dashboard = getDashboardSceneFor(model); const { optionsPane } = model.useState(); const styles = useStyles2(getStyles); - const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed(); + const [isInitiallyCollapsed, setIsCollapsed] = useEditPaneCollapsed(); + + const isScrollingLayout = useScrollReflowLimit(); const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = useSnappingSplitter({ @@ -28,8 +31,9 @@ export function PanelEditorRenderer({ model }: SceneComponentProps) dragPosition: 'end', initialSize: 330, usePixels: true, - collapsed: isCollapsed, + collapsed: isInitiallyCollapsed, collapseBelowPixels: 250, + disabled: isScrollingLayout, }); useEffect(() => { @@ -80,17 +84,20 @@ function VizAndDataPane({ model }: SceneComponentProps) { const { controls } = dashboard.useState(); const styles = useStyles2(getStyles); + const isScrollingLayout = useScrollReflowLimit(); + const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = useSnappingSplitter({ direction: 'column', dragPosition: 'start', initialSize: 0.5, collapseBelowPixels: 150, + disabled: isScrollingLayout, }); containerProps.className = cx(containerProps.className, styles.container); - if (!dataPane) { + if (!dataPane && !isScrollingLayout) { primaryProps.style.flexGrow = 1; } @@ -102,7 +109,7 @@ function VizAndDataPane({ model }: SceneComponentProps) { )}
-
+
{showLibraryPanelSaveModal && libraryPanel && ( @@ -123,7 +130,10 @@ function VizAndDataPane({ model }: SceneComponentProps) { {dataPane && ( <>
-
+
{splitterState.collapsed && (
- + @@ -209,9 +208,6 @@ function getStyles(theme: GrafanaTheme2) { searchWrapper: css({ padding: theme.spacing(2, 2, 2, 0), }), - vizField: css({ - marginBottom: theme.spacing(0), - }), rotateIcon: css({ rotate: '180deg', }), diff --git a/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts b/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts index 67e97e9dac9..f5579074804 100644 --- a/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts +++ b/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts @@ -16,6 +16,9 @@ export interface UseSnappingSplitterOptions { handleSize?: ComponentSize; usePixels?: boolean; collapseBelowPixels: number; + + /* Disables the splitter, hiding all of its styles */ + disabled?: boolean; } interface PaneState { @@ -31,6 +34,7 @@ export function useSnappingSplitter({ collapsed, handleSize, usePixels, + disabled, }: UseSnappingSplitterOptions) { const [state, setState] = useState({ collapsed: collapsed ?? false, @@ -91,6 +95,26 @@ export function useSnappingSplitter({ onSizeChanged, }); + // This does cause the loss of the adjustment position when toggling disabled on and off again. + // Fixing this properly would require changing how useSplitter works to not both pass and + // adjust styles directly on the element by ref. That causes a React conflict. + if (disabled) { + containerProps.className = ''; + primaryProps.className = ''; + primaryProps.style = {}; + secondaryProps.className = ''; + secondaryProps.style = {}; + splitterProps.style.display = 'none'; + return { + containerProps, + primaryProps, + secondaryProps, + splitterProps, + splitterState: { collapsed: false }, + onToggleCollapse, + }; + } + // This is to allow resizing it beyond the content dimensions secondaryProps.style.overflow = 'hidden'; secondaryProps.style.minWidth = 'unset'; diff --git a/public/app/features/dashboard-scene/panel-edit/useScrollReflowLimit.ts b/public/app/features/dashboard-scene/panel-edit/useScrollReflowLimit.ts new file mode 100644 index 00000000000..3f8285fa366 --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/useScrollReflowLimit.ts @@ -0,0 +1,15 @@ +import { useMedia } from 'react-use'; + +/** + * Media query body "(max-height: 540px)" which matches screens small enough we have zoom reflow + * problems. + * 540px is one of the round screen size numbers that's about what we want. + */ +export const scrollReflowMediaCondition = '(max-height: 540px)'; + +/** + * @returns {boolean} true when the screen is small enough to need zoom reflow handling + */ +export function useScrollReflowLimit(): boolean { + return useMedia(scrollReflowMediaCondition); +} diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index f5099ba1b95..5bbe9733755 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -147,10 +147,10 @@ function DashboardControlsRenderer({ model }: SceneComponentProps} {!hideTimeControls && ( - +
- +
)} @@ -181,7 +181,7 @@ function getStyles(theme: GrafanaTheme2) { gap: theme.spacing(1), padding: theme.spacing(2), flexDirection: 'row', - flexWrap: 'nowrap', + flexWrap: 'wrap-reverse', position: 'relative', width: '100%', marginLeft: 'auto', @@ -198,5 +198,12 @@ function getStyles(theme: GrafanaTheme2) { background: 'unset', position: 'unset', }), + timeControlStack: css({ + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'flex-end', + gap: theme.spacing(1), + marginLeft: 'auto', + }), }; }