Skip to content

Commit 200922a

Browse files
authored
[kbn-grid-layout] Add useCustomDragHandle prop (#210463)
This PR **might** resolve #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)
1 parent 84fdbcb commit 200922a

17 files changed

Lines changed: 578 additions & 522 deletions

File tree

examples/grid_example/public/app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ export const GridExample = ({
337337
rowHeight,
338338
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
339339
}}
340+
useCustomDragHandle={true}
340341
renderPanelContents={renderPanelContents}
341342
onLayoutChange={(newLayout) => {
342343
const { panels, rows } = gridLayoutToDashboardPanelMap(

src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx

Lines changed: 40 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,53 +11,46 @@ import React, { useEffect, useRef } from 'react';
1111
import { combineLatest, skip } from 'rxjs';
1212

1313
import { css } from '@emotion/react';
14-
15-
import { GridLayoutStateManager } from './types';
16-
17-
export const DragPreview = React.memo(
18-
({
19-
rowIndex,
20-
gridLayoutStateManager,
21-
}: {
22-
rowIndex: number;
23-
gridLayoutStateManager: GridLayoutStateManager;
24-
}) => {
25-
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
26-
27-
useEffect(
28-
() => {
29-
/** Update the styles of the drag preview via a subscription to prevent re-renders */
30-
const styleSubscription = combineLatest([
31-
gridLayoutStateManager.activePanel$,
32-
gridLayoutStateManager.proposedGridLayout$,
33-
])
34-
.pipe(skip(1)) // skip the first emit because the drag preview is only rendered after a user action
35-
.subscribe(([activePanel, proposedGridLayout]) => {
36-
if (!dragPreviewRef.current) return;
37-
38-
if (!activePanel || !proposedGridLayout?.[rowIndex].panels[activePanel.id]) {
39-
dragPreviewRef.current.style.display = 'none';
40-
} else {
41-
const panel = proposedGridLayout[rowIndex].panels[activePanel.id];
42-
dragPreviewRef.current.style.display = 'block';
43-
dragPreviewRef.current.style.gridColumnStart = `${panel.column + 1}`;
44-
dragPreviewRef.current.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
45-
dragPreviewRef.current.style.gridRowStart = `${panel.row + 1}`;
46-
dragPreviewRef.current.style.gridRowEnd = `${panel.row + 1 + panel.height}`;
47-
}
48-
});
49-
50-
return () => {
51-
styleSubscription.unsubscribe();
52-
};
53-
},
54-
// eslint-disable-next-line react-hooks/exhaustive-deps
55-
[]
56-
);
57-
58-
return <div ref={dragPreviewRef} className={'kbnGridPanel--dragPreview'} css={styles} />;
59-
}
60-
);
14+
import { useGridLayoutContext } from './use_grid_layout_context';
15+
16+
export const DragPreview = React.memo(({ rowIndex }: { rowIndex: number }) => {
17+
const { gridLayoutStateManager } = useGridLayoutContext();
18+
19+
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
20+
21+
useEffect(
22+
() => {
23+
/** Update the styles of the drag preview via a subscription to prevent re-renders */
24+
const styleSubscription = combineLatest([
25+
gridLayoutStateManager.activePanel$,
26+
gridLayoutStateManager.proposedGridLayout$,
27+
])
28+
.pipe(skip(1)) // skip the first emit because the drag preview is only rendered after a user action
29+
.subscribe(([activePanel, proposedGridLayout]) => {
30+
if (!dragPreviewRef.current) return;
31+
32+
if (!activePanel || !proposedGridLayout?.[rowIndex].panels[activePanel.id]) {
33+
dragPreviewRef.current.style.display = 'none';
34+
} else {
35+
const panel = proposedGridLayout[rowIndex].panels[activePanel.id];
36+
dragPreviewRef.current.style.display = 'block';
37+
dragPreviewRef.current.style.gridColumnStart = `${panel.column + 1}`;
38+
dragPreviewRef.current.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
39+
dragPreviewRef.current.style.gridRowStart = `${panel.row + 1}`;
40+
dragPreviewRef.current.style.gridRowEnd = `${panel.row + 1 + panel.height}`;
41+
}
42+
});
43+
44+
return () => {
45+
styleSubscription.unsubscribe();
46+
};
47+
},
48+
// eslint-disable-next-line react-hooks/exhaustive-deps
49+
[]
50+
);
51+
52+
return <div ref={dragPreviewRef} className={'kbnGridPanel--dragPreview'} css={styles} />;
53+
});
6154

6255
const styles = css({ display: 'none', pointerEvents: 'none' });
6356

src/platform/packages/private/kbn-grid-layout/grid/grid_height_smoother.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88
*/
99

1010
import { css } from '@emotion/react';
11-
import React, { PropsWithChildren, useEffect, useRef } from 'react';
11+
import React, { useEffect, useRef } from 'react';
1212
import { combineLatest } from 'rxjs';
13-
import { GridLayoutStateManager } from './types';
13+
import { useGridLayoutContext } from './use_grid_layout_context';
1414

1515
export const GridHeightSmoother = React.memo(
16-
({
17-
children,
18-
gridLayoutStateManager,
19-
}: PropsWithChildren<{ gridLayoutStateManager: GridLayoutStateManager }>) => {
16+
({ children }: { children: React.ReactNode | undefined }) => {
17+
const { gridLayoutStateManager } = useGridLayoutContext();
18+
2019
// set the parent div size directly to smooth out height changes.
2120
const smoothHeightRef = useRef<HTMLDivElement | null>(null);
2221

src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,23 @@ import {
2525
const onLayoutChange = jest.fn();
2626

2727
const renderGridLayout = (propsOverrides: Partial<GridLayoutProps> = {}) => {
28-
const defaultProps: GridLayoutProps = {
28+
const props = {
2929
accessMode: 'EDIT',
3030
layout: getSampleLayout(),
3131
gridSettings,
3232
renderPanelContents: mockRenderPanelContents,
3333
onLayoutChange,
34-
};
34+
...propsOverrides,
35+
} as GridLayoutProps;
3536

36-
const { rerender, ...rtlRest } = render(<GridLayout {...defaultProps} {...propsOverrides} />);
37+
const { rerender, ...rtlRest } = render(<GridLayout {...props} />);
3738

3839
return {
3940
...rtlRest,
40-
rerender: (overrides: Partial<GridLayoutProps>) =>
41-
rerender(<GridLayout {...defaultProps} {...overrides} />),
41+
rerender: (overrides: Partial<GridLayoutProps>) => {
42+
const newProps = { ...props, ...overrides } as GridLayoutProps;
43+
return rerender(<GridLayout {...newProps} />);
44+
},
4245
};
4346
};
4447

src/platform/packages/private/kbn-grid-layout/grid/grid_layout.tsx

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,27 @@
99

1010
import classNames from 'classnames';
1111
import { cloneDeep } from 'lodash';
12-
import React, { useEffect, useRef, useState } from 'react';
12+
import React, { useEffect, useMemo, useRef, useState } from 'react';
1313
import { combineLatest, distinctUntilChanged, map, pairwise, skip } from 'rxjs';
1414

1515
import { css } from '@emotion/react';
1616

1717
import { GridHeightSmoother } from './grid_height_smoother';
1818
import { GridRow } from './grid_row';
19-
import { GridAccessMode, GridLayoutData, GridSettings } from './types';
19+
import { GridAccessMode, GridLayoutData, GridSettings, UseCustomDragHandle } from './types';
20+
import { GridLayoutContext, GridLayoutContextType } from './use_grid_layout_context';
2021
import { useGridLayoutState } from './use_grid_layout_state';
2122
import { isLayoutEqual } from './utils/equality_checks';
2223
import { resolveGridRow } from './utils/resolve_grid_row';
2324

24-
export interface GridLayoutProps {
25+
export type GridLayoutProps = {
2526
layout: GridLayoutData;
2627
gridSettings: GridSettings;
27-
renderPanelContents: (
28-
panelId: string,
29-
setDragHandles?: (refs: Array<HTMLElement | null>) => void
30-
) => React.ReactNode;
3128
onLayoutChange: (newLayout: GridLayoutData) => void;
3229
expandedPanelId?: string;
3330
accessMode?: GridAccessMode;
3431
className?: string; // this makes it so that custom CSS can be passed via Emotion
35-
}
32+
} & UseCustomDragHandle;
3633

3734
export const GridLayout = ({
3835
layout,
@@ -42,6 +39,7 @@ export const GridLayout = ({
4239
expandedPanelId,
4340
accessMode = 'EDIT',
4441
className,
42+
useCustomDragHandle = false,
4543
}: GridLayoutProps) => {
4644
const layoutRef = useRef<HTMLDivElement | null>(null);
4745
const { gridLayoutStateManager, setDimensionsRef } = useGridLayoutState({
@@ -134,33 +132,38 @@ export const GridLayout = ({
134132
// eslint-disable-next-line react-hooks/exhaustive-deps
135133
}, []);
136134

135+
const memoizedContext = useMemo(
136+
() =>
137+
({
138+
renderPanelContents,
139+
useCustomDragHandle,
140+
gridLayoutStateManager,
141+
} as GridLayoutContextType),
142+
[renderPanelContents, useCustomDragHandle, gridLayoutStateManager]
143+
);
144+
137145
return (
138-
<GridHeightSmoother gridLayoutStateManager={gridLayoutStateManager}>
139-
<div
140-
ref={(divElement) => {
141-
layoutRef.current = divElement;
142-
setDimensionsRef(divElement);
143-
}}
144-
className={classNames('kbnGrid', className)}
145-
css={[
146-
styles.layoutPadding,
147-
styles.hasActivePanel,
148-
styles.singleColumn,
149-
styles.hasExpandedPanel,
150-
]}
151-
>
152-
{Array.from({ length: rowCount }, (_, rowIndex) => {
153-
return (
154-
<GridRow
155-
key={rowIndex}
156-
rowIndex={rowIndex}
157-
renderPanelContents={renderPanelContents}
158-
gridLayoutStateManager={gridLayoutStateManager}
159-
/>
160-
);
161-
})}
162-
</div>
163-
</GridHeightSmoother>
146+
<GridLayoutContext.Provider value={memoizedContext}>
147+
<GridHeightSmoother>
148+
<div
149+
ref={(divElement) => {
150+
layoutRef.current = divElement;
151+
setDimensionsRef(divElement);
152+
}}
153+
className={classNames('kbnGrid', className)}
154+
css={[
155+
styles.layoutPadding,
156+
styles.hasActivePanel,
157+
styles.singleColumn,
158+
styles.hasExpandedPanel,
159+
]}
160+
>
161+
{Array.from({ length: rowCount }, (_, rowIndex) => {
162+
return <GridRow key={rowIndex} rowIndex={rowIndex} />;
163+
})}
164+
</div>
165+
</GridHeightSmoother>
166+
</GridLayoutContext.Provider>
164167
);
165168
};
166169

src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/default_drag_handle.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ import React from 'react';
1010
import { EuiIcon, type UseEuiTheme } from '@elastic/eui';
1111
import { css } from '@emotion/react';
1212
import { i18n } from '@kbn/i18n';
13-
import { UserInteractionEvent } from '../../use_grid_layout_events/types';
13+
import { DragHandleApi } from './use_drag_handle_api';
1414

1515
export const DefaultDragHandle = React.memo(
16-
({ onDragStart }: { onDragStart: (e: UserInteractionEvent) => void }) => {
16+
({ dragHandleApi }: { dragHandleApi: DragHandleApi }) => {
1717
return (
1818
<button
19-
onMouseDown={onDragStart}
20-
onTouchStart={onDragStart}
19+
onMouseDown={dragHandleApi.startDrag}
20+
onTouchStart={dragHandleApi.startDrag}
2121
aria-label={i18n.translate('kbnGridLayout.dragHandle.ariaLabel', {
2222
defaultMessage: 'Drag to move',
2323
})}
2424
className="kbnGridPanel__dragHandle"
25+
data-test-subj="kbnGridPanel--dragHandle"
2526
css={styles}
2627
>
2728
<EuiIcon type="grabOmnidirectional" />

src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/index.tsx

Lines changed: 0 additions & 73 deletions
This file was deleted.

0 commit comments

Comments
 (0)