From 219f31ab2da2fd31486109920694a56d60df8212 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Mon, 10 Feb 2025 13:09:50 -0700 Subject: [PATCH] [kbn-grid-layout] Cleanup memoization and styling (#210285) ## Summary This PR cleans up the `kbn-grid-layout` code in two ways: 1. Rather than memoizing components in their parents, I swapped to using `React.memo` for all components, which accomplishes the same behaviour in a slightly cleaner way. 2. I moved all Emotion style definitions **outside** of the React components so that we no longer have to re-parse the CSS string on every render (see [this comment](https://github.com/elastic/eui/discussions/6828#discussioncomment-11247425)). ### Checklist - [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) --- .../kbn-grid-layout/grid/drag_preview.tsx | 91 +++-- .../grid/grid_height_smoother.tsx | 107 +++--- .../kbn-grid-layout/grid/grid_layout.tsx | 155 +++++---- .../drag_handle/default_drag_handle.tsx | 102 +++--- .../grid/grid_panel/drag_handle/index.tsx | 88 ++--- .../grid/grid_panel/grid_panel.tsx | 313 +++++++++--------- .../grid/grid_panel/resize_handle.tsx | 98 +++--- .../grid/grid_row/grid_row.tsx | 313 +++++++++--------- .../grid/grid_row/grid_row_header.tsx | 62 ++-- .../private/kbn-grid-layout/tsconfig.json | 15 +- 10 files changed, 671 insertions(+), 673 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx b/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx index 4594df315d265..f2d0b300024d2 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx @@ -14,54 +14,51 @@ import { css } from '@emotion/react'; import { GridLayoutStateManager } from './types'; -export const DragPreview = ({ - rowIndex, - gridLayoutStateManager, -}: { - rowIndex: number; - gridLayoutStateManager: GridLayoutStateManager; -}) => { - const dragPreviewRef = useRef(null); +export const DragPreview = React.memo( + ({ + rowIndex, + gridLayoutStateManager, + }: { + rowIndex: number; + gridLayoutStateManager: GridLayoutStateManager; + }) => { + const dragPreviewRef = useRef(null); - useEffect( - () => { - /** Update the styles of the drag preview via a subscription to prevent re-renders */ - const styleSubscription = combineLatest([ - gridLayoutStateManager.activePanel$, - gridLayoutStateManager.proposedGridLayout$, - ]) - .pipe(skip(1)) // skip the first emit because the drag preview is only rendered after a user action - .subscribe(([activePanel, proposedGridLayout]) => { - if (!dragPreviewRef.current) return; + useEffect( + () => { + /** Update the styles of the drag preview via a subscription to prevent re-renders */ + const styleSubscription = combineLatest([ + gridLayoutStateManager.activePanel$, + gridLayoutStateManager.proposedGridLayout$, + ]) + .pipe(skip(1)) // skip the first emit because the drag preview is only rendered after a user action + .subscribe(([activePanel, proposedGridLayout]) => { + if (!dragPreviewRef.current) return; - if (!activePanel || !proposedGridLayout?.[rowIndex].panels[activePanel.id]) { - dragPreviewRef.current.style.display = 'none'; - } else { - const panel = proposedGridLayout[rowIndex].panels[activePanel.id]; - dragPreviewRef.current.style.display = 'block'; - dragPreviewRef.current.style.gridColumnStart = `${panel.column + 1}`; - dragPreviewRef.current.style.gridColumnEnd = `${panel.column + 1 + panel.width}`; - dragPreviewRef.current.style.gridRowStart = `${panel.row + 1}`; - dragPreviewRef.current.style.gridRowEnd = `${panel.row + 1 + panel.height}`; - } - }); + if (!activePanel || !proposedGridLayout?.[rowIndex].panels[activePanel.id]) { + dragPreviewRef.current.style.display = 'none'; + } else { + const panel = proposedGridLayout[rowIndex].panels[activePanel.id]; + dragPreviewRef.current.style.display = 'block'; + dragPreviewRef.current.style.gridColumnStart = `${panel.column + 1}`; + dragPreviewRef.current.style.gridColumnEnd = `${panel.column + 1 + panel.width}`; + dragPreviewRef.current.style.gridRowStart = `${panel.row + 1}`; + dragPreviewRef.current.style.gridRowEnd = `${panel.row + 1 + panel.height}`; + } + }); - return () => { - styleSubscription.unsubscribe(); - }; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); + return () => { + styleSubscription.unsubscribe(); + }; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); - return ( -
- ); -}; + return
; + } +); + +const styles = css({ display: 'none', pointerEvents: 'none' }); + +DragPreview.displayName = 'KbnGridLayoutDragPreview'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_height_smoother.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_height_smoother.tsx index 792d8bb1aabe7..1cb45cb898d85 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_height_smoother.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_height_smoother.tsx @@ -12,58 +12,67 @@ import React, { PropsWithChildren, useEffect, useRef } from 'react'; import { combineLatest } from 'rxjs'; import { GridLayoutStateManager } from './types'; -export const GridHeightSmoother = ({ - children, - gridLayoutStateManager, -}: PropsWithChildren<{ gridLayoutStateManager: GridLayoutStateManager }>) => { - // set the parent div size directly to smooth out height changes. - const smoothHeightRef = useRef(null); +export const GridHeightSmoother = React.memo( + ({ + children, + gridLayoutStateManager, + }: PropsWithChildren<{ gridLayoutStateManager: GridLayoutStateManager }>) => { + // set the parent div size directly to smooth out height changes. + const smoothHeightRef = useRef(null); - useEffect(() => { - /** - * When the user is interacting with an element, the page can grow, but it cannot - * shrink. This is to stop a behaviour where the page would scroll up automatically - * making the panel shrink or grow unpredictably. - */ - const interactionStyleSubscription = combineLatest([ - gridLayoutStateManager.gridDimensions$, - gridLayoutStateManager.interactionEvent$, - ]).subscribe(([dimensions, interactionEvent]) => { - if (!smoothHeightRef.current || gridLayoutStateManager.expandedPanelId$.getValue()) return; + useEffect(() => { + /** + * When the user is interacting with an element, the page can grow, but it cannot + * shrink. This is to stop a behaviour where the page would scroll up automatically + * making the panel shrink or grow unpredictably. + */ + const interactionStyleSubscription = combineLatest([ + gridLayoutStateManager.gridDimensions$, + gridLayoutStateManager.interactionEvent$, + ]).subscribe(([dimensions, interactionEvent]) => { + if (!smoothHeightRef.current || gridLayoutStateManager.expandedPanelId$.getValue()) return; - if (!interactionEvent) { - smoothHeightRef.current.style.minHeight = `${dimensions.height}px`; - return; - } - smoothHeightRef.current.style.minHeight = `${ - smoothHeightRef.current.getBoundingClientRect().height - }px`; - }); + if (!interactionEvent) { + smoothHeightRef.current.style.minHeight = `${dimensions.height}px`; + return; + } + smoothHeightRef.current.style.minHeight = `${ + smoothHeightRef.current.getBoundingClientRect().height + }px`; + }); - return () => { - interactionStyleSubscription.unsubscribe(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + return () => { + interactionStyleSubscription.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - return ( -
+ {children} +
+ ); + } +); - &: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; - } - `} - > - {children} -
- ); +const styles = { + heightSmoothing: css({ + height: '100%', + overflowAnchor: 'none', + transition: 'min-height 500ms linear', + }), + hasActivePanel: css({ + '&:has(.kbnGridPanel--expanded)': { + minHeight: '100% !important', + maxHeight: '100vh', // fallback in case if the parent doesn't set the height correctly + position: 'relative', + transition: 'none', + }, + }), }; + +GridHeightSmoother.displayName = 'KbnGridLayoutHeightSmoother'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.tsx index f42d61321ad59..0d5b95e587adf 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.tsx @@ -9,7 +9,7 @@ import classNames from 'classnames'; import { cloneDeep } from 'lodash'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { combineLatest, distinctUntilChanged, map, pairwise, skip } from 'rxjs'; import { css } from '@emotion/react'; @@ -134,22 +134,6 @@ export const GridLayout = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - /** - * Memoize row children components to prevent unnecessary re-renders - */ - const children = useMemo(() => { - return Array.from({ length: rowCount }, (_, rowIndex) => { - return ( - - ); - }); - }, [rowCount, gridLayoutStateManager, renderPanelContents]); - return (
- {children} + {Array.from({ length: rowCount }, (_, rowIndex) => { + return ( + + ); + })}
); }; -const singleColumnStyles = css` - .kbnGridRow { - grid-template-columns: 100%; - grid-template-rows: auto; - grid-auto-flow: row; - grid-auto-rows: auto; - } - - .kbnGridPanel { - grid-area: unset !important; - } -`; - -const expandedPanelStyles = css` - height: 100%; - - & .kbnGridRowContainer:has(.kbnGridPanel--expanded) { - // targets the grid row container that contains the expanded panel - .kbnGridRowHeader { - height: 0px; // used instead of 'display: none' due to a11y concerns - } - .kbnGridRow { - display: block !important; // overwrite grid display - height: 100%; - .kbnGridPanel { - &.kbnGridPanel--expanded { - height: 100% !important; - } - &:not(.kbnGridPanel--expanded) { - // hide the non-expanded panels - position: absolute; - top: -9999px; - left: -9999px; - visibility: hidden; // remove hidden panels and their contents from tab order for a11y - } - } - } - } - - & .kbnGridRowContainer:not(:has(.kbnGridPanel--expanded)) { - // targets the grid row containers that **do not** contain the expanded panel - position: absolute; - top: -9999px; - left: -9999px; - } -`; +const styles = { + layoutPadding: css({ + padding: 'calc(var(--kbnGridGutterSize) * 1px)', + }), + hasActivePanel: css({ + '&:has(.kbnGridPanel--active)': { + // disable pointer events and user select on drag + resize + userSelect: 'none', + pointerEvents: 'none', + }, + }), + singleColumn: css({ + '&.kbnGrid--mobileView': { + '.kbnGridRow': { + gridTemplateAreas: '100%', + gridTemplateRows: 'auto', + gridAutoFlow: 'row', + gridAutoRows: 'auto', + }, + '.kbnGridPanel': { + gridArea: 'unset !important', + }, + }, + }), + hasExpandedPanel: css({ + '&:has(.kbnGridPanel--expanded)': { + height: '100%', + // targets the grid row container that contains the expanded panel + '& .kbnGridRowContainer:has(.kbnGridPanel--expanded)': { + '.kbnGridRowHeader': { + height: '0px', // used instead of 'display: none' due to a11y concerns + }, + '.kbnGridRow': { + display: 'block !important', // overwrite grid display + height: '100%', + '.kbnGridPanel': { + '&.kbnGridPanel--expanded': { + height: '100% !important', + }, + // hide the non-expanded panels + '&:not(.kbnGridPanel--expanded)': { + position: 'absolute', + top: '-9999px', + left: '-9999px', + visibility: 'hidden', // remove hidden panels and their contents from tab order for a11y + }, + }, + }, + }, + // targets the grid row containers that **do not** contain the expanded panel + '& .kbnGridRowContainer:not(:has(.kbnGridPanel--expanded))': { + position: 'absolute', + top: '-9999px', + left: '-9999px', + }, + }, + }), +}; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/default_drag_handle.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/default_drag_handle.tsx index 547ebd757411b..89df4e52615c8 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/default_drag_handle.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/default_drag_handle.tsx @@ -7,61 +7,57 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ import React from 'react'; -import { EuiIcon, useEuiTheme } from '@elastic/eui'; +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'; -export const DefaultDragHandle = ({ - onDragStart, -}: { - onDragStart: (e: UserInteractionEvent) => void; -}) => { - const { euiTheme } = useEuiTheme(); +export const DefaultDragHandle = React.memo( + ({ onDragStart }: { onDragStart: (e: UserInteractionEvent) => void }) => { + return ( + + ); + } +); - return ( - - ); -}; +const styles = ({ euiTheme }: UseEuiTheme) => + css({ + opacity: 0, + display: 'flex', + cursor: 'grab', + position: 'absolute', + alignItems: 'center', + justifyContent: 'center', + top: `-${euiTheme.size.l}`, + width: euiTheme.size.l, + height: euiTheme.size.l, + zIndex: euiTheme.levels.modal, + marginLeft: euiTheme.size.s, + border: `1px solid ${euiTheme.border.color}`, + borderBottom: 'none', + backgroundColor: euiTheme.colors.backgroundBasePlain, + borderRadius: `${euiTheme.border.radius} ${euiTheme.border.radius} 0 0`, + transition: `${euiTheme.animation.slow} opacity`, + touchAction: 'none', + '.kbnGridPanel:hover &, .kbnGridPanel:focus-within &, &:active, &:focus': { + opacity: '1 !important', + }, + '&:active': { + cursor: 'grabbing', + }, + '.kbnGrid--static &, .kbnGridPanel--expanded &': { + display: 'none', + }, + }); + +DefaultDragHandle.displayName = 'KbnGridLayoutDefaultDragHandle'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/index.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/index.tsx index 027175bd1763d..7b715019ba3fd 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/index.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/index.tsx @@ -17,53 +17,57 @@ export interface DragHandleApi { setDragHandles: (refs: Array) => void; } -export const DragHandle = React.forwardRef< - DragHandleApi, - { - gridLayoutStateManager: GridLayoutStateManager; - panelId: string; - rowIndex: number; - } ->(({ gridLayoutStateManager, panelId, rowIndex }, ref) => { - const startInteraction = useGridLayoutEvents({ - interactionType: 'drag', - gridLayoutStateManager, - panelId, - rowIndex, - }); +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(0); - const removeEventListenersRef = useRef<(() => void) | null>(null); + const [dragHandleCount, setDragHandleCount] = useState(0); + const removeEventListenersRef = useRef<(() => void) | null>(null); - const setDragHandles = useCallback( - (dragHandles: Array) => { - 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 = () => { + const setDragHandles = useCallback( + (dragHandles: Array) => { + setDragHandleCount(dragHandles.length); for (const handle of dragHandles) { if (handle === null) return; - handle.removeEventListener('mousedown', startInteraction); - handle.removeEventListener('touchstart', startInteraction); + handle.addEventListener('mousedown', startInteraction, { passive: true }); + handle.addEventListener('touchstart', startInteraction, { passive: true }); + handle.style.touchAction = 'none'; } - }; - }, - [startInteraction] - ); + 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?.(); - }, - [] - ); + useEffect( + () => () => { + // on unmount, remove all drag handle event listeners + removeEventListenersRef.current?.(); + }, + [] + ); - useImperativeHandle(ref, () => ({ setDragHandles }), [setDragHandles]); + useImperativeHandle(ref, () => ({ setDragHandles }), [setDragHandles]); - return Boolean(dragHandleCount) ? null : ; -}); + return Boolean(dragHandleCount) ? null : ; + }) +); + +DragHandle.displayName = 'KbnGridLayoutDragHandle'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.tsx index 686751356451f..8f480f5f5fc60 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.tsx @@ -27,166 +27,165 @@ export interface GridPanelProps { gridLayoutStateManager: GridLayoutStateManager; } -export const GridPanel = ({ - panelId, - rowIndex, - renderPanelContents, - gridLayoutStateManager, -}: GridPanelProps) => { - const [dragHandleApi, setDragHandleApi] = useState(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]); - - 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(); - - 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(); - - 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`; +export const GridPanel = React.memo( + ({ panelId, rowIndex, renderPanelContents, gridLayoutStateManager }: GridPanelProps) => { + const [dragHandleApi, setDragHandleApi] = useState(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]); + + 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(); + + 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(); + + 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 { - // if the current panel is being dragged, render it with a fixed position + size - ref.style.position = 'fixed'; + ref.classList.remove('kbnGridPanel--active'); - 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`; + ref.style.zIndex = `auto`; - // undo any "lock to grid" styles - ref.style.gridArea = `auto`; // shortcut to set all grid styles to `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}`; + } + }); + + /** + * 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'); + } else { + ref.classList.remove('kbnGridPanel--expanded'); } - } 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}`; } - }); - - /** - * 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'); - } else { - ref.classList.remove('kbnGridPanel--expanded'); + ); + + 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 ( +
{ + if (!gridLayoutStateManager.panelRefs.current[rowIndex]) { + gridLayoutStateManager.panelRefs.current[rowIndex] = {}; } - } - ); - - 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 ( -
{ - if (!gridLayoutStateManager.panelRefs.current[rowIndex]) { - gridLayoutStateManager.panelRefs.current[rowIndex] = {}; - } - gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element; - }} - css={initialStyles} - className="kbnGridPanel" - > - - {panelContents} - -
- ); -}; + gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element; + }} + css={initialStyles} + className="kbnGridPanel" + > + + {panelContents} + +
+ ); + } +); + +GridPanel.displayName = 'KbnGridLayoutPanel'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/resize_handle.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/resize_handle.tsx index 17ec144d70727..e65221d1a7805 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/resize_handle.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/resize_handle.tsx @@ -7,58 +7,64 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import React from 'react'; + +import type { UseEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import { useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; + import { GridLayoutStateManager } from '../types'; import { useGridLayoutEvents } from '../use_grid_layout_events'; -export const ResizeHandle = ({ - gridLayoutStateManager, - rowIndex, - panelId, -}: { - gridLayoutStateManager: GridLayoutStateManager; - rowIndex: number; - panelId: string; -}) => { - const { euiTheme } = useEuiTheme(); - const startInteraction = useGridLayoutEvents({ - interactionType: 'resize', +export const ResizeHandle = React.memo( + ({ gridLayoutStateManager, - panelId, rowIndex, + panelId, + }: { + gridLayoutStateManager: GridLayoutStateManager; + rowIndex: number; + panelId: string; + }) => { + const startInteraction = useGridLayoutEvents({ + interactionType: 'resize', + gridLayoutStateManager, + panelId, + rowIndex, + }); + + return ( +
); - }, [panelIds, gridLayoutStateManager, renderPanelContents, rowIndex]); + } +); - return ( -
- {rowIndex !== 0 && ( - { - const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value); - newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed; - gridLayoutStateManager.gridLayout$.next(newLayout); - }} - rowTitle={rowTitle} - /> - )} - {!isCollapsed && ( -
- (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])} - -
- )} -
- ); +const styles = { + fullHeight: css({ + height: '100%', + }), + grid: css({ + position: 'relative', + justifyItems: 'stretch', + display: 'grid', + gap: 'calc(var(--kbnGridGutterSize) * 1px)', + gridAutoRows: 'calc(var(--kbnGridRowHeight) * 1px)', + gridTemplateColumns: `repeat( + var(--kbnGridRowColumnCount), + calc( + (100% - (var(--kbnGridGutterSize) * (var(--kbnGridRowColumnCount) - 1) * 1px)) / + var(--kbnGridRowColumnCount) + ) + )`, + }), }; + +GridRow.displayName = 'KbnGridLayoutRow'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.tsx index e728009a2bf18..bcdca2b91ea87 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.tsx @@ -10,32 +10,36 @@ import React from 'react'; import { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -export const GridRowHeader = ({ - isCollapsed, - toggleIsCollapsed, - rowTitle, -}: { - isCollapsed: boolean; - toggleIsCollapsed: () => void; - rowTitle?: string; -}) => { - return ( -
- - - - -

{rowTitle}

-
-
- -
- ); -}; +export const GridRowHeader = React.memo( + ({ + isCollapsed, + toggleIsCollapsed, + rowTitle, + }: { + isCollapsed: boolean; + toggleIsCollapsed: () => void; + rowTitle?: string; + }) => { + return ( +
+ + + + +

{rowTitle}

+
+
+ +
+ ); + } +); + +GridRowHeader.displayName = 'KbnGridLayoutRowHeader'; diff --git a/src/platform/packages/private/kbn-grid-layout/tsconfig.json b/src/platform/packages/private/kbn-grid-layout/tsconfig.json index 89796203132c0..508e9ab9463a1 100644 --- a/src/platform/packages/private/kbn-grid-layout/tsconfig.json +++ b/src/platform/packages/private/kbn-grid-layout/tsconfig.json @@ -1,16 +1,9 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "target/types", + "outDir": "target/types" }, - "include": [ - "**/*.ts", - "**/*.tsx", - ], - "exclude": [ - "target/**/*" - ], - "kbn_references": [ - "@kbn/i18n", - ] + "include": ["**/*.ts", "**/*.tsx", "../../../../../typings/emotion.d.ts"], + "exclude": ["target/**/*"], + "kbn_references": ["@kbn/i18n"] }