[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:
Marta Bondyra 2025-01-28 00:28:30 +01:00 committed by GitHub
parent 3e0fd1f82e
commit 00d822d88a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 309 additions and 306 deletions

View file

@ -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;
}

View file

@ -145,9 +145,6 @@ export const GridLayout = ({
rowIndex={rowIndex}
renderPanelContents={renderPanelContents}
gridLayoutStateManager={gridLayoutStateManager}
ref={(element: HTMLDivElement | null) =>
(gridLayoutStateManager.rowRefs.current[rowIndex] = element)
}
/>
);
});

View file

@ -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 = () => {

View file

@ -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>
);
};

View file

@ -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>
);
};