mirror of https://github.com/grafana/grafana.git
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 <ashley.harrison@grafana.com>
This commit is contained in:
parent
1758fa8fbb
commit
8dd82c34f7
|
@ -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"]
|
||||
],
|
||||
|
|
|
@ -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<PanelEditor>) {
|
||||
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<PanelEditor>)
|
|||
dragPosition: 'end',
|
||||
initialSize: 330,
|
||||
usePixels: true,
|
||||
collapsed: isCollapsed,
|
||||
collapsed: isInitiallyCollapsed,
|
||||
collapseBelowPixels: 250,
|
||||
disabled: isScrollingLayout,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -80,17 +84,20 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
|||
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<PanelEditor>) {
|
|||
</div>
|
||||
)}
|
||||
<div {...containerProps}>
|
||||
<div {...primaryProps}>
|
||||
<div {...primaryProps} className={cx(primaryProps.className, isScrollingLayout && styles.fixedSizeViz)}>
|
||||
<VizWrapper panel={panel} tableView={tableView} />
|
||||
</div>
|
||||
{showLibraryPanelSaveModal && libraryPanel && (
|
||||
|
@ -123,7 +130,10 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
|||
{dataPane && (
|
||||
<>
|
||||
<div {...splitterProps} />
|
||||
<div {...secondaryProps}>
|
||||
<div
|
||||
{...secondaryProps}
|
||||
className={cx(secondaryProps.className, isScrollingLayout && styles.fullSizeEditor)}
|
||||
>
|
||||
{splitterState.collapsed && (
|
||||
<div className={styles.expandDataPane}>
|
||||
<Button
|
||||
|
@ -163,6 +173,7 @@ function VizWrapper({ panel, tableView }: VizWrapperProps) {
|
|||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
const scrollReflowMediaQuery = '@media ' + scrollReflowMediaCondition;
|
||||
return {
|
||||
pageContainer: css({
|
||||
display: 'grid',
|
||||
|
@ -171,6 +182,9 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
gridTemplateColumns: `1fr`,
|
||||
gridTemplateRows: '1fr',
|
||||
height: '100%',
|
||||
[scrollReflowMediaQuery]: {
|
||||
gridTemplateColumns: `100%`,
|
||||
},
|
||||
}),
|
||||
pageContainerWithControls: css({
|
||||
gridTemplateAreas: `
|
||||
|
@ -196,6 +210,14 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'unset',
|
||||
[scrollReflowMediaQuery]: {
|
||||
height: 'auto',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(470px, 1fr) 330px',
|
||||
gridTemplateRows: '1fr',
|
||||
gap: theme.spacing(1),
|
||||
width: '100%',
|
||||
},
|
||||
}),
|
||||
body: css({
|
||||
label: 'body',
|
||||
|
@ -248,5 +270,11 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
width: '100%',
|
||||
paddingLeft: theme.spacing(2),
|
||||
}),
|
||||
fixedSizeViz: css({
|
||||
height: '100vh',
|
||||
}),
|
||||
fullSizeEditor: css({
|
||||
height: 'max-content',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import { getAllPanelPluginMeta } from 'app/features/panel/state/util';
|
|||
import { PanelOptions } from './PanelOptions';
|
||||
import { PanelVizTypePicker } from './PanelVizTypePicker';
|
||||
import { INTERACTION_EVENT_NAME, INTERACTION_ITEM } from './interaction';
|
||||
import { useScrollReflowLimit } from './useScrollReflowLimit';
|
||||
|
||||
export interface PanelOptionsPaneState extends SceneObjectState {
|
||||
isVizPickerOpen?: boolean;
|
||||
|
@ -132,16 +133,14 @@ function PanelOptionsPaneComponent({ model }: SceneComponentProps<PanelOptionsPa
|
|||
const hasFieldConfig = !isSearching && !panel.getPlugin()?.fieldConfigRegistry.isEmpty();
|
||||
const [isSearchingOptions, setIsSearchingOptions] = useToggle(false);
|
||||
const onlyOverrides = listMode === OptionFilter.Overrides;
|
||||
const isScrollingLayout = useScrollReflowLimit();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isVizPickerOpen && (
|
||||
<>
|
||||
<div className={styles.top}>
|
||||
<Field
|
||||
label={t('dashboard.panel-edit.visualization-button-label', 'Visualization')}
|
||||
className={styles.vizField}
|
||||
>
|
||||
<Field label={t('dashboard.panel-edit.visualization-button-label', 'Visualization')} noMargin>
|
||||
<Stack gap={1}>
|
||||
<VisualizationButton pluginId={pluginId} onOpen={model.onToggleVizPicker} />
|
||||
<Button
|
||||
|
@ -178,7 +177,7 @@ function PanelOptionsPaneComponent({ model }: SceneComponentProps<PanelOptionsPa
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<ScrollContainer>
|
||||
<ScrollContainer minHeight={isScrollingLayout ? 'max-content' : 0}>
|
||||
<PanelOptions panel={panel} searchQuery={searchQuery} listMode={listMode} data={data} />
|
||||
</ScrollContainer>
|
||||
</>
|
||||
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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<PaneState>({
|
||||
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';
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -147,10 +147,10 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
|
|||
{editPanel && <PanelEditControls panelEditor={editPanel} />}
|
||||
</Stack>
|
||||
{!hideTimeControls && (
|
||||
<Stack justifyContent={'flex-end'}>
|
||||
<div className={styles.timeControlStack}>
|
||||
<timePicker.Component model={timePicker} />
|
||||
<refreshPicker.Component model={refreshPicker} />
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
<Stack>
|
||||
<DropdownVariableControls dashboard={dashboard} />
|
||||
|
@ -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',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue