mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Collapsable panels] Refactor forward refs (#208360)
## Summary This PR fixes a few very small issues: 1. Removes this warning via setting explicity `touchstart` passive. I read that `touchstart` is passive by default, but apparently it varies between browsers. <img width="1053" alt="Screenshot 2025-01-27 at 14 04 26" src="https://github.com/user-attachments/assets/0d641575-df6c-429c-a731-e9f41dc9ec65" /> 2. Removes the `containerRef` that we stopped using, but didn't remove the variable. 3. Sets the refs for `rowRefs` and `panelRefs` inside the component instead of passing `forwardRefs` and passing it on parent components. Unless I am missing something, there's no reason for adding this complexity. Plus `forwardRef` is deprecated in React 19 so it's good to remove now :) 4. Adds `max-height: 100vh` for expanded version of gridHeightSmoother. We need that, since setting it to 100% right now will not always work properly if parent won't set up its height. The problem is very visible in our example app with Lens datatable, (uses EuiDataGrid underneath). When we maximize the datatable panel, it will grow forever and cause a lot of console errors about Resize Observers.
This commit is contained in:
parent
3e0fd1f82e
commit
00d822d88a
5 changed files with 309 additions and 306 deletions
|
@ -60,6 +60,7 @@ export const GridHeightSmoother = ({
|
|||
|
||||
&:has(.kbnGridPanel--expanded) {
|
||||
min-height: 100% !important;
|
||||
max-height: 100vh; // fallback in case if the parent doesn't set the height correctly
|
||||
position: relative;
|
||||
transition: none;
|
||||
}
|
||||
|
|
|
@ -145,9 +145,6 @@ export const GridLayout = ({
|
|||
rowIndex={rowIndex}
|
||||
renderPanelContents={renderPanelContents}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
ref={(element: HTMLDivElement | null) =>
|
||||
(gridLayoutStateManager.rowRefs.current[rowIndex] = element)
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -41,7 +41,7 @@ export const DragHandle = React.forwardRef<
|
|||
for (const handle of dragHandles) {
|
||||
if (handle === null) return;
|
||||
handle.addEventListener('mousedown', startInteraction, { passive: true });
|
||||
handle.addEventListener('touchstart', startInteraction);
|
||||
handle.addEventListener('touchstart', startInteraction, { passive: true });
|
||||
handle.style.touchAction = 'none';
|
||||
}
|
||||
removeEventListenersRef.current = () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { forwardRef, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { combineLatest, skip } from 'rxjs';
|
||||
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
|
@ -27,154 +27,166 @@ export interface GridPanelProps {
|
|||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}
|
||||
|
||||
export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
|
||||
({ panelId, rowIndex, renderPanelContents, gridLayoutStateManager }, panelRef) => {
|
||||
const [dragHandleApi, setDragHandleApi] = useState<DragHandleApi | null>(null);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
export const GridPanel = ({
|
||||
panelId,
|
||||
rowIndex,
|
||||
renderPanelContents,
|
||||
gridLayoutStateManager,
|
||||
}: GridPanelProps) => {
|
||||
const [dragHandleApi, setDragHandleApi] = useState<DragHandleApi | null>(null);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
/** Set initial styles based on state at mount to prevent styles from "blipping" */
|
||||
const initialStyles = useMemo(() => {
|
||||
const initialPanel = (gridLayoutStateManager.proposedGridLayout$.getValue() ??
|
||||
gridLayoutStateManager.gridLayout$.getValue())[rowIndex].panels[panelId];
|
||||
return css`
|
||||
position: relative;
|
||||
height: calc(
|
||||
1px *
|
||||
(
|
||||
${initialPanel.height} * (var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) -
|
||||
var(--kbnGridGutterSize)
|
||||
)
|
||||
);
|
||||
grid-column-start: ${initialPanel.column + 1};
|
||||
grid-column-end: ${initialPanel.column + 1 + initialPanel.width};
|
||||
grid-row-start: ${initialPanel.row + 1};
|
||||
grid-row-end: ${initialPanel.row + 1 + initialPanel.height};
|
||||
`;
|
||||
}, [gridLayoutStateManager, rowIndex, panelId]);
|
||||
/** Set initial styles based on state at mount to prevent styles from "blipping" */
|
||||
const initialStyles = useMemo(() => {
|
||||
const initialPanel = (gridLayoutStateManager.proposedGridLayout$.getValue() ??
|
||||
gridLayoutStateManager.gridLayout$.getValue())[rowIndex].panels[panelId];
|
||||
return css`
|
||||
position: relative;
|
||||
height: calc(
|
||||
1px *
|
||||
(
|
||||
${initialPanel.height} * (var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) -
|
||||
var(--kbnGridGutterSize)
|
||||
)
|
||||
);
|
||||
grid-column-start: ${initialPanel.column + 1};
|
||||
grid-column-end: ${initialPanel.column + 1 + initialPanel.width};
|
||||
grid-row-start: ${initialPanel.row + 1};
|
||||
grid-row-end: ${initialPanel.row + 1 + initialPanel.height};
|
||||
`;
|
||||
}, [gridLayoutStateManager, rowIndex, panelId]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
/** Update the styles of the panel via a subscription to prevent re-renders */
|
||||
const activePanelStyleSubscription = combineLatest([
|
||||
gridLayoutStateManager.activePanel$,
|
||||
gridLayoutStateManager.gridLayout$,
|
||||
gridLayoutStateManager.proposedGridLayout$,
|
||||
])
|
||||
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
|
||||
.subscribe(([activePanel, gridLayout, proposedGridLayout]) => {
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
const panel = (proposedGridLayout ?? gridLayout)[rowIndex].panels[panelId];
|
||||
if (!ref || !panel) return;
|
||||
useEffect(
|
||||
() => {
|
||||
/** Update the styles of the panel via a subscription to prevent re-renders */
|
||||
const activePanelStyleSubscription = combineLatest([
|
||||
gridLayoutStateManager.activePanel$,
|
||||
gridLayoutStateManager.gridLayout$,
|
||||
gridLayoutStateManager.proposedGridLayout$,
|
||||
])
|
||||
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
|
||||
.subscribe(([activePanel, gridLayout, proposedGridLayout]) => {
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
const panel = (proposedGridLayout ?? gridLayout)[rowIndex].panels[panelId];
|
||||
if (!ref || !panel) return;
|
||||
|
||||
const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
|
||||
const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
|
||||
|
||||
if (panelId === activePanel?.id) {
|
||||
ref.classList.add('kbnGridPanel--active');
|
||||
if (panelId === activePanel?.id) {
|
||||
ref.classList.add('kbnGridPanel--active');
|
||||
|
||||
// if the current panel is active, give it fixed positioning depending on the interaction event
|
||||
const { position: draggingPosition } = activePanel;
|
||||
const runtimeSettings = gridLayoutStateManager.runtimeSettings$.getValue();
|
||||
// if the current panel is active, give it fixed positioning depending on the interaction event
|
||||
const { position: draggingPosition } = activePanel;
|
||||
const runtimeSettings = gridLayoutStateManager.runtimeSettings$.getValue();
|
||||
|
||||
ref.style.zIndex = `${euiTheme.levels.modal}`;
|
||||
if (currentInteractionEvent?.type === 'resize') {
|
||||
// if the current panel is being resized, ensure it is not shrunk past the size of a single cell
|
||||
ref.style.width = `${Math.max(
|
||||
draggingPosition.right - draggingPosition.left,
|
||||
runtimeSettings.columnPixelWidth
|
||||
)}px`;
|
||||
ref.style.height = `${Math.max(
|
||||
draggingPosition.bottom - draggingPosition.top,
|
||||
runtimeSettings.rowHeight
|
||||
)}px`;
|
||||
ref.style.zIndex = `${euiTheme.levels.modal}`;
|
||||
if (currentInteractionEvent?.type === 'resize') {
|
||||
// if the current panel is being resized, ensure it is not shrunk past the size of a single cell
|
||||
ref.style.width = `${Math.max(
|
||||
draggingPosition.right - draggingPosition.left,
|
||||
runtimeSettings.columnPixelWidth
|
||||
)}px`;
|
||||
ref.style.height = `${Math.max(
|
||||
draggingPosition.bottom - draggingPosition.top,
|
||||
runtimeSettings.rowHeight
|
||||
)}px`;
|
||||
|
||||
// undo any "lock to grid" styles **except** for the top left corner, which stays locked
|
||||
ref.style.gridColumnStart = `${panel.column + 1}`;
|
||||
ref.style.gridRowStart = `${panel.row + 1}`;
|
||||
ref.style.gridColumnEnd = `auto`;
|
||||
ref.style.gridRowEnd = `auto`;
|
||||
} else {
|
||||
// if the current panel is being dragged, render it with a fixed position + size
|
||||
ref.style.position = 'fixed';
|
||||
|
||||
ref.style.left = `${draggingPosition.left}px`;
|
||||
ref.style.top = `${draggingPosition.top}px`;
|
||||
ref.style.width = `${draggingPosition.right - draggingPosition.left}px`;
|
||||
ref.style.height = `${draggingPosition.bottom - draggingPosition.top}px`;
|
||||
|
||||
// undo any "lock to grid" styles
|
||||
ref.style.gridArea = `auto`; // shortcut to set all grid styles to `auto`
|
||||
}
|
||||
} else {
|
||||
ref.classList.remove('kbnGridPanel--active');
|
||||
|
||||
ref.style.zIndex = `auto`;
|
||||
|
||||
// if the panel is not being dragged and/or resized, undo any fixed position styles
|
||||
ref.style.position = '';
|
||||
ref.style.left = ``;
|
||||
ref.style.top = ``;
|
||||
ref.style.width = ``;
|
||||
// setting the height is necessary for mobile mode
|
||||
ref.style.height = `calc(1px * (${panel.height} * (var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) - var(--kbnGridGutterSize)))`;
|
||||
|
||||
// and render the panel locked to the grid
|
||||
// undo any "lock to grid" styles **except** for the top left corner, which stays locked
|
||||
ref.style.gridColumnStart = `${panel.column + 1}`;
|
||||
ref.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
|
||||
ref.style.gridRowStart = `${panel.row + 1}`;
|
||||
ref.style.gridRowEnd = `${panel.row + 1 + panel.height}`;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This subscription adds and/or removes the necessary class name for expanded panel styling
|
||||
*/
|
||||
const expandedPanelSubscription = gridLayoutStateManager.expandedPanelId$.subscribe(
|
||||
(expandedPanelId) => {
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
const gridLayout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
const panel = gridLayout[rowIndex].panels[panelId];
|
||||
if (!ref || !panel) return;
|
||||
|
||||
if (expandedPanelId && expandedPanelId === panelId) {
|
||||
ref.classList.add('kbnGridPanel--expanded');
|
||||
ref.style.gridColumnEnd = `auto`;
|
||||
ref.style.gridRowEnd = `auto`;
|
||||
} else {
|
||||
ref.classList.remove('kbnGridPanel--expanded');
|
||||
// if the current panel is being dragged, render it with a fixed position + size
|
||||
ref.style.position = 'fixed';
|
||||
|
||||
ref.style.left = `${draggingPosition.left}px`;
|
||||
ref.style.top = `${draggingPosition.top}px`;
|
||||
ref.style.width = `${draggingPosition.right - draggingPosition.left}px`;
|
||||
ref.style.height = `${draggingPosition.bottom - draggingPosition.top}px`;
|
||||
|
||||
// undo any "lock to grid" styles
|
||||
ref.style.gridArea = `auto`; // shortcut to set all grid styles to `auto`
|
||||
}
|
||||
} else {
|
||||
ref.classList.remove('kbnGridPanel--active');
|
||||
|
||||
ref.style.zIndex = `auto`;
|
||||
|
||||
// if the panel is not being dragged and/or resized, undo any fixed position styles
|
||||
ref.style.position = '';
|
||||
ref.style.left = ``;
|
||||
ref.style.top = ``;
|
||||
ref.style.width = ``;
|
||||
// setting the height is necessary for mobile mode
|
||||
ref.style.height = `calc(1px * (${panel.height} * (var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) - var(--kbnGridGutterSize)))`;
|
||||
|
||||
// and render the panel locked to the grid
|
||||
ref.style.gridColumnStart = `${panel.column + 1}`;
|
||||
ref.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
|
||||
ref.style.gridRowStart = `${panel.row + 1}`;
|
||||
ref.style.gridRowEnd = `${panel.row + 1 + panel.height}`;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
expandedPanelSubscription.unsubscribe();
|
||||
activePanelStyleSubscription.unsubscribe();
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
/**
|
||||
* This subscription adds and/or removes the necessary class name for expanded panel styling
|
||||
*/
|
||||
const expandedPanelSubscription = gridLayoutStateManager.expandedPanelId$.subscribe(
|
||||
(expandedPanelId) => {
|
||||
const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
const gridLayout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
const panel = gridLayout[rowIndex].panels[panelId];
|
||||
if (!ref || !panel) return;
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
if (expandedPanelId && expandedPanelId === panelId) {
|
||||
ref.classList.add('kbnGridPanel--expanded');
|
||||
} else {
|
||||
ref.classList.remove('kbnGridPanel--expanded');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={panelRef} css={initialStyles} className="kbnGridPanel">
|
||||
<DragHandle
|
||||
ref={setDragHandleApi}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
/>
|
||||
{panelContents}
|
||||
<ResizeHandle
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
expandedPanelSubscription.unsubscribe();
|
||||
activePanelStyleSubscription.unsubscribe();
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(element) => {
|
||||
if (!gridLayoutStateManager.panelRefs.current[rowIndex]) {
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex] = {};
|
||||
}
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element;
|
||||
}}
|
||||
css={initialStyles}
|
||||
className="kbnGridPanel"
|
||||
>
|
||||
<DragHandle
|
||||
ref={setDragHandleApi}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
/>
|
||||
{panelContents}
|
||||
<ResizeHandle
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { map, pairwise, skip, combineLatest } from 'rxjs';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
@ -27,177 +27,170 @@ export interface GridRowProps {
|
|||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}
|
||||
|
||||
export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
||||
({ rowIndex, renderPanelContents, gridLayoutStateManager }, gridRef) => {
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex];
|
||||
export const GridRow = ({
|
||||
rowIndex,
|
||||
renderPanelContents,
|
||||
gridLayoutStateManager,
|
||||
}: GridRowProps) => {
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex];
|
||||
|
||||
const [panelIds, setPanelIds] = useState<string[]>(Object.keys(currentRow.panels));
|
||||
const [panelIdsInOrder, setPanelIdsInOrder] = useState<string[]>(() =>
|
||||
getKeysInOrder(currentRow.panels)
|
||||
);
|
||||
const [rowTitle, setRowTitle] = useState<string>(currentRow.title);
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(currentRow.isCollapsed);
|
||||
const [panelIds, setPanelIds] = useState<string[]>(Object.keys(currentRow.panels));
|
||||
const [panelIdsInOrder, setPanelIdsInOrder] = useState<string[]>(() =>
|
||||
getKeysInOrder(currentRow.panels)
|
||||
);
|
||||
const [rowTitle, setRowTitle] = useState<string>(currentRow.title);
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(currentRow.isCollapsed);
|
||||
|
||||
const rowContainer = useRef<HTMLDivElement | null>(null);
|
||||
/** Set initial styles based on state at mount to prevent styles from "blipping" */
|
||||
const initialStyles = useMemo(() => {
|
||||
const { columnCount } = gridLayoutStateManager.runtimeSettings$.getValue();
|
||||
return css`
|
||||
grid-auto-rows: calc(var(--kbnGridRowHeight) * 1px);
|
||||
grid-template-columns: repeat(${columnCount}, minmax(0, 1fr));
|
||||
gap: calc(var(--kbnGridGutterSize) * 1px);
|
||||
`;
|
||||
}, [gridLayoutStateManager]);
|
||||
|
||||
/** Set initial styles based on state at mount to prevent styles from "blipping" */
|
||||
const initialStyles = useMemo(() => {
|
||||
const { columnCount } = gridLayoutStateManager.runtimeSettings$.getValue();
|
||||
return css`
|
||||
grid-auto-rows: calc(var(--kbnGridRowHeight) * 1px);
|
||||
grid-template-columns: repeat(${columnCount}, minmax(0, 1fr));
|
||||
gap: calc(var(--kbnGridGutterSize) * 1px);
|
||||
`;
|
||||
}, [gridLayoutStateManager]);
|
||||
useEffect(
|
||||
() => {
|
||||
/** Update the styles of the grid row via a subscription to prevent re-renders */
|
||||
const interactionStyleSubscription = gridLayoutStateManager.interactionEvent$
|
||||
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
|
||||
.subscribe((interactionEvent) => {
|
||||
const rowRef = gridLayoutStateManager.rowRefs.current[rowIndex];
|
||||
if (!rowRef) return;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
/** Update the styles of the grid row via a subscription to prevent re-renders */
|
||||
const interactionStyleSubscription = gridLayoutStateManager.interactionEvent$
|
||||
.pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
|
||||
.subscribe((interactionEvent) => {
|
||||
const rowRef = gridLayoutStateManager.rowRefs.current[rowIndex];
|
||||
if (!rowRef) return;
|
||||
|
||||
const targetRow = interactionEvent?.targetRowIndex;
|
||||
if (rowIndex === targetRow && interactionEvent) {
|
||||
rowRef.classList.add('kbnGridRow--targeted');
|
||||
} else {
|
||||
rowRef.classList.remove('kbnGridRow--targeted');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This subscription ensures that the row will re-render when one of the following changes:
|
||||
* - Title
|
||||
* - Collapsed state
|
||||
* - Panel IDs (adding/removing/replacing, but not reordering)
|
||||
*/
|
||||
const rowStateSubscription = combineLatest([
|
||||
gridLayoutStateManager.proposedGridLayout$,
|
||||
gridLayoutStateManager.gridLayout$,
|
||||
])
|
||||
.pipe(
|
||||
map(([proposedGridLayout, gridLayout]) => {
|
||||
const displayedGridLayout = proposedGridLayout ?? gridLayout;
|
||||
return {
|
||||
title: displayedGridLayout[rowIndex].title,
|
||||
isCollapsed: displayedGridLayout[rowIndex].isCollapsed,
|
||||
panelIds: Object.keys(displayedGridLayout[rowIndex].panels),
|
||||
};
|
||||
}),
|
||||
pairwise()
|
||||
)
|
||||
.subscribe(([oldRowData, newRowData]) => {
|
||||
if (oldRowData.title !== newRowData.title) setRowTitle(newRowData.title);
|
||||
if (oldRowData.isCollapsed !== newRowData.isCollapsed)
|
||||
setIsCollapsed(newRowData.isCollapsed);
|
||||
if (
|
||||
oldRowData.panelIds.length !== newRowData.panelIds.length ||
|
||||
!(
|
||||
oldRowData.panelIds.every((p) => newRowData.panelIds.includes(p)) &&
|
||||
newRowData.panelIds.every((p) => oldRowData.panelIds.includes(p))
|
||||
)
|
||||
) {
|
||||
setPanelIds(newRowData.panelIds);
|
||||
setPanelIdsInOrder(
|
||||
getKeysInOrder(
|
||||
(gridLayoutStateManager.proposedGridLayout$.getValue() ??
|
||||
gridLayoutStateManager.gridLayout$.getValue())[rowIndex].panels
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure the row re-renders to reflect the new panel order after a drag-and-drop interaction, since
|
||||
* 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 newPanelIdsInOrder = getKeysInOrder(gridLayout[rowIndex].panels);
|
||||
if (panelIdsInOrder.join() !== newPanelIdsInOrder.join()) {
|
||||
setPanelIdsInOrder(newPanelIdsInOrder);
|
||||
}
|
||||
const targetRow = interactionEvent?.targetRowIndex;
|
||||
if (rowIndex === targetRow && interactionEvent) {
|
||||
rowRef.classList.add('kbnGridRow--targeted');
|
||||
} else {
|
||||
rowRef.classList.remove('kbnGridRow--targeted');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
interactionStyleSubscription.unsubscribe();
|
||||
gridLayoutSubscription.unsubscribe();
|
||||
rowStateSubscription.unsubscribe();
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[rowIndex]
|
||||
);
|
||||
/**
|
||||
* This subscription ensures that the row will re-render when one of the following changes:
|
||||
* - Title
|
||||
* - Collapsed state
|
||||
* - Panel IDs (adding/removing/replacing, but not reordering)
|
||||
*/
|
||||
const rowStateSubscription = combineLatest([
|
||||
gridLayoutStateManager.proposedGridLayout$,
|
||||
gridLayoutStateManager.gridLayout$,
|
||||
])
|
||||
.pipe(
|
||||
map(([proposedGridLayout, gridLayout]) => {
|
||||
const displayedGridLayout = proposedGridLayout ?? gridLayout;
|
||||
return {
|
||||
title: displayedGridLayout[rowIndex].title,
|
||||
isCollapsed: displayedGridLayout[rowIndex].isCollapsed,
|
||||
panelIds: Object.keys(displayedGridLayout[rowIndex].panels),
|
||||
};
|
||||
}),
|
||||
pairwise()
|
||||
)
|
||||
.subscribe(([oldRowData, newRowData]) => {
|
||||
if (oldRowData.title !== newRowData.title) setRowTitle(newRowData.title);
|
||||
if (oldRowData.isCollapsed !== newRowData.isCollapsed)
|
||||
setIsCollapsed(newRowData.isCollapsed);
|
||||
if (
|
||||
oldRowData.panelIds.length !== newRowData.panelIds.length ||
|
||||
!(
|
||||
oldRowData.panelIds.every((p) => newRowData.panelIds.includes(p)) &&
|
||||
newRowData.panelIds.every((p) => oldRowData.panelIds.includes(p))
|
||||
)
|
||||
) {
|
||||
setPanelIds(newRowData.panelIds);
|
||||
setPanelIdsInOrder(
|
||||
getKeysInOrder(
|
||||
(gridLayoutStateManager.proposedGridLayout$.getValue() ??
|
||||
gridLayoutStateManager.gridLayout$.getValue())[rowIndex].panels
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Memoize panel children components (independent of their order) to prevent unnecessary re-renders
|
||||
*/
|
||||
const children: { [panelId: string]: React.ReactNode } = useMemo(() => {
|
||||
return panelIds.reduce(
|
||||
(prev, panelId) => ({
|
||||
...prev,
|
||||
[panelId]: (
|
||||
<GridPanel
|
||||
key={panelId}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
renderPanelContents={renderPanelContents}
|
||||
ref={(element) => {
|
||||
if (!gridLayoutStateManager.panelRefs.current[rowIndex]) {
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex] = {};
|
||||
}
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element;
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}, [panelIds, gridLayoutStateManager, renderPanelContents, rowIndex]);
|
||||
/**
|
||||
* Ensure the row re-renders to reflect the new panel order after a drag-and-drop interaction, since
|
||||
* 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 newPanelIdsInOrder = getKeysInOrder(gridLayout[rowIndex].panels);
|
||||
if (panelIdsInOrder.join() !== newPanelIdsInOrder.join()) {
|
||||
setPanelIdsInOrder(newPanelIdsInOrder);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rowContainer}
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
className="kbnGridRowContainer"
|
||||
>
|
||||
{rowIndex !== 0 && (
|
||||
<GridRowHeader
|
||||
isCollapsed={isCollapsed}
|
||||
toggleIsCollapsed={() => {
|
||||
const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value);
|
||||
newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}}
|
||||
rowTitle={rowTitle}
|
||||
return () => {
|
||||
interactionStyleSubscription.unsubscribe();
|
||||
gridLayoutSubscription.unsubscribe();
|
||||
rowStateSubscription.unsubscribe();
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[rowIndex]
|
||||
);
|
||||
|
||||
/**
|
||||
* Memoize panel children components (independent of their order) to prevent unnecessary re-renders
|
||||
*/
|
||||
const children: { [panelId: string]: React.ReactNode } = useMemo(() => {
|
||||
return panelIds.reduce(
|
||||
(prev, panelId) => ({
|
||||
...prev,
|
||||
[panelId]: (
|
||||
<GridPanel
|
||||
key={panelId}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
renderPanelContents={renderPanelContents}
|
||||
/>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
className={'kbnGridRow'}
|
||||
ref={gridRef}
|
||||
css={css`
|
||||
height: 100%;
|
||||
display: grid;
|
||||
position: relative;
|
||||
justify-items: stretch;
|
||||
transition: background-color 300ms linear;
|
||||
${initialStyles};
|
||||
`}
|
||||
>
|
||||
{/* render the panels **in order** for accessibility, using the memoized panel components */}
|
||||
{panelIdsInOrder.map((panelId) => children[panelId])}
|
||||
<DragPreview rowIndex={rowIndex} gridLayoutStateManager={gridLayoutStateManager} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
);
|
||||
}, [panelIds, gridLayoutStateManager, renderPanelContents, rowIndex]);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
className="kbnGridRowContainer"
|
||||
>
|
||||
{rowIndex !== 0 && (
|
||||
<GridRowHeader
|
||||
isCollapsed={isCollapsed}
|
||||
toggleIsCollapsed={() => {
|
||||
const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value);
|
||||
newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}}
|
||||
rowTitle={rowTitle}
|
||||
/>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
className={'kbnGridRow'}
|
||||
ref={(element: HTMLDivElement | null) =>
|
||||
(gridLayoutStateManager.rowRefs.current[rowIndex] = element)
|
||||
}
|
||||
css={css`
|
||||
height: 100%;
|
||||
display: grid;
|
||||
position: relative;
|
||||
justify-items: stretch;
|
||||
transition: background-color 300ms linear;
|
||||
${initialStyles};
|
||||
`}
|
||||
>
|
||||
{/* render the panels **in order** for accessibility, using the memoized panel components */}
|
||||
{panelIdsInOrder.map((panelId) => children[panelId])}
|
||||
<DragPreview rowIndex={rowIndex} gridLayoutStateManager={gridLayoutStateManager} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue