[kbn-grid-layout] Add useCustomDragHandle prop (#210463)

This PR **might** resolve
https://github.com/elastic/kibana/issues/207011 but we will need time
for the telemetry metrics to settle before we know for sure.

## Summary

This PR removes the conditional rendering of the default drag handle in
`kbn-grid-layout`, which has two benefits:

1. It removes the double render of `GridPanel` that was caused by
relying on the `dragHandleCount` to be updated in order to determine
whether the default drag handle should be rendered
2. The default drag handle no longer "flashes" when Dashboards are
loading and waiting for `dragHandleCount` to update
  
   - **Before:**
   

https://github.com/user-attachments/assets/30a032fc-4df3-42ce-9494-dd7f69637c03
      
   - **After:**
   

https://github.com/user-attachments/assets/db447911-cbe2-40dd-9a07-405d1e35a75d


Instead, the consumer of `kbn-grid-layout` is responsible for setting
the `useCustomDragHandle` prop to `true` when they want to use a drag
handle other than the default one.


When adding the `useCustomDragHandle` prop, I got annoyed that I had to
pass this prop all the way down to `grid_panel` - so I decided to swap
to using React context in this PR, as well. The API for the grid layout
component will most likely continue to grow, so this should make it
easier to manage the props.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Hannah Mudge 2025-02-13 15:18:00 -07:00 committed by GitHub
parent 84fdbcba62
commit 200922a512
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 557 additions and 501 deletions

View file

@ -337,6 +337,7 @@ export const GridExample = ({
rowHeight,
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
}}
useCustomDragHandle={true}
renderPanelContents={renderPanelContents}
onLayoutChange={(newLayout) => {
const { panels, rows } = gridLayoutToDashboardPanelMap(

View file

@ -11,17 +11,11 @@ import React, { useEffect, useRef } from 'react';
import { combineLatest, skip } from 'rxjs';
import { css } from '@emotion/react';
import { useGridLayoutContext } from './use_grid_layout_context';
import { GridLayoutStateManager } from './types';
export const DragPreview = React.memo(({ rowIndex }: { rowIndex: number }) => {
const { gridLayoutStateManager } = useGridLayoutContext();
export const DragPreview = React.memo(
({
rowIndex,
gridLayoutStateManager,
}: {
rowIndex: number;
gridLayoutStateManager: GridLayoutStateManager;
}) => {
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
useEffect(
@ -56,8 +50,7 @@ export const DragPreview = React.memo(
);
return <div ref={dragPreviewRef} className={'kbnGridPanel--dragPreview'} css={styles} />;
}
);
});
const styles = css({ display: 'none', pointerEvents: 'none' });

View file

@ -8,15 +8,14 @@
*/
import { css } from '@emotion/react';
import React, { PropsWithChildren, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import { combineLatest } from 'rxjs';
import { GridLayoutStateManager } from './types';
import { useGridLayoutContext } from './use_grid_layout_context';
export const GridHeightSmoother = React.memo(
({
children,
gridLayoutStateManager,
}: PropsWithChildren<{ gridLayoutStateManager: GridLayoutStateManager }>) => {
({ children }: { children: React.ReactNode | undefined }) => {
const { gridLayoutStateManager } = useGridLayoutContext();
// set the parent div size directly to smooth out height changes.
const smoothHeightRef = useRef<HTMLDivElement | null>(null);

View file

@ -25,20 +25,23 @@ import {
const onLayoutChange = jest.fn();
const renderGridLayout = (propsOverrides: Partial<GridLayoutProps> = {}) => {
const defaultProps: GridLayoutProps = {
const props = {
accessMode: 'EDIT',
layout: getSampleLayout(),
gridSettings,
renderPanelContents: mockRenderPanelContents,
onLayoutChange,
};
...propsOverrides,
} as GridLayoutProps;
const { rerender, ...rtlRest } = render(<GridLayout {...defaultProps} {...propsOverrides} />);
const { rerender, ...rtlRest } = render(<GridLayout {...props} />);
return {
...rtlRest,
rerender: (overrides: Partial<GridLayoutProps>) =>
rerender(<GridLayout {...defaultProps} {...overrides} />),
rerender: (overrides: Partial<GridLayoutProps>) => {
const newProps = { ...props, ...overrides } as GridLayoutProps;
return rerender(<GridLayout {...newProps} />);
},
};
};

View file

@ -9,30 +9,27 @@
import classNames from 'classnames';
import { cloneDeep } from 'lodash';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { combineLatest, distinctUntilChanged, map, pairwise, skip } from 'rxjs';
import { css } from '@emotion/react';
import { GridHeightSmoother } from './grid_height_smoother';
import { GridRow } from './grid_row';
import { GridAccessMode, GridLayoutData, GridSettings } from './types';
import { GridAccessMode, GridLayoutData, GridSettings, UseCustomDragHandle } from './types';
import { GridLayoutContext, GridLayoutContextType } from './use_grid_layout_context';
import { useGridLayoutState } from './use_grid_layout_state';
import { isLayoutEqual } from './utils/equality_checks';
import { resolveGridRow } from './utils/resolve_grid_row';
export interface GridLayoutProps {
export type GridLayoutProps = {
layout: GridLayoutData;
gridSettings: GridSettings;
renderPanelContents: (
panelId: string,
setDragHandles?: (refs: Array<HTMLElement | null>) => void
) => React.ReactNode;
onLayoutChange: (newLayout: GridLayoutData) => void;
expandedPanelId?: string;
accessMode?: GridAccessMode;
className?: string; // this makes it so that custom CSS can be passed via Emotion
}
} & UseCustomDragHandle;
export const GridLayout = ({
layout,
@ -42,6 +39,7 @@ export const GridLayout = ({
expandedPanelId,
accessMode = 'EDIT',
className,
useCustomDragHandle = false,
}: GridLayoutProps) => {
const layoutRef = useRef<HTMLDivElement | null>(null);
const { gridLayoutStateManager, setDimensionsRef } = useGridLayoutState({
@ -134,8 +132,19 @@ export const GridLayout = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const memoizedContext = useMemo(
() =>
({
renderPanelContents,
useCustomDragHandle,
gridLayoutStateManager,
} as GridLayoutContextType),
[renderPanelContents, useCustomDragHandle, gridLayoutStateManager]
);
return (
<GridHeightSmoother gridLayoutStateManager={gridLayoutStateManager}>
<GridLayoutContext.Provider value={memoizedContext}>
<GridHeightSmoother>
<div
ref={(divElement) => {
layoutRef.current = divElement;
@ -150,17 +159,11 @@ export const GridLayout = ({
]}
>
{Array.from({ length: rowCount }, (_, rowIndex) => {
return (
<GridRow
key={rowIndex}
rowIndex={rowIndex}
renderPanelContents={renderPanelContents}
gridLayoutStateManager={gridLayoutStateManager}
/>
);
return <GridRow key={rowIndex} rowIndex={rowIndex} />;
})}
</div>
</GridHeightSmoother>
</GridLayoutContext.Provider>
);
};

View file

@ -10,18 +10,19 @@ import React from 'react';
import { EuiIcon, type UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { UserInteractionEvent } from '../../use_grid_layout_events/types';
import { DragHandleApi } from './use_drag_handle_api';
export const DefaultDragHandle = React.memo(
({ onDragStart }: { onDragStart: (e: UserInteractionEvent) => void }) => {
({ dragHandleApi }: { dragHandleApi: DragHandleApi }) => {
return (
<button
onMouseDown={onDragStart}
onTouchStart={onDragStart}
onMouseDown={dragHandleApi.startDrag}
onTouchStart={dragHandleApi.startDrag}
aria-label={i18n.translate('kbnGridLayout.dragHandle.ariaLabel', {
defaultMessage: 'Drag to move',
})}
className="kbnGridPanel__dragHandle"
data-test-subj="kbnGridPanel--dragHandle"
css={styles}
>
<EuiIcon type="grabOmnidirectional" />

View file

@ -1,73 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { GridLayoutStateManager } from '../../types';
import { useGridLayoutEvents } from '../../use_grid_layout_events';
import { DefaultDragHandle } from './default_drag_handle';
export interface DragHandleApi {
setDragHandles: (refs: Array<HTMLElement | null>) => void;
}
export const DragHandle = React.memo(
React.forwardRef<
DragHandleApi,
{
gridLayoutStateManager: GridLayoutStateManager;
panelId: string;
rowIndex: number;
}
>(({ gridLayoutStateManager, panelId, rowIndex }, ref) => {
const startInteraction = useGridLayoutEvents({
interactionType: 'drag',
gridLayoutStateManager,
panelId,
rowIndex,
});
const [dragHandleCount, setDragHandleCount] = useState<number>(0);
const removeEventListenersRef = useRef<(() => void) | null>(null);
const setDragHandles = useCallback(
(dragHandles: Array<HTMLElement | null>) => {
setDragHandleCount(dragHandles.length);
for (const handle of dragHandles) {
if (handle === null) return;
handle.addEventListener('mousedown', startInteraction, { passive: true });
handle.addEventListener('touchstart', startInteraction, { passive: true });
handle.style.touchAction = 'none';
}
removeEventListenersRef.current = () => {
for (const handle of dragHandles) {
if (handle === null) return;
handle.removeEventListener('mousedown', startInteraction);
handle.removeEventListener('touchstart', startInteraction);
}
};
},
[startInteraction]
);
useEffect(
() => () => {
// on unmount, remove all drag handle event listeners
removeEventListenersRef.current?.();
},
[]
);
useImperativeHandle(ref, () => ({ setDragHandles }), [setDragHandles]);
return Boolean(dragHandleCount) ? null : <DefaultDragHandle onDragStart={startInteraction} />;
})
);
DragHandle.displayName = 'KbnGridLayoutDragHandle';

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { useCallback, useEffect, useRef } from 'react';
import { useGridLayoutEvents } from '../../use_grid_layout_events';
import { UserInteractionEvent } from '../../use_grid_layout_events/types';
import { useGridLayoutContext } from '../../use_grid_layout_context';
export interface DragHandleApi {
startDrag: (e: UserInteractionEvent) => void;
setDragHandles?: (refs: Array<HTMLElement | null>) => void;
}
export const useDragHandleApi = ({
panelId,
rowIndex,
}: {
panelId: string;
rowIndex: number;
}): DragHandleApi => {
const { useCustomDragHandle } = useGridLayoutContext();
const startInteraction = useGridLayoutEvents({
interactionType: 'drag',
panelId,
rowIndex,
});
const removeEventListenersRef = useRef<(() => void) | null>(null);
const setDragHandles = useCallback(
(dragHandles: Array<HTMLElement | null>) => {
for (const handle of dragHandles) {
if (handle === null) return;
handle.addEventListener('mousedown', startInteraction, { passive: true });
handle.addEventListener('touchstart', startInteraction, { passive: true });
handle.style.touchAction = 'none';
}
removeEventListenersRef.current = () => {
for (const handle of dragHandles) {
if (handle === null) return;
handle.removeEventListener('mousedown', startInteraction);
handle.removeEventListener('touchstart', startInteraction);
}
};
},
[startInteraction]
);
useEffect(
() => () => {
// on unmount, remove all drag handle event listeners
removeEventListenersRef.current?.();
},
[]
);
return {
startDrag: startInteraction,
setDragHandles: useCustomDragHandle ? setDragHandles : undefined,
};
};

View file

@ -9,20 +9,51 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { GridPanel, GridPanelProps } from './grid_panel';
import { GridPanel, type GridPanelProps } from './grid_panel';
import { gridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks';
import { GridLayoutContext, type GridLayoutContextType } from '../use_grid_layout_context';
describe('GridPanel', () => {
const renderGridPanel = (propsOverrides: Partial<GridPanelProps> = {}) => {
return render(
<GridPanel
panelId="panel1"
rowIndex={0}
renderPanelContents={mockRenderPanelContents}
gridLayoutStateManager={gridLayoutStateManagerMock}
{...propsOverrides}
/>
const renderGridPanel = (overrides?: {
propsOverrides?: Partial<GridPanelProps>;
contextOverrides?: Partial<GridLayoutContextType>;
}) => {
const contextValue = {
renderPanelContents: mockRenderPanelContents,
gridLayoutStateManager: gridLayoutStateManagerMock,
...(overrides?.contextOverrides ?? {}),
} as GridLayoutContextType;
const panelProps = {
panelId: 'panel1',
rowIndex: 0,
...(overrides?.propsOverrides ?? {}),
};
const { rerender, ...rtlRest } = render(
<GridLayoutContext.Provider value={contextValue}>
<GridPanel {...panelProps} />
</GridLayoutContext.Provider>
);
return {
...rtlRest,
rerender: (newOverrides?: {
propsOverrides?: Partial<GridPanelProps>;
contextOverrides?: Partial<GridLayoutContextType>;
}) => {
return rerender(
<GridLayoutContext.Provider
value={
{
...contextValue,
...(newOverrides?.contextOverrides ?? {}),
} as GridLayoutContextType
}
>
<GridPanel {...panelProps} {...(newOverrides?.propsOverrides ?? {})} />
</GridLayoutContext.Provider>
);
},
};
};
afterEach(() => {
@ -32,6 +63,22 @@ describe('GridPanel', () => {
it('renders panel contents', () => {
renderGridPanel();
expect(screen.getByText('panel content panel1')).toBeInTheDocument();
expect(mockRenderPanelContents).toHaveBeenCalledWith('panel1', undefined);
});
describe('use custom drag handle', () => {
it('renders default drag handle when `useCustomDragHandle` is false | undefined', () => {
const panel = renderGridPanel();
expect(panel.queryByTestId('kbnGridPanel--dragHandle')).toBeInTheDocument();
panel.rerender({ contextOverrides: { useCustomDragHandle: false } });
expect(panel.queryByTestId('kbnGridPanel--dragHandle')).toBeInTheDocument();
});
it('does not render default drag handle when `useCustomDragHandle` is true and calls setDragHandles', () => {
const panel = renderGridPanel({ contextOverrides: { useCustomDragHandle: true } });
expect(panel.queryByTestId('kbnGridPanel--dragHandle')).not.toBeInTheDocument();
expect(mockRenderPanelContents).toHaveBeenCalledWith('panel1', expect.any(Function));
});
});
});

View file

@ -7,30 +7,28 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo } from 'react';
import { combineLatest, skip } from 'rxjs';
import { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { GridLayoutStateManager } from '../types';
import { DragHandle, DragHandleApi } from './drag_handle';
import { useGridLayoutContext } from '../use_grid_layout_context';
import { DefaultDragHandle } from './drag_handle/default_drag_handle';
import { useDragHandleApi } from './drag_handle/use_drag_handle_api';
import { ResizeHandle } from './resize_handle';
export interface GridPanelProps {
panelId: string;
rowIndex: number;
renderPanelContents: (
panelId: string,
setDragHandles?: (refs: Array<HTMLElement | null>) => void
) => React.ReactNode;
gridLayoutStateManager: GridLayoutStateManager;
}
export const GridPanel = React.memo(
({ panelId, rowIndex, renderPanelContents, gridLayoutStateManager }: GridPanelProps) => {
const [dragHandleApi, setDragHandleApi] = useState<DragHandleApi | null>(null);
export const GridPanel = React.memo(({ panelId, rowIndex }: GridPanelProps) => {
const { gridLayoutStateManager, useCustomDragHandle, renderPanelContents } =
useGridLayoutContext();
const { euiTheme } = useEuiTheme();
const dragHandleApi = useDragHandleApi({ panelId, rowIndex });
/** Set initial styles based on state at mount to prevent styles from "blipping" */
const initialStyles = useMemo(() => {
@ -156,7 +154,6 @@ export const GridPanel = React.memo(
* Memoize panel contents to prevent unnecessary re-renders
*/
const panelContents = useMemo(() => {
if (!dragHandleApi) return <></>; // delays the rendering of the panel until after dragHandleApi is defined
return renderPanelContents(panelId, dragHandleApi.setDragHandles);
}, [panelId, renderPanelContents, dragHandleApi]);
@ -171,21 +168,11 @@ export const GridPanel = React.memo(
css={initialStyles}
className="kbnGridPanel"
>
<DragHandle
ref={setDragHandleApi}
gridLayoutStateManager={gridLayoutStateManager}
panelId={panelId}
rowIndex={rowIndex}
/>
{!useCustomDragHandle && <DefaultDragHandle dragHandleApi={dragHandleApi} />}
{panelContents}
<ResizeHandle
gridLayoutStateManager={gridLayoutStateManager}
panelId={panelId}
rowIndex={rowIndex}
/>
<ResizeHandle panelId={panelId} rowIndex={rowIndex} />
</div>
);
}
);
});
GridPanel.displayName = 'KbnGridLayoutPanel';

View file

@ -13,22 +13,12 @@ import type { UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { GridLayoutStateManager } from '../types';
import { useGridLayoutEvents } from '../use_grid_layout_events';
export const ResizeHandle = React.memo(
({
gridLayoutStateManager,
rowIndex,
panelId,
}: {
gridLayoutStateManager: GridLayoutStateManager;
rowIndex: number;
panelId: string;
}) => {
({ rowIndex, panelId }: { rowIndex: number; panelId: string }) => {
const startInteraction = useGridLayoutEvents({
interactionType: 'resize',
gridLayoutStateManager,
panelId,
rowIndex,
});

View file

@ -9,19 +9,28 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { GridRow, GridRowProps } from './grid_row';
import { GridRow, type GridRowProps } from './grid_row';
import { gridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks';
import { getSampleLayout } from '../test_utils/sample_layout';
import { GridLayoutContext, type GridLayoutContextType } from '../use_grid_layout_context';
describe('GridRow', () => {
const renderGridRow = (propsOverrides: Partial<GridRowProps> = {}) => {
const renderGridRow = (
propsOverrides: Partial<GridRowProps> = {},
contextOverrides: Partial<GridLayoutContextType> = {}
) => {
return render(
<GridRow
rowIndex={0}
renderPanelContents={mockRenderPanelContents}
gridLayoutStateManager={gridLayoutStateManagerMock}
{...propsOverrides}
/>
<GridLayoutContext.Provider
value={
{
renderPanelContents: mockRenderPanelContents,
gridLayoutStateManager: gridLayoutStateManagerMock,
...contextOverrides,
} as GridLayoutContextType
}
>
<GridRow rowIndex={0} {...propsOverrides} />
</GridLayoutContext.Provider>
);
};

View file

@ -15,21 +15,17 @@ import { css } from '@emotion/react';
import { DragPreview } from '../drag_preview';
import { GridPanel } from '../grid_panel';
import { GridLayoutStateManager } from '../types';
import { useGridLayoutContext } from '../use_grid_layout_context';
import { getKeysInOrder } from '../utils/resolve_grid_row';
import { GridRowHeader } from './grid_row_header';
export interface GridRowProps {
rowIndex: number;
renderPanelContents: (
panelId: string,
setDragHandles?: (refs: Array<HTMLElement | null>) => void
) => React.ReactNode;
gridLayoutStateManager: GridLayoutStateManager;
}
export const GridRow = React.memo(
({ rowIndex, renderPanelContents, gridLayoutStateManager }: GridRowProps) => {
export const GridRow = React.memo(({ rowIndex }: GridRowProps) => {
const { gridLayoutStateManager } = useGridLayoutContext();
const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex];
const [panelIdsInOrder, setPanelIdsInOrder] = useState<string[]>(() =>
@ -101,14 +97,12 @@ export const GridRow = React.memo(
* the order of rendered panels need to be aligned with how they are displayed in the grid for accessibility
* reasons (screen readers and focus management).
*/
const gridLayoutSubscription = gridLayoutStateManager.gridLayout$.subscribe(
(gridLayout) => {
const gridLayoutSubscription = gridLayoutStateManager.gridLayout$.subscribe((gridLayout) => {
const newPanelIdsInOrder = getKeysInOrder(gridLayout[rowIndex].panels);
if (panelIdsInOrder.join() !== newPanelIdsInOrder.join()) {
setPanelIdsInOrder(newPanelIdsInOrder);
}
}
);
});
const columnCountSubscription = gridLayoutStateManager.runtimeSettings$
.pipe(
@ -155,21 +149,14 @@ export const GridRow = React.memo(
>
{/* render the panels **in order** for accessibility, using the memoized panel components */}
{panelIdsInOrder.map((panelId) => (
<GridPanel
key={panelId}
panelId={panelId}
rowIndex={rowIndex}
gridLayoutStateManager={gridLayoutStateManager}
renderPanelContents={renderPanelContents}
/>
<GridPanel key={panelId} panelId={panelId} rowIndex={rowIndex} />
))}
<DragPreview rowIndex={rowIndex} gridLayoutStateManager={gridLayoutStateManager} />
<DragPreview rowIndex={rowIndex} />
</div>
)}
</div>
);
}
);
});
const styles = {
fullHeight: css({

View file

@ -107,18 +107,24 @@ export interface PanelInteractionEvent {
};
}
// TODO: Remove from Dashboard plugin as part of https://github.com/elastic/kibana/issues/190446
export enum PanelPlacementStrategy {
/** Place on the very top of the grid layout, add the height of this panel to all other panels. */
placeAtTop = 'placeAtTop',
/** Look for the smallest y and x value where the default panel will fit. */
findTopLeftMostOpenSpace = 'findTopLeftMostOpenSpace',
}
export interface PanelPlacementSettings {
strategy?: PanelPlacementStrategy;
height: number;
width: number;
}
/**
* This type is used to conditionally change the type of `renderPanelContents` depending
* on the value of `useCustomDragHandle`
*/
export type UseCustomDragHandle =
| {
useCustomDragHandle: true;
renderPanelContents: (
panelId: string,
setDragHandles: (refs: Array<HTMLElement | null>) => void
) => React.ReactNode;
}
| {
useCustomDragHandle?: false;
renderPanelContents: (panelId: string) => React.ReactNode;
};
/**
* Controls whether the resize + drag handles are visible and functioning
*/
export type GridAccessMode = 'VIEW' | 'EDIT';

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { createContext, useContext } from 'react';
import { GridLayoutStateManager } from './types';
export interface GridLayoutContextType<UseCustomDragHandle extends boolean = boolean> {
gridLayoutStateManager: GridLayoutStateManager;
useCustomDragHandle: UseCustomDragHandle;
renderPanelContents: (
panelId: string,
setDragHandles: UseCustomDragHandle extends true
? (refs: Array<HTMLElement | null>) => void
: undefined
) => React.ReactNode;
}
export const GridLayoutContext = createContext<GridLayoutContextType | undefined>(undefined);
export const useGridLayoutContext = (): GridLayoutContextType => {
const context = useContext<GridLayoutContextType | undefined>(GridLayoutContext);
if (!context) {
throw new Error('useGridLayoutContext must be used inside GridLayoutContext');
}
return context;
};

View file

@ -18,6 +18,7 @@ import {
} from './sensors';
import { commitAction, moveAction, startAction } from './state_manager_actions';
import { UserInteractionEvent } from './types';
import { useGridLayoutContext } from '../use_grid_layout_context';
/*
* This hook sets up and manages drag/resize interaction logic for grid panels.
@ -28,15 +29,15 @@ import { UserInteractionEvent } from './types';
export const useGridLayoutEvents = ({
interactionType,
gridLayoutStateManager,
rowIndex,
panelId,
}: {
interactionType: PanelInteractionEvent['type'];
gridLayoutStateManager: GridLayoutStateManager;
rowIndex: number;
panelId: string;
}) => {
const { gridLayoutStateManager } = useGridLayoutContext();
const lastRequestedPanelPosition = useRef<GridPanelData | undefined>(undefined);
const pointerPixel = useRef<{ clientX: number; clientY: number }>({ clientX: 0, clientY: 0 });

View file

@ -97,7 +97,7 @@ export const DashboardGrid = ({
);
const renderPanelContents = useCallback(
(id: string, setDragHandles?: (refs: Array<HTMLElement | null>) => void) => {
(id: string, setDragHandles: (refs: Array<HTMLElement | null>) => void) => {
const currentPanels = dashboardApi.panels$.getValue();
if (!currentPanels[id]) return;
@ -132,6 +132,7 @@ export const DashboardGrid = ({
rowHeight: DASHBOARD_GRID_HEIGHT,
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
}}
useCustomDragHandle={true}
renderPanelContents={renderPanelContents}
onLayoutChange={onLayoutChange}
expandedPanelId={expandedPanelId}