From 4a740cb0ac621225e8b653af06eeb3482ad2855b Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 30 Jan 2025 13:22:33 -0700 Subject: [PATCH 01/42] First pass at UI --- .../grid/grid_row/grid_row.tsx | 6 +- .../grid/grid_row/grid_row_header.tsx | 168 ++++++++++++++++-- 2 files changed, 157 insertions(+), 17 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 6e0d0ff87c9a5..7a478f6681636 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -38,7 +38,6 @@ export const GridRow = ({ const [panelIdsInOrder, setPanelIdsInOrder] = useState(() => getKeysInOrder(currentRow.panels) ); - const [rowTitle, setRowTitle] = useState(currentRow.title); const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed); /** Set initial styles based on state at mount to prevent styles from "blipping" */ @@ -73,7 +72,6 @@ export const GridRow = ({ /** * 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) */ @@ -93,7 +91,6 @@ export const GridRow = ({ pairwise() ) .subscribe(([oldRowData, newRowData]) => { - if (oldRowData.title !== newRowData.title) setRowTitle(newRowData.title); if (oldRowData.isCollapsed !== newRowData.isCollapsed) setIsCollapsed(newRowData.isCollapsed); if ( @@ -165,13 +162,14 @@ export const GridRow = ({ > {rowIndex !== 0 && ( { const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value); newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed; gridLayoutStateManager.gridLayout$.next(newLayout); }} - rowTitle={rowTitle} /> )} {!isCollapsed && ( 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..d476692dff7cd 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 @@ -6,23 +6,107 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { cloneDeep } from 'lodash'; +import React, { useCallback, useEffect, useState } from 'react'; +import { distinctUntilChanged, map } from 'rxjs'; + +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiInlineEditTitle, + EuiText, + euiCanAnimate, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; +import { GridLayoutStateManager } from '../types'; + export const GridRowHeader = ({ + rowIndex, + gridLayoutStateManager, isCollapsed, toggleIsCollapsed, - rowTitle, }: { + rowIndex: number; + gridLayoutStateManager: GridLayoutStateManager; isCollapsed: boolean; toggleIsCollapsed: () => void; - rowTitle?: string; }) => { + const { euiTheme } = useEuiTheme(); + + const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; + + const [editMode, setEditMode] = useState(false); + const [readOnly, setReadOnly] = useState( + gridLayoutStateManager.accessMode$.getValue() === 'VIEW' + ); + const [rowTitle, setRowTitle] = useState(currentRow.title); + + useEffect(() => { + const titleSubscription = gridLayoutStateManager.gridLayout$ + .pipe( + map((gridLayout) => gridLayout[rowIndex].title), + distinctUntilChanged() + ) + .subscribe((title) => { + setRowTitle(title); + }); + + const accessModeSubscription = gridLayoutStateManager.accessMode$ + .pipe(distinctUntilChanged()) + .subscribe((accessMode) => { + setReadOnly(accessMode === 'VIEW'); + }); + + return () => { + titleSubscription.unsubscribe(); + accessModeSubscription.unsubscribe(); + }; + }, [rowIndex, gridLayoutStateManager]); + + const updateTitle = useCallback( + (title: string) => { + const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.getValue()); + newLayout[rowIndex].title = title; + gridLayoutStateManager.gridLayout$.next(newLayout); + }, + [rowIndex, gridLayoutStateManager.gridLayout$] + ); + return ( -
- - + + - -

{rowTitle}

-
-
- -
+ + + { + if (readOnly) { + toggleIsCollapsed(); + } else { + setEditMode(!editMode); + } + }} + readModeProps={{ + onClick: readOnly ? toggleIsCollapsed : undefined, + css: css` + &:hover, + &:focus { + text-decoration: none !important; + } + & svg { + inline-size: 16px; + block-size: 16px; + } + .euiButtonEmpty__content { + gap: ${euiTheme.size.xs}; // decrease gap between title and pencil icon + // &:after { + // flex: 0; + // display: block; + // content: '(10 panels)'; + // color: ${euiTheme.colors.textSubdued}; + // } + } + `, + }} + /> + + {!readOnly && !editMode && ( + <> + {isCollapsed && ( + + {`(${ + Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length + } panels)`} + + )} + + + + + )} + {isCollapsed && ( + + + + )} + ); }; From fe4e4ea7af493835cccb1ea77d368938d3de3e4b Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 30 Jan 2025 14:13:27 -0700 Subject: [PATCH 02/42] Slightly cleaner UI --- .../grid/grid_row/grid_row_header.tsx | 63 +++++++++++-------- 1 file changed, 36 insertions(+), 27 deletions(-) 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 d476692dff7cd..97a5e74463edf 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 @@ -144,40 +144,49 @@ export const GridRowHeader = ({ } .euiButtonEmpty__content { gap: ${euiTheme.size.xs}; // decrease gap between title and pencil icon - // &:after { - // flex: 0; - // display: block; - // content: '(10 panels)'; - // color: ${euiTheme.colors.textSubdued}; - // } } `, }} /> - {!readOnly && !editMode && ( - <> - {isCollapsed && ( + { + /** + * Add actions at the end of the header section when the layout is editable + the section title + * is not in edit mode + */ + !readOnly && !editMode && ( + <> + {isCollapsed && ( + + {`(${ + /** + * we can get away with grabbing the panel count without React state because this count + * is only rendered when the section is collapsed, and the count can only be updated when + * the section isn't collapsed + */ + Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length + } panels)`} + + )} - {`(${ - Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length - } panels)`} + - )} - - - - - )} - {isCollapsed && ( - - - - )} + {isCollapsed && ( + + + + )} + + ) + } ); }; From 0a992360f4a5f7c9f0a4f8fbd736ff23a6e1e120 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 30 Jan 2025 16:38:01 -0700 Subject: [PATCH 03/42] First attempt at delete --- .../grid/grid_row/grid_row_header.tsx | 322 ++++++++++++------ 1 file changed, 212 insertions(+), 110 deletions(-) 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 97a5e74463edf..f95f13d85540c 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 @@ -12,17 +12,25 @@ import { distinctUntilChanged, map } from 'rxjs'; import { EuiButtonIcon, + EuiModal, EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle, EuiText, euiCanAnimate, useEuiTheme, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { GridLayoutStateManager } from '../types'; +import { GridLayoutData, GridLayoutStateManager } from '../types'; +import { resolveGridRow } from '../utils/resolve_grid_row'; export const GridRowHeader = ({ rowIndex, @@ -40,6 +48,8 @@ export const GridRowHeader = ({ const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; const [editMode, setEditMode] = useState(false); + const [deleteModalVisible, setDeleteModalVisible] = useState(false); + const [readOnly, setReadOnly] = useState( gridLayoutStateManager.accessMode$.getValue() === 'VIEW' ); @@ -67,126 +77,218 @@ export const GridRowHeader = ({ }; }, [rowIndex, gridLayoutStateManager]); - const updateTitle = useCallback( - (title: string) => { - const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.getValue()); - newLayout[rowIndex].title = title; - gridLayoutStateManager.gridLayout$.next(newLayout); + const updateTitle = useCallback((layout: GridLayoutData, index: number, title: string) => { + const newLayout = cloneDeep(layout); + newLayout[index].title = title; + return newLayout; + }, []); + + const movePanelsToRow = useCallback( + (layout: GridLayoutData, startingRow: number, newRow: number) => { + const newLayout = cloneDeep(layout); + const panelsToMove = newLayout[startingRow].panels; + const maxRow = Math.max( + ...Object.values(newLayout[newRow].panels).map(({ row, height }) => row + height) + ); + Object.keys(panelsToMove).forEach((index) => (panelsToMove[index].row += maxRow)); + newLayout[newRow].panels = { ...newLayout[newRow].panels, ...panelsToMove }; + newLayout[newRow] = resolveGridRow(newLayout[newRow]); + return newLayout; }, - [rowIndex, gridLayoutStateManager.gridLayout$] + [] ); - return ( - { + const newLayout = cloneDeep(layout); + newLayout.splice(index, 1); + return newLayout; + }, []); - .kbnGridLayout--deleteRowIcon { - margin-left: ${euiTheme.size.xs}; - } - .kbnGridLayout--moveRowIcon { - margin-left: auto; - } + return ( + <> + - - - - - { - if (readOnly) { - toggleIsCollapsed(); - } else { - setEditMode(!editMode); + .kbnGridLayout--moveRowIcon { + margin-left: auto; + } + + // these styles hide the delete + move actions by default and only show them on hover + .kbnGridLayout--deleteRowIcon, + .kbnGridLayout--moveRowIcon { + opacity: 0; + ${euiCanAnimate} { + transition: opacity ${euiTheme.animation.extraFast} ease-in; } - }} - readModeProps={{ - onClick: readOnly ? toggleIsCollapsed : undefined, - css: css` - &:hover, - &:focus { - text-decoration: none !important; - } - & svg { - inline-size: 16px; - block-size: 16px; - } - .euiButtonEmpty__content { - gap: ${euiTheme.size.xs}; // decrease gap between title and pencil icon + } + &:hover .kbnGridLayout--deleteRowIcon, + &:hover .kbnGridLayout--moveRowIcon { + opacity: 1; + } + `} + className="kbnGridRowHeader" + > + + + + + { + const newLayout = updateTitle( + gridLayoutStateManager.gridLayout$.getValue(), + rowIndex, + title + ); + gridLayoutStateManager.gridLayout$.next(newLayout); + }} + onClick={() => { + if (readOnly) { + toggleIsCollapsed(); + } else { + setEditMode(!editMode); } - `, - }} - /> - - { - /** - * Add actions at the end of the header section when the layout is editable + the section title - * is not in edit mode - */ - !readOnly && !editMode && ( - <> - {isCollapsed && ( + }} + readModeProps={{ + onClick: readOnly ? toggleIsCollapsed : undefined, + css: css` + &:hover, + &:focus { + text-decoration: none !important; + } + & svg { + inline-size: 16px; + block-size: 16px; + } + .euiButtonEmpty__content { + gap: ${euiTheme.size.xs}; // decrease gap between title and pencil icon + } + `, + }} + /> + + { + /** + * Add actions at the end of the header section when the layout is editable + the section title + * is not in edit mode + */ + !readOnly && !editMode && ( + <> + {isCollapsed && ( + + {`(${ + /** + * we can get away with grabbing the panel count without React state because this count + * is only rendered when the section is collapsed, and the count can only be updated when + * the section isn't collapsed + */ + Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels) + .length + } panels)`} + + )} - {`(${ - /** - * we can get away with grabbing the panel count without React state because this count - * is only rendered when the section is collapsed, and the count can only be updated when - * the section isn't collapsed - */ - Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length - } panels)`} - - )} - - - - {isCollapsed && ( - { + const panelCount = Object.keys( + gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels + ).length; + if (!panelCount) { + deleteSection(gridLayoutStateManager.gridLayout$.getValue(), rowIndex); + } else { + setDeleteModalVisible(true); + } + }} /> - )} - - ) - } - + {isCollapsed && ( + + + + )} + + ) + } + + {deleteModalVisible && ( + { + setDeleteModalVisible(false); + }} + > + + Delete section + + + {`Are you sure you want to remove this section and its ${ + Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length + } panels?`} + + + { + setDeleteModalVisible(false); + }} + > + Cancel + + { + setDeleteModalVisible(false); + const newLayout = deleteSection( + gridLayoutStateManager.gridLayout$.getValue(), + rowIndex + ); + gridLayoutStateManager.gridLayout$.next(newLayout); + }} + fill + color="danger" + > + Yes + + { + setDeleteModalVisible(false); + let newLayout = movePanelsToRow( + gridLayoutStateManager.gridLayout$.getValue(), + rowIndex, + 0 + ); + newLayout = deleteSection(newLayout, rowIndex); + gridLayoutStateManager.gridLayout$.next(newLayout); + }} + fill + > + Delete section only + + + + )} + ); }; From b46e62886510031f6a7ae9782c61beb2b153a970 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 31 Jan 2025 13:58:22 -0700 Subject: [PATCH 04/42] Make only pencil icon clickable --- .../grid/grid_row/grid_row.tsx | 73 +++++---- .../grid/grid_row/grid_row_header.tsx | 148 +++++++----------- .../grid/grid_row/grid_row_title.tsx | 86 ++++++++++ 3 files changed, 188 insertions(+), 119 deletions(-) create mode 100644 src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 7a478f6681636..50ed379331851 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -8,8 +8,8 @@ */ import { cloneDeep } from 'lodash'; -import React, { useEffect, useMemo, useState } from 'react'; -import { map, pairwise, skip, combineLatest } from 'rxjs'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { map, pairwise, skip, combineLatest, distinctUntilChanged } from 'rxjs'; import { css } from '@emotion/react'; import { DragPreview } from '../drag_preview'; @@ -33,12 +33,12 @@ export const GridRow = ({ gridLayoutStateManager, }: GridRowProps) => { const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex]; + const containerRef = useRef(null); const [panelIds, setPanelIds] = useState(Object.keys(currentRow.panels)); const [panelIdsInOrder, setPanelIdsInOrder] = useState(() => getKeysInOrder(currentRow.panels) ); - const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed); /** Set initial styles based on state at mount to prevent styles from "blipping" */ const initialStyles = useMemo(() => { @@ -72,7 +72,6 @@ export const GridRow = ({ /** * This subscription ensures that the row will re-render when one of the following changes: - * - Collapsed state * - Panel IDs (adding/removing/replacing, but not reordering) */ const rowStateSubscription = combineLatest([ @@ -84,15 +83,12 @@ export const GridRow = ({ 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.isCollapsed !== newRowData.isCollapsed) - setIsCollapsed(newRowData.isCollapsed); if ( oldRowData.panelIds.length !== newRowData.panelIds.length || !( @@ -122,10 +118,28 @@ export const GridRow = ({ } }); + /** + * Handle collapsed state via class name + */ + const collapsedStateSubscription = gridLayoutStateManager.gridLayout$ + .pipe( + map((gridLayout) => gridLayout[rowIndex].isCollapsed), + distinctUntilChanged() + ) + .subscribe((isCollapsed) => { + if (!containerRef.current) return; + if (isCollapsed) { + containerRef.current.classList.add('kbnGridRowContainer--collapsed'); + } else { + containerRef.current.classList.remove('kbnGridRowContainer--collapsed'); + } + }); + return () => { interactionStyleSubscription.unsubscribe(); gridLayoutSubscription.unsubscribe(); rowStateSubscription.unsubscribe(); + collapsedStateSubscription.unsubscribe(); }; }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -155,8 +169,13 @@ export const GridRow = ({ return (
@@ -164,7 +183,6 @@ export const GridRow = ({ { const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value); newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed; @@ -172,26 +190,25 @@ export const GridRow = ({ }} /> )} - {!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])} - -
- )} + +
+ (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])} + +
); }; 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 f95f13d85540c..be11f2e44933b 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 @@ -31,40 +31,26 @@ import { i18n } from '@kbn/i18n'; import { GridLayoutData, GridLayoutStateManager } from '../types'; import { resolveGridRow } from '../utils/resolve_grid_row'; +import { GridRowTitle } from './grid_row_title'; export const GridRowHeader = ({ rowIndex, gridLayoutStateManager, - isCollapsed, toggleIsCollapsed, }: { rowIndex: number; gridLayoutStateManager: GridLayoutStateManager; - isCollapsed: boolean; toggleIsCollapsed: () => void; }) => { const { euiTheme } = useEuiTheme(); - const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; - - const [editMode, setEditMode] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); - const [readOnly, setReadOnly] = useState( gridLayoutStateManager.accessMode$.getValue() === 'VIEW' ); - const [rowTitle, setRowTitle] = useState(currentRow.title); + const [editTitleOpen, setEditTitleOpen] = useState(false); useEffect(() => { - const titleSubscription = gridLayoutStateManager.gridLayout$ - .pipe( - map((gridLayout) => gridLayout[rowIndex].title), - distinctUntilChanged() - ) - .subscribe((title) => { - setRowTitle(title); - }); - const accessModeSubscription = gridLayoutStateManager.accessMode$ .pipe(distinctUntilChanged()) .subscribe((accessMode) => { @@ -72,16 +58,9 @@ export const GridRowHeader = ({ }); return () => { - titleSubscription.unsubscribe(); accessModeSubscription.unsubscribe(); }; - }, [rowIndex, gridLayoutStateManager]); - - const updateTitle = useCallback((layout: GridLayoutData, index: number, title: string) => { - const newLayout = cloneDeep(layout); - newLayout[index].title = title; - return newLayout; - }, []); + }, [gridLayoutStateManager]); const movePanelsToRow = useCallback( (layout: GridLayoutData, startingRow: number, newRow: number) => { @@ -110,15 +89,16 @@ export const GridRowHeader = ({ gutterSize="xs" alignItems="center" css={css` - border-bottom: ${isCollapsed ? euiTheme.border.thin : 'none'}; padding: ${euiTheme.size.s} 0px; + border-bottom: none; + .kbnGridRowContainer--collapsed & { + border-bottom: ${euiTheme.border.thin}; + } + .kbnGridLayout--deleteRowIcon { margin-left: ${euiTheme.size.xs}; } - .kbnGridLayout--moveRowIcon { - margin-left: auto; - } // these styles hide the delete + move actions by default and only show them on hover .kbnGridLayout--deleteRowIcon, @@ -141,70 +121,49 @@ export const GridRowHeader = ({ aria-label={i18n.translate('kbnGridLayout.row.toggleCollapse', { defaultMessage: 'Toggle collapse', })} - iconType={isCollapsed ? 'arrowRight' : 'arrowDown'} + iconType={'arrowDown'} onClick={toggleIsCollapsed} - /> - - - { - const newLayout = updateTitle( - gridLayoutStateManager.gridLayout$.getValue(), - rowIndex, - title - ); - gridLayoutStateManager.gridLayout$.next(newLayout); - }} - onClick={() => { - if (readOnly) { - toggleIsCollapsed(); - } else { - setEditMode(!editMode); + css={css` + transition: ${euiTheme.animation.extraFast} + transform: rotate(0deg)t; + .kbnGridRowContainer--collapsed & { + transform: rotate(-90deg) !important; } - }} - readModeProps={{ - onClick: readOnly ? toggleIsCollapsed : undefined, - css: css` - &:hover, - &:focus { - text-decoration: none !important; - } - & svg { - inline-size: 16px; - block-size: 16px; - } - .euiButtonEmpty__content { - gap: ${euiTheme.size.xs}; // decrease gap between title and pencil icon - } - `, - }} + `} /> + { /** * Add actions at the end of the header section when the layout is editable + the section title * is not in edit mode */ - !readOnly && !editMode && ( + !readOnly && !editTitleOpen && ( <> - {isCollapsed && ( - - {`(${ - /** - * we can get away with grabbing the panel count without React state because this count - * is only rendered when the section is collapsed, and the count can only be updated when - * the section isn't collapsed - */ - Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels) - .length - } panels)`} - - )} + + {`(${ + /** + * we can get away with grabbing the panel count without React state because this count + * is only rendered when the section is collapsed, and the count can only be updated when + * the section isn't collapsed + */ + Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length + } panels)`} + - {isCollapsed && ( - - - - )} + + + ) } diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx new file mode 100644 index 0000000000000..31a6706c4994b --- /dev/null +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import { cloneDeep } from 'lodash'; +import React, { useCallback, useEffect, useState } from 'react'; +import { distinctUntilChanged, map } from 'rxjs'; + +import { EuiButtonIcon, EuiFlexItem, EuiInlineEditTitle, EuiTitle } from '@elastic/eui'; + +import { GridLayoutStateManager } from '../types'; + +export const GridRowTitle = ({ + readOnly, + rowIndex, + editTitleOpen, + setEditTitleOpen, + gridLayoutStateManager, +}: { + readOnly: boolean; + rowIndex: number; + editTitleOpen: boolean; + setEditTitleOpen: (value: boolean) => void; + gridLayoutStateManager: GridLayoutStateManager; +}) => { + const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; + const [rowTitle, setRowTitle] = useState(currentRow.title); + + useEffect(() => { + const titleSubscription = gridLayoutStateManager.gridLayout$ + .pipe( + map((gridLayout) => gridLayout[rowIndex].title), + distinctUntilChanged() + ) + .subscribe((title) => { + setRowTitle(title); + }); + + return () => { + titleSubscription.unsubscribe(); + }; + }, [rowIndex, gridLayoutStateManager]); + + const updateTitle = useCallback( + (title: string) => { + const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.getValue()); + newLayout[rowIndex].title = title; + gridLayoutStateManager.gridLayout$.next(newLayout); + setEditTitleOpen(false); + }, + [rowIndex, setEditTitleOpen, gridLayoutStateManager.gridLayout$] + ); + + return ( + <> + {!readOnly && editTitleOpen ? ( + + setEditTitleOpen(false) } }} + startWithEditOpen + /> + + ) : ( + <> + + +

{rowTitle}

+
+
+ + setEditTitleOpen(true)} /> + + + )} + + ); +}; From aaab60c8cfda8df528e8d038cb7c91abdb3adbae Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 31 Jan 2025 15:28:37 -0700 Subject: [PATCH 05/42] More cleanup --- .../grid/grid_row/grid_row.tsx | 78 +++++++++---------- .../grid/grid_row/grid_row_header.tsx | 4 +- .../grid/grid_row/grid_row_title.tsx | 30 +++++-- 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 50ed379331851..ec994d2442659 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -35,6 +35,7 @@ export const GridRow = ({ const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex]; const containerRef = useRef(null); + const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed); const [panelIds, setPanelIds] = useState(Object.keys(currentRow.panels)); const [panelIdsInOrder, setPanelIdsInOrder] = useState(() => getKeysInOrder(currentRow.panels) @@ -83,12 +84,16 @@ export const GridRow = ({ 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.isCollapsed !== newRowData.isCollapsed) { + setIsCollapsed(newRowData.isCollapsed); + } if ( oldRowData.panelIds.length !== newRowData.panelIds.length || !( @@ -118,34 +123,28 @@ export const GridRow = ({ } }); - /** - * Handle collapsed state via class name - */ - const collapsedStateSubscription = gridLayoutStateManager.gridLayout$ - .pipe( - map((gridLayout) => gridLayout[rowIndex].isCollapsed), - distinctUntilChanged() - ) - .subscribe((isCollapsed) => { - if (!containerRef.current) return; - if (isCollapsed) { - containerRef.current.classList.add('kbnGridRowContainer--collapsed'); - } else { - containerRef.current.classList.remove('kbnGridRowContainer--collapsed'); - } - }); - return () => { interactionStyleSubscription.unsubscribe(); gridLayoutSubscription.unsubscribe(); rowStateSubscription.unsubscribe(); - collapsedStateSubscription.unsubscribe(); }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [rowIndex] ); + /** + * Set a class for the collapsed state in order to control styles of header + */ + useEffect(() => { + if (!containerRef.current) return; + if (isCollapsed) { + containerRef.current.classList.add('kbnGridRowContainer--collapsed'); + } else { + containerRef.current.classList.remove('kbnGridRowContainer--collapsed'); + } + }, [isCollapsed]); + /** * Memoize panel children components (independent of their order) to prevent unnecessary re-renders */ @@ -172,10 +171,6 @@ export const GridRow = ({ ref={containerRef} css={css` height: 100%; - - &.kbnGridRowContainer--collapsed .kbnGridRow { - display: none; - } `} className="kbnGridRowContainer" > @@ -190,25 +185,26 @@ export const GridRow = ({ }} /> )} - -
- (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])} - -
+ {!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])} + +
+ )} ); }; 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 be11f2e44933b..b7ba77347171d 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 @@ -89,9 +89,10 @@ export const GridRowHeader = ({ gutterSize="xs" alignItems="center" css={css` + height: calc(${euiTheme.size.xl} + (2 * ${euiTheme.size.s})); padding: ${euiTheme.size.s} 0px; - border-bottom: none; + border-bottom: 1px solid transparent; // prevents layout shift .kbnGridRowContainer--collapsed & { border-bottom: ${euiTheme.border.thin}; } @@ -135,6 +136,7 @@ export const GridRowHeader = ({ void; + toggleIsCollapsed: () => void; gridLayoutStateManager: GridLayoutStateManager; }) => { const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; @@ -47,6 +49,7 @@ export const GridRowTitle = ({ const updateTitle = useCallback( (title: string) => { + console.log('ON SAVE'); const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.getValue()); newLayout[rowIndex].title = title; gridLayoutStateManager.gridLayout$.next(newLayout); @@ -55,29 +58,40 @@ export const GridRowTitle = ({ [rowIndex, setEditTitleOpen, gridLayoutStateManager.gridLayout$] ); + useEffect(() => { + if (!editTitleOpen) return; + }, [editTitleOpen]); + return ( <> {!readOnly && editTitleOpen ? ( + {/* @ts-ignore - */} setEditTitleOpen(false)} onSave={updateTitle} - editModeProps={{ cancelButtonProps: { onClick: () => setEditTitleOpen(false) } }} + editModeProps={{ + cancelButtonProps: { onClick: () => setEditTitleOpen(false) }, + formRowProps: { className: 'editModeFormRow ' }, + }} startWithEditOpen + inputAriaLabel="Edit title inline" /> ) : ( <> - -

{rowTitle}

-
+ + +

{rowTitle}

+
+
- setEditTitleOpen(true)} /> + setEditTitleOpen(true)} color="text" /> )} From 22eb5113b951d9e7bdf67a632bb30ffdfcc9eaa1 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 31 Jan 2025 16:02:12 -0700 Subject: [PATCH 06/42] Clean up styles + seperate modal --- .../grid/grid_row/delete_grid_row_modal.tsx | 84 ++++++++ .../grid/grid_row/grid_row.tsx | 33 ++- .../grid/grid_row/grid_row_header.tsx | 188 ++++-------------- .../grid/grid_row/grid_row_title.tsx | 1 - .../grid_row/use_grid_row_header_styles.tsx | 47 +++++ .../grid/utils/row_management.ts | 44 ++++ 6 files changed, 229 insertions(+), 168 deletions(-) create mode 100644 src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx create mode 100644 src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx create mode 100644 src/platform/packages/private/kbn-grid-layout/grid/utils/row_management.ts diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx new file mode 100644 index 0000000000000..4b10794ec0aec --- /dev/null +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; + +import { GridLayoutStateManager } from '../types'; +import { deleteRow, movePanelsToRow } from '../utils/row_management'; + +export const DeleteGridRowModal = ({ + rowIndex, + gridLayoutStateManager, + setDeleteModalVisible, +}: { + rowIndex: number; + gridLayoutStateManager: GridLayoutStateManager; + setDeleteModalVisible: (visible: boolean) => void; +}) => { + return ( + { + setDeleteModalVisible(false); + }} + > + + Delete section + + + {`Are you sure you want to remove this section and its ${ + Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length + } panels?`} + + + { + setDeleteModalVisible(false); + }} + > + Cancel + + { + setDeleteModalVisible(false); + const newLayout = deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowIndex); + gridLayoutStateManager.gridLayout$.next(newLayout); + }} + fill + color="danger" + > + Yes + + { + setDeleteModalVisible(false); + let newLayout = movePanelsToRow( + gridLayoutStateManager.gridLayout$.getValue(), + rowIndex, + 0 + ); + newLayout = deleteRow(newLayout, rowIndex); + gridLayoutStateManager.gridLayout$.next(newLayout); + }} + fill + > + Delete section only + + + + ); +}; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index ec994d2442659..8aa458f7fe672 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -7,10 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { css } from '@emotion/react'; import { cloneDeep } from 'lodash'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { map, pairwise, skip, combineLatest, distinctUntilChanged } from 'rxjs'; -import { css } from '@emotion/react'; +import { combineLatest, map, pairwise, skip } from 'rxjs'; import { DragPreview } from '../drag_preview'; import { GridPanel } from '../grid_panel'; @@ -167,13 +167,7 @@ export const GridRow = ({ }, [panelIds, gridLayoutStateManager, renderPanelContents, rowIndex]); return ( -
+
{rowIndex !== 0 && ( (gridLayoutStateManager.rowRefs.current[rowIndex] = element) } - css={css` - height: 100%; - display: grid; - position: relative; - justify-items: stretch; - transition: background-color 300ms linear; - ${initialStyles}; - `} + css={[styles.fullHeight, styles.gridRow, initialStyles]} > {/* render the panels **in order** for accessibility, using the memoized panel components */} {panelIdsInOrder.map((panelId) => children[panelId])} @@ -208,3 +195,15 @@ export const GridRow = ({
); }; + +const styles = { + gridRow: css({ + display: 'grid', + position: 'relative', + justifyItems: 'stretch', + transition: 'background-color 300ms linear', + }), + fullHeight: css({ + height: '100%', + }), +}; 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 b7ba77347171d..17e4b7b644482 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 @@ -6,32 +6,18 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -import { cloneDeep } from 'lodash'; -import React, { useCallback, useEffect, useState } from 'react'; -import { distinctUntilChanged, map } from 'rxjs'; +import React, { useEffect, useState } from 'react'; +import { distinctUntilChanged } from 'rxjs'; -import { - EuiButtonIcon, - EuiModal, - EuiFlexGroup, - EuiFlexItem, - EuiInlineEditTitle, - EuiText, - euiCanAnimate, - useEuiTheme, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, - EuiButton, - EuiButtonEmpty, -} from '@elastic/eui'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { GridLayoutData, GridLayoutStateManager } from '../types'; -import { resolveGridRow } from '../utils/resolve_grid_row'; +import { GridLayoutStateManager } from '../types'; +import { deleteRow } from '../utils/row_management'; +import { DeleteGridRowModal } from './delete_grid_row_modal'; import { GridRowTitle } from './grid_row_title'; +import { useGridRowHeaderStyles } from './use_grid_row_header_styles'; export const GridRowHeader = ({ rowIndex, @@ -42,13 +28,13 @@ export const GridRowHeader = ({ gridLayoutStateManager: GridLayoutStateManager; toggleIsCollapsed: () => void; }) => { - const { euiTheme } = useEuiTheme(); - + const [editTitleOpen, setEditTitleOpen] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [readOnly, setReadOnly] = useState( gridLayoutStateManager.accessMode$.getValue() === 'VIEW' ); - const [editTitleOpen, setEditTitleOpen] = useState(false); + + const headerStyles = useGridRowHeaderStyles(); useEffect(() => { const accessModeSubscription = gridLayoutStateManager.accessMode$ @@ -62,58 +48,12 @@ export const GridRowHeader = ({ }; }, [gridLayoutStateManager]); - const movePanelsToRow = useCallback( - (layout: GridLayoutData, startingRow: number, newRow: number) => { - const newLayout = cloneDeep(layout); - const panelsToMove = newLayout[startingRow].panels; - const maxRow = Math.max( - ...Object.values(newLayout[newRow].panels).map(({ row, height }) => row + height) - ); - Object.keys(panelsToMove).forEach((index) => (panelsToMove[index].row += maxRow)); - newLayout[newRow].panels = { ...newLayout[newRow].panels, ...panelsToMove }; - newLayout[newRow] = resolveGridRow(newLayout[newRow]); - return newLayout; - }, - [] - ); - - const deleteSection = useCallback((layout: GridLayoutData, index: number) => { - const newLayout = cloneDeep(layout); - newLayout.splice(index, 1); - return newLayout; - }, []); - return ( <> @@ -124,13 +64,7 @@ export const GridRowHeader = ({ })} iconType={'arrowDown'} onClick={toggleIsCollapsed} - css={css` - transition: ${euiTheme.animation.extraFast} - transform: rotate(0deg)t; - .kbnGridRowContainer--collapsed & { - transform: rotate(-90deg) !important; - } - `} + css={styles.accordianArrow} /> - + {`(${ /** * we can get away with grabbing the panel count without React state because this count @@ -176,23 +102,14 @@ export const GridRowHeader = ({ gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels ).length; if (!panelCount) { - deleteSection(gridLayoutStateManager.gridLayout$.getValue(), rowIndex); + deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowIndex); } else { setDeleteModalVisible(true); } }} /> - + {deleteModalVisible && ( - { - setDeleteModalVisible(false); - }} - > - - Delete section - - - {`Are you sure you want to remove this section and its ${ - Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length - } panels?`} - - - { - setDeleteModalVisible(false); - }} - > - Cancel - - { - setDeleteModalVisible(false); - const newLayout = deleteSection( - gridLayoutStateManager.gridLayout$.getValue(), - rowIndex - ); - gridLayoutStateManager.gridLayout$.next(newLayout); - }} - fill - color="danger" - > - Yes - - { - setDeleteModalVisible(false); - let newLayout = movePanelsToRow( - gridLayoutStateManager.gridLayout$.getValue(), - rowIndex, - 0 - ); - newLayout = deleteSection(newLayout, rowIndex); - gridLayoutStateManager.gridLayout$.next(newLayout); - }} - fill - > - Delete section only - - - + )} ); }; + +const styles = { + accordianArrow: css({ + transform: 'rotate(0deg)', + '.kbnGridRowContainer--collapsed &': { + transform: 'rotate(-90deg) !important', + }, + }), + hiddenOnCollapsed: css({ + display: 'none', + '.kbnGridRowContainer--collapsed &': { + display: 'block', + }, + }), + floatToRight: css({ + marginLeft: 'auto', + }), +}; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 2c94d44e4b41d..eb325b179a33a 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -49,7 +49,6 @@ export const GridRowTitle = ({ const updateTitle = useCallback( (title: string) => { - console.log('ON SAVE'); const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.getValue()); newLayout[rowIndex].title = title; gridLayoutStateManager.gridLayout$.next(newLayout); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx new file mode 100644 index 0000000000000..f3b07ace4b18f --- /dev/null +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import { useMemo } from 'react'; + +import { euiCanAnimate, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export const useGridRowHeaderStyles = () => { + const { euiTheme } = useEuiTheme(); + + const headerStyles = useMemo(() => { + return css` + height: calc(${euiTheme.size.xl} + (2 * ${euiTheme.size.s})); + padding: ${euiTheme.size.s} 0px; + + border-bottom: 1px solid transparent; // prevents layout shift + .kbnGridRowContainer--collapsed & { + border-bottom: ${euiTheme.border.thin}; + } + + .kbnGridLayout--deleteRowIcon { + margin-left: ${euiTheme.size.xs}; + } + + // these styles hide the delete + move actions by default and only show them on hover + .kbnGridLayout--deleteRowIcon, + .kbnGridLayout--moveRowIcon { + opacity: 0; + ${euiCanAnimate} { + transition: opacity ${euiTheme.animation.extraFast} ease-in; + } + } + &:hover .kbnGridLayout--deleteRowIcon, + &:hover .kbnGridLayout--moveRowIcon { + opacity: 1; + } + `; + }, [euiTheme]); + + return headerStyles; +}; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/utils/row_management.ts b/src/platform/packages/private/kbn-grid-layout/grid/utils/row_management.ts new file mode 100644 index 0000000000000..e81be386a3c56 --- /dev/null +++ b/src/platform/packages/private/kbn-grid-layout/grid/utils/row_management.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import { cloneDeep } from 'lodash'; + +import { GridLayoutData } from '../types'; +import { resolveGridRow } from './resolve_grid_row'; + +/** + * Move the panels in the `startingRow` to the bottom of the `newRow` and resolve the resulting layout + * @param layout Starting layout + * @param startingRow The source row for the panels + * @param newRow The destination row for the panels + * @returns Updated layout with panels moved from `startingRow` to `newRow` + */ +export const movePanelsToRow = (layout: GridLayoutData, startingRow: number, newRow: number) => { + const newLayout = cloneDeep(layout); + const panelsToMove = newLayout[startingRow].panels; + const maxRow = Math.max( + ...Object.values(newLayout[newRow].panels).map(({ row, height }) => row + height) + ); + Object.keys(panelsToMove).forEach((index) => (panelsToMove[index].row += maxRow)); + newLayout[newRow].panels = { ...newLayout[newRow].panels, ...panelsToMove }; + newLayout[newRow] = resolveGridRow(newLayout[newRow]); + newLayout[startingRow] = { ...newLayout[startingRow], panels: {} }; + return newLayout; +}; + +/** + * Deletes an entire row from the layout, including all of its panels + * @param layout Starting layout + * @param rowIndex The row to be deleted + * @returns Updated layout with the row at `rowIndex` deleted + */ +export const deleteRow = (layout: GridLayoutData, rowIndex: number) => { + const newLayout = cloneDeep(layout); + newLayout.splice(rowIndex, 1); + return newLayout; +}; From 2c71617a81500661a8f74a644aa5a20b1418a757 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 31 Jan 2025 16:11:52 -0700 Subject: [PATCH 07/42] Show actions on visible focus + clean up styles --- .../grid/grid_row/grid_row.tsx | 26 +++++++------------ .../grid/grid_row/grid_row_header.tsx | 4 +-- .../grid_row/use_grid_row_header_styles.tsx | 4 ++- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 8aa458f7fe672..b612d98459719 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -7,11 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { css } from '@emotion/react'; +import classNames from 'classnames'; import { cloneDeep } from 'lodash'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { combineLatest, map, pairwise, skip } from 'rxjs'; +import { css } from '@emotion/react'; + import { DragPreview } from '../drag_preview'; import { GridPanel } from '../grid_panel'; import { GridLayoutStateManager } from '../types'; @@ -33,7 +35,6 @@ export const GridRow = ({ gridLayoutStateManager, }: GridRowProps) => { const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex]; - const containerRef = useRef(null); const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed); const [panelIds, setPanelIds] = useState(Object.keys(currentRow.panels)); @@ -133,18 +134,6 @@ export const GridRow = ({ [rowIndex] ); - /** - * Set a class for the collapsed state in order to control styles of header - */ - useEffect(() => { - if (!containerRef.current) return; - if (isCollapsed) { - containerRef.current.classList.add('kbnGridRowContainer--collapsed'); - } else { - containerRef.current.classList.remove('kbnGridRowContainer--collapsed'); - } - }, [isCollapsed]); - /** * Memoize panel children components (independent of their order) to prevent unnecessary re-renders */ @@ -167,7 +156,12 @@ export const GridRow = ({ }, [panelIds, gridLayoutStateManager, renderPanelContents, rowIndex]); return ( -
+
{rowIndex !== 0 && ( void; }) => { + const headerStyles = useGridRowHeaderStyles(); + const [editTitleOpen, setEditTitleOpen] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [readOnly, setReadOnly] = useState( gridLayoutStateManager.accessMode$.getValue() === 'VIEW' ); - const headerStyles = useGridRowHeaderStyles(); - useEffect(() => { const accessModeSubscription = gridLayoutStateManager.accessMode$ .pipe(distinctUntilChanged()) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx index f3b07ace4b18f..896caa08edb07 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx @@ -37,7 +37,9 @@ export const useGridRowHeaderStyles = () => { } } &:hover .kbnGridLayout--deleteRowIcon, - &:hover .kbnGridLayout--moveRowIcon { + &:hover .kbnGridLayout--moveRowIcon, + &:has(:focus-visible) .kbnGridLayout--deleteRowIcon, + &:has(:focus-visible) .kbnGridLayout--moveRowIcon { opacity: 1; } `; From 0add607b3ea47e3ade8b1f2021cfd83d56cc5a9f Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 31 Jan 2025 17:01:45 -0700 Subject: [PATCH 08/42] Add ability to add collapsible sections --- .../components/add_button.tsx | 12 ++++ examples/grid_example/public/app.tsx | 22 ++++++- .../grid/grid_row/grid_row_header.tsx | 58 +++++++++++-------- .../grid/grid_row/grid_row_title.tsx | 12 +++- 4 files changed, 75 insertions(+), 29 deletions(-) diff --git a/examples/embeddable_examples/public/app/presentation_container_example/components/add_button.tsx b/examples/embeddable_examples/public/app/presentation_container_example/components/add_button.tsx index a2f11f55cd757..e1ebd164ae7aa 100644 --- a/examples/embeddable_examples/public/app/presentation_container_example/components/add_button.tsx +++ b/examples/embeddable_examples/public/app/presentation_container_example/components/add_button.tsx @@ -10,11 +10,22 @@ import React, { ReactElement, useEffect, useState } from 'react'; import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { ADD_PANEL_TRIGGER, UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { + PublishingSubject, + ViewMode, + apiPublishesViewMode, + useStateFromPublishingSubject, +} from '@kbn/presentation-publishing'; +import { of } from 'rxjs'; export function AddButton({ pageApi, uiActions }: { pageApi: unknown; uiActions: UiActionsStart }) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [items, setItems] = useState([]); + const viewMode = useStateFromPublishingSubject( + apiPublishesViewMode(pageApi) ? pageApi?.viewMode$ : (of('edit') as PublishingSubject) + ); + useEffect(() => { let cancelled = false; @@ -59,6 +70,7 @@ export function AddButton({ pageApi, uiActions }: { pageApi: unknown; uiActions: onClick={() => { setIsPopoverOpen(!isPopoverOpen); }} + disabled={viewMode !== 'edit'} > Add panel diff --git a/examples/grid_example/public/app.tsx b/examples/grid_example/public/app.tsx index 30bfa74d041a9..5ee3f69adcd81 100644 --- a/examples/grid_example/public/app.tsx +++ b/examples/grid_example/public/app.tsx @@ -196,7 +196,27 @@ export const GridExample = ({ - {' '} + + + + { + mockDashboardApi.rows$.next([ + ...mockDashboardApi.rows$.getValue(), + { + title: i18n.translate('examples.gridExample.defaultSectionTitle', { + defaultMessage: 'New collapsible section', + }), + collapsed: false, + }, + ]); + }} + disabled={viewMode !== 'edit'} + > + {i18n.translate('examples.gridExample.addRowButton', { + defaultMessage: 'Add collapsible section', + })} + {`(${ @@ -92,30 +92,38 @@ export const GridRowHeader = ({ Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length } panels)`} - - { - const panelCount = Object.keys( - gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels - ).length; - if (!panelCount) { - deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowIndex); - } else { - setDeleteModalVisible(true); - } - }} - /> - - - - + {!readOnly && ( + <> + + { + const panelCount = Object.keys( + gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels + ).length; + if (!Boolean(panelCount)) { + const newLayout = deleteRow( + gridLayoutStateManager.gridLayout$.getValue(), + rowIndex + ); + gridLayoutStateManager.gridLayout$.next(newLayout); + } else { + setDeleteModalVisible(true); + } + }} + /> + + + + + + )} ) } diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index eb325b179a33a..98c5a56624c2a 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -89,9 +89,15 @@ export const GridRowTitle = ({ - - setEditTitleOpen(true)} color="text" /> - + {!readOnly && ( + + setEditTitleOpen(true)} + color="text" + /> + + )} )} From 9a9cb6e78386181bf0936560fdda8f0e9d9f0815 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Mon, 3 Feb 2025 15:58:30 -0700 Subject: [PATCH 09/42] Fix error thrown on delete --- .../kbn-grid-layout/grid/grid_layout.tsx | 21 +++++++++++++++---- .../grid/grid_row/grid_row.tsx | 11 +++++----- .../grid/grid_row/grid_row_title.tsx | 2 +- 3 files changed, 24 insertions(+), 10 deletions(-) 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..c12ca843ba42d 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 @@ -24,23 +24,29 @@ import { resolveGridRow } from './utils/resolve_grid_row'; export interface GridLayoutProps { layout: GridLayoutData; gridSettings: GridSettings; + expandedPanelId?: string; + accessMode?: GridAccessMode; + renderPanelContents: ( panelId: string, setDragHandles?: (refs: Array) => void ) => React.ReactNode; onLayoutChange: (newLayout: GridLayoutData) => void; - expandedPanelId?: string; - accessMode?: GridAccessMode; + onRowAdded?: (rowId: string, rowRef: HTMLDivElement | null) => void; + className?: string; // this makes it so that custom CSS can be passed via Emotion } export const GridLayout = ({ layout, gridSettings, - renderPanelContents, - onLayoutChange, expandedPanelId, accessMode = 'EDIT', + + renderPanelContents, + onLayoutChange, + onRowAdded, + className, }: GridLayoutProps) => { const layoutRef = useRef(null); @@ -100,6 +106,13 @@ export const GridLayout = ({ .subscribe(([layoutBefore, layoutAfter]) => { if (!isLayoutEqual(layoutBefore, layoutAfter)) { onLayoutChange(layoutAfter); + if (onRowAdded && layoutBefore.length < layoutAfter.length) { + console.log(gridLayoutStateManager.rowRefs.current.length); + onRowAdded( + `kbnGridLayoutRow--${layoutAfter.length}`, + gridLayoutStateManager.rowRefs.current[layoutAfter.length] + ); + } } }); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index b612d98459719..db34ec516a4c0 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -9,7 +9,7 @@ import classNames from 'classnames'; import { cloneDeep } from 'lodash'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { combineLatest, map, pairwise, skip } from 'rxjs'; import { css } from '@emotion/react'; @@ -84,9 +84,8 @@ export const GridRow = ({ map(([proposedGridLayout, gridLayout]) => { const displayedGridLayout = proposedGridLayout ?? gridLayout; return { - title: displayedGridLayout[rowIndex].title, - isCollapsed: displayedGridLayout[rowIndex].isCollapsed, - panelIds: Object.keys(displayedGridLayout[rowIndex].panels), + isCollapsed: displayedGridLayout[rowIndex]?.isCollapsed ?? false, + panelIds: Object.keys(displayedGridLayout[rowIndex]?.panels ?? {}), }; }), pairwise() @@ -106,7 +105,7 @@ export const GridRow = ({ setPanelIdsInOrder( getKeysInOrder( (gridLayoutStateManager.proposedGridLayout$.getValue() ?? - gridLayoutStateManager.gridLayout$.getValue())[rowIndex].panels + gridLayoutStateManager.gridLayout$.getValue())[rowIndex]?.panels ?? {} ) ); } @@ -118,6 +117,7 @@ export const GridRow = ({ * reasons (screen readers and focus management). */ const gridLayoutSubscription = gridLayoutStateManager.gridLayout$.subscribe((gridLayout) => { + if (!gridLayout[rowIndex]) return; const newPanelIdsInOrder = getKeysInOrder(gridLayout[rowIndex].panels); if (panelIdsInOrder.join() !== newPanelIdsInOrder.join()) { setPanelIdsInOrder(newPanelIdsInOrder); @@ -157,6 +157,7 @@ export const GridRow = ({ return (
{ const titleSubscription = gridLayoutStateManager.gridLayout$ .pipe( - map((gridLayout) => gridLayout[rowIndex].title), + map((gridLayout) => gridLayout[rowIndex]?.title ?? ''), distinctUntilChanged() ) .subscribe((title) => { From 2ece8a5205c06c9a4e814a29c6cb8accaf3c1192 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 4 Feb 2025 11:12:04 -0700 Subject: [PATCH 10/42] Make open animation smoother --- .../grid/grid_row/grid_row.tsx | 14 +- .../grid/grid_row/grid_row_header.tsx | 231 +++++++++--------- .../grid/grid_row/grid_row_title.tsx | 164 +++++++------ 3 files changed, 208 insertions(+), 201 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index db34ec516a4c0..70c93f36664e2 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -9,7 +9,7 @@ import classNames from 'classnames'; import { cloneDeep } from 'lodash'; -import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { combineLatest, map, pairwise, skip } from 'rxjs'; import { css } from '@emotion/react'; @@ -155,6 +155,12 @@ export const GridRow = ({ ); }, [panelIds, gridLayoutStateManager, renderPanelContents, rowIndex]); + const toggleIsCollapsed = useCallback(() => { + const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value); + newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed; + gridLayoutStateManager.gridLayout$.next(newLayout); + }, [rowIndex, gridLayoutStateManager.gridLayout$]); + return (
{ - const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value); - newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed; - gridLayoutStateManager.gridLayout$.next(newLayout); - }} + toggleIsCollapsed={toggleIsCollapsed} /> )} {!isCollapsed && ( 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 b61acc0cda228..19a29ff880c01 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 @@ -19,125 +19,128 @@ import { DeleteGridRowModal } from './delete_grid_row_modal'; import { GridRowTitle } from './grid_row_title'; import { useGridRowHeaderStyles } from './use_grid_row_header_styles'; -export const GridRowHeader = ({ - rowIndex, - gridLayoutStateManager, - toggleIsCollapsed, -}: { - rowIndex: number; - gridLayoutStateManager: GridLayoutStateManager; - toggleIsCollapsed: () => void; -}) => { - const headerStyles = useGridRowHeaderStyles(); +export const GridRowHeader = React.memo( + ({ + rowIndex, + gridLayoutStateManager, + toggleIsCollapsed, + }: { + rowIndex: number; + gridLayoutStateManager: GridLayoutStateManager; + toggleIsCollapsed: () => void; + }) => { + const headerStyles = useGridRowHeaderStyles(); - const [editTitleOpen, setEditTitleOpen] = useState(false); - const [deleteModalVisible, setDeleteModalVisible] = useState(false); - const [readOnly, setReadOnly] = useState( - gridLayoutStateManager.accessMode$.getValue() === 'VIEW' - ); + const [editTitleOpen, setEditTitleOpen] = useState(false); + const [deleteModalVisible, setDeleteModalVisible] = useState(false); + const [readOnly, setReadOnly] = useState( + gridLayoutStateManager.accessMode$.getValue() === 'VIEW' + ); - useEffect(() => { - const accessModeSubscription = gridLayoutStateManager.accessMode$ - .pipe(distinctUntilChanged()) - .subscribe((accessMode) => { - setReadOnly(accessMode === 'VIEW'); - }); + useEffect(() => { + const accessModeSubscription = gridLayoutStateManager.accessMode$ + .pipe(distinctUntilChanged()) + .subscribe((accessMode) => { + setReadOnly(accessMode === 'VIEW'); + }); - return () => { - accessModeSubscription.unsubscribe(); - }; - }, [gridLayoutStateManager]); + return () => { + accessModeSubscription.unsubscribe(); + }; + }, [gridLayoutStateManager]); - return ( - <> - - - + + + + + - - - { - /** - * Add actions at the end of the header section when the layout is editable + the section title - * is not in edit mode - */ - !editTitleOpen && ( - <> - - {`(${ - /** - * we can get away with grabbing the panel count without React state because this count - * is only rendered when the section is collapsed, and the count can only be updated when - * the section isn't collapsed - */ - Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length - } panels)`} - - {!readOnly && ( - <> - - { - const panelCount = Object.keys( - gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels - ).length; - if (!Boolean(panelCount)) { - const newLayout = deleteRow( - gridLayoutStateManager.gridLayout$.getValue(), - rowIndex - ); - gridLayoutStateManager.gridLayout$.next(newLayout); - } else { - setDeleteModalVisible(true); - } - }} - /> - - - - - - )} - - ) - } - - {deleteModalVisible && ( - - )} - - ); -}; + { + /** + * Add actions at the end of the header section when the layout is editable + the section title + * is not in edit mode + */ + !editTitleOpen && ( + <> + + {`(${ + /** + * we can get away with grabbing the panel count without React state because this count + * is only rendered when the section is collapsed, and the count can only be updated when + * the section isn't collapsed + */ + Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels) + .length + } panels)`} + + {!readOnly && ( + <> + + { + const panelCount = Object.keys( + gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels + ).length; + if (!Boolean(panelCount)) { + const newLayout = deleteRow( + gridLayoutStateManager.gridLayout$.getValue(), + rowIndex + ); + gridLayoutStateManager.gridLayout$.next(newLayout); + } else { + setDeleteModalVisible(true); + } + }} + /> + + + + + + )} + + ) + } + + {deleteModalVisible && ( + + )} + + ); + } +); const styles = { accordianArrow: css({ diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 31b94366dbc89..afe818010c15d 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -14,92 +14,94 @@ import { EuiButtonIcon, EuiFlexItem, EuiInlineEditTitle, EuiLink, EuiTitle } fro import { GridLayoutStateManager } from '../types'; -export const GridRowTitle = ({ - readOnly, - rowIndex, - editTitleOpen, - setEditTitleOpen, - toggleIsCollapsed, - gridLayoutStateManager, -}: { - readOnly: boolean; - rowIndex: number; - editTitleOpen: boolean; - setEditTitleOpen: (value: boolean) => void; - toggleIsCollapsed: () => void; - gridLayoutStateManager: GridLayoutStateManager; -}) => { - const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; - const [rowTitle, setRowTitle] = useState(currentRow.title); +export const GridRowTitle = React.memo( + ({ + readOnly, + rowIndex, + editTitleOpen, + setEditTitleOpen, + toggleIsCollapsed, + gridLayoutStateManager, + }: { + readOnly: boolean; + rowIndex: number; + editTitleOpen: boolean; + setEditTitleOpen: (value: boolean) => void; + toggleIsCollapsed: () => void; + gridLayoutStateManager: GridLayoutStateManager; + }) => { + const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; + const [rowTitle, setRowTitle] = useState(currentRow.title); - useEffect(() => { - const titleSubscription = gridLayoutStateManager.gridLayout$ - .pipe( - map((gridLayout) => gridLayout[rowIndex]?.title ?? ''), - distinctUntilChanged() - ) - .subscribe((title) => { - setRowTitle(title); - }); + useEffect(() => { + const titleSubscription = gridLayoutStateManager.gridLayout$ + .pipe( + map((gridLayout) => gridLayout[rowIndex]?.title ?? ''), + distinctUntilChanged() + ) + .subscribe((title) => { + setRowTitle(title); + }); - return () => { - titleSubscription.unsubscribe(); - }; - }, [rowIndex, gridLayoutStateManager]); + return () => { + titleSubscription.unsubscribe(); + }; + }, [rowIndex, gridLayoutStateManager]); - const updateTitle = useCallback( - (title: string) => { - const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.getValue()); - newLayout[rowIndex].title = title; - gridLayoutStateManager.gridLayout$.next(newLayout); - setEditTitleOpen(false); - }, - [rowIndex, setEditTitleOpen, gridLayoutStateManager.gridLayout$] - ); + const updateTitle = useCallback( + (title: string) => { + const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.getValue()); + newLayout[rowIndex].title = title; + gridLayoutStateManager.gridLayout$.next(newLayout); + setEditTitleOpen(false); + }, + [rowIndex, setEditTitleOpen, gridLayoutStateManager.gridLayout$] + ); - useEffect(() => { - if (!editTitleOpen) return; - }, [editTitleOpen]); + useEffect(() => { + if (!editTitleOpen) return; + }, [editTitleOpen]); - return ( - <> - {!readOnly && editTitleOpen ? ( - - {/* @ts-ignore - */} - setEditTitleOpen(false)} - onSave={updateTitle} - editModeProps={{ - cancelButtonProps: { onClick: () => setEditTitleOpen(false) }, - formRowProps: { className: 'editModeFormRow ' }, - }} - startWithEditOpen - inputAriaLabel="Edit title inline" - /> - - ) : ( - <> - - - -

{rowTitle}

-
-
+ return ( + <> + {!readOnly && editTitleOpen ? ( + + {/* @ts-ignore - EUI typing issue that will be resolved with https://github.com/elastic/eui/pull/8307 */} + setEditTitleOpen(false)} + onSave={updateTitle} + editModeProps={{ + cancelButtonProps: { onClick: () => setEditTitleOpen(false) }, + formRowProps: { className: 'editModeFormRow ' }, + }} + startWithEditOpen + inputAriaLabel="Edit title inline" + /> - {!readOnly && ( + ) : ( + <> - setEditTitleOpen(true)} - color="text" - /> + + +

{rowTitle}

+
+
- )} - - )} - - ); -}; + {!readOnly && ( + + setEditTitleOpen(true)} + color="text" + /> + + )} + + )} + + ); + } +); From 6c0c83a2ab53564f22412e42ab98aa866703cf3c Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 4 Feb 2025 13:08:26 -0700 Subject: [PATCH 11/42] Undo anonymous components --- .../private/kbn-grid-layout/grid/grid_row/grid_row_header.tsx | 2 ++ .../private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx | 2 ++ 2 files changed, 4 insertions(+) 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 19a29ff880c01..097fbce619ed4 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 @@ -159,3 +159,5 @@ const styles = { marginLeft: 'auto', }), }; + +GridRowHeader.displayName = 'GridRowHeader'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index afe818010c15d..dc86564bff623 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -105,3 +105,5 @@ export const GridRowTitle = React.memo( ); } ); + +GridRowTitle.displayName = 'GridRowTitle'; From bda847549bc23273ea393f754082f86bc8abfab6 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 4 Feb 2025 14:58:03 -0700 Subject: [PATCH 12/42] Scroll to bottom on section add --- examples/grid_example/public/app.tsx | 4 ++++ .../private/kbn-grid-layout/grid/grid_layout.tsx | 9 --------- .../packages/private/kbn-grid-layout/grid/types.ts | 14 -------------- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/examples/grid_example/public/app.tsx b/examples/grid_example/public/app.tsx index 5ee3f69adcd81..cdc8ef9c95519 100644 --- a/examples/grid_example/public/app.tsx +++ b/examples/grid_example/public/app.tsx @@ -210,6 +210,10 @@ export const GridExample = ({ collapsed: false, }, ]); + // scroll to bottom after row is added + new Promise((resolve) => setTimeout(resolve, 100)).then(() => { + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + }); }} disabled={viewMode !== 'edit'} > 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 c12ca843ba42d..4f31e7153c3b1 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 @@ -32,7 +32,6 @@ export interface GridLayoutProps { setDragHandles?: (refs: Array) => void ) => React.ReactNode; onLayoutChange: (newLayout: GridLayoutData) => void; - onRowAdded?: (rowId: string, rowRef: HTMLDivElement | null) => void; className?: string; // this makes it so that custom CSS can be passed via Emotion } @@ -45,7 +44,6 @@ export const GridLayout = ({ renderPanelContents, onLayoutChange, - onRowAdded, className, }: GridLayoutProps) => { @@ -106,13 +104,6 @@ export const GridLayout = ({ .subscribe(([layoutBefore, layoutAfter]) => { if (!isLayoutEqual(layoutBefore, layoutAfter)) { onLayoutChange(layoutAfter); - if (onRowAdded && layoutBefore.length < layoutAfter.length) { - console.log(gridLayoutStateManager.rowRefs.current.length); - onRowAdded( - `kbnGridLayoutRow--${layoutAfter.length}`, - gridLayoutStateManager.rowRefs.current[layoutAfter.length] - ); - } } }); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/types.ts b/src/platform/packages/private/kbn-grid-layout/grid/types.ts index dc68877ec3a5b..dd9c1942a1e91 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/types.ts +++ b/src/platform/packages/private/kbn-grid-layout/grid/types.ts @@ -107,18 +107,4 @@ export interface PanelInteractionEvent { }; } -// TODO: Remove from Dashboard plugin as part of https://github.com/elastic/kibana/issues/190446 -export enum PanelPlacementStrategy { - /** Place on the very top of the grid layout, add the height of this panel to all other panels. */ - placeAtTop = 'placeAtTop', - /** Look for the smallest y and x value where the default panel will fit. */ - findTopLeftMostOpenSpace = 'findTopLeftMostOpenSpace', -} - -export interface PanelPlacementSettings { - strategy?: PanelPlacementStrategy; - height: number; - width: number; -} - export type GridAccessMode = 'VIEW' | 'EDIT'; From 07356ccc16e638b900e95a44c4b0891f2d0fef6c Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 4 Feb 2025 16:26:48 -0700 Subject: [PATCH 13/42] Cleaner implementation --- examples/grid_example/public/app.tsx | 50 ++++++++++++++++++---------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/examples/grid_example/public/app.tsx b/examples/grid_example/public/app.tsx index cdc8ef9c95519..21b9a298fa8b6 100644 --- a/examples/grid_example/public/app.tsx +++ b/examples/grid_example/public/app.tsx @@ -10,7 +10,7 @@ import deepEqual from 'fast-deep-equal'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; -import { combineLatest, debounceTime } from 'rxjs'; +import { Subject, combineLatest, debounceTime, map, skip, take } from 'rxjs'; import { EuiBadge, @@ -75,30 +75,43 @@ export const GridExample = ({ mockDashboardApi.viewMode$, mockDashboardApi.expandedPanelId$ ); + const layoutUpdated$ = useMemo(() => new Subject(), []); useEffect(() => { combineLatest([mockDashboardApi.panels$, mockDashboardApi.rows$]) - .pipe(debounceTime(0)) // debounce to avoid subscribe being called twice when both panels$ and rows$ publish - .subscribe(([panels, rows]) => { - const panelIds = Object.keys(panels); - let panelsAreEqual = true; - for (const panelId of panelIds) { - if (!panelsAreEqual) break; - const currentPanel = panels[panelId]; - const savedPanel = savedState.current.panels[panelId]; - panelsAreEqual = deepEqual( - { row: 0, ...currentPanel.gridData }, - { row: 0, ...savedPanel.gridData } - ); - } - - const hasChanges = !(panelsAreEqual && deepEqual(rows, savedState.current.rows)); + .pipe( + debounceTime(0), // debounce to avoid subscribe being called twice when both panels$ and rows$ publish + map(([panels, rows]) => { + const panelIds = Object.keys(panels); + let panelsAreEqual = true; + for (const panelId of panelIds) { + if (!panelsAreEqual) break; + const currentPanel = panels[panelId]; + const savedPanel = savedState.current.panels[panelId]; + panelsAreEqual = deepEqual( + { row: 0, ...currentPanel.gridData }, + { row: 0, ...savedPanel.gridData } + ); + } + const hasChanges = !(panelsAreEqual && deepEqual(rows, savedState.current.rows)); + return { hasChanges, updatedLayout: dashboardInputToGridLayout({ panels, rows }) }; + }) + ) + .subscribe(({ hasChanges, updatedLayout }) => { setHasUnsavedChanges(hasChanges); - setCurrentLayout(dashboardInputToGridLayout({ panels, rows })); + setCurrentLayout(updatedLayout); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + /** + * On layout update, emit `layoutUpdated$` so that side effects such as scroll to bottom on section + * add can happen + */ + useEffect(() => { + layoutUpdated$.next(); + }, [currentLayout, layoutUpdated$]); + const renderPanelContents = useCallback( (id: string, setDragHandles?: (refs: Array) => void) => { const currentPanels = mockDashboardApi.panels$.getValue(); @@ -210,8 +223,9 @@ export const GridExample = ({ collapsed: false, }, ]); + // scroll to bottom after row is added - new Promise((resolve) => setTimeout(resolve, 100)).then(() => { + layoutUpdated$.pipe(skip(1), take(1)).subscribe(() => { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); }); }} From dff845601d8001c98ba1616ebf32f90986ea2f5d Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 4 Feb 2025 16:53:31 -0700 Subject: [PATCH 14/42] Clean up missed import --- .../packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 70c93f36664e2..bf5d838de8ff6 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -9,7 +9,7 @@ import classNames from 'classnames'; import { cloneDeep } from 'lodash'; -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { combineLatest, map, pairwise, skip } from 'rxjs'; import { css } from '@emotion/react'; From 23ccf10c6bcb49d313d955f526300a41c118f800 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 5 Feb 2025 09:03:58 -0700 Subject: [PATCH 15/42] Cleanup example app --- examples/grid_example/public/app.tsx | 183 ++++++------------ .../public/grid_layout_options.tsx | 116 +++++++++++ examples/grid_example/public/types.ts | 18 ++ .../public/use_mock_dashboard_api.tsx | 15 +- .../kbn-grid-layout/grid/grid_layout.tsx | 12 +- 5 files changed, 203 insertions(+), 141 deletions(-) create mode 100644 examples/grid_example/public/grid_layout_options.tsx diff --git a/examples/grid_example/public/app.tsx b/examples/grid_example/public/app.tsx index 21b9a298fa8b6..6a2df79bbb80f 100644 --- a/examples/grid_example/public/app.tsx +++ b/examples/grid_example/public/app.tsx @@ -16,14 +16,10 @@ import { EuiBadge, EuiButton, EuiButtonEmpty, - EuiButtonGroup, EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiPageTemplate, - EuiPopover, - EuiRange, EuiSpacer, transparentize, useEuiTheme, @@ -33,12 +29,13 @@ import { AppMountParameters } from '@kbn/core-application-browser'; import { CoreStart } from '@kbn/core-lifecycle-browser'; import { AddEmbeddableButton } from '@kbn/embeddable-examples-plugin/public'; import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; -import { GridLayout, GridLayoutData } from '@kbn/grid-layout'; +import { GridLayout, GridLayoutData, GridSettings } from '@kbn/grid-layout'; import { i18n } from '@kbn/i18n'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { GridLayoutOptions } from './grid_layout_options'; import { clearSerializedDashboardState, getSerializedDashboardState, @@ -66,9 +63,11 @@ export const GridExample = ({ const [currentLayout, setCurrentLayout] = useState( dashboardInputToGridLayout(savedState.current) ); - const [isSettingsPopoverOpen, setIsSettingsPopoverOpen] = useState(false); - const [gutterSize, setGutterSize] = useState(DASHBOARD_MARGIN_SIZE); - const [rowHeight, setRowHeight] = useState(DASHBOARD_GRID_HEIGHT); + const [gridSettings, setGridSettings] = useState({ + gutterSize: DASHBOARD_MARGIN_SIZE, + rowHeight: DASHBOARD_GRID_HEIGHT, + columnCount: DASHBOARD_GRID_COLUMN_COUNT, + }); const mockDashboardApi = useMockDashboardApi({ savedState: savedState.current }); const [viewMode, expandedPanelId] = useBatchedPublishingSubjects( @@ -105,8 +104,8 @@ export const GridExample = ({ }, []); /** - * On layout update, emit `layoutUpdated$` so that side effects such as scroll to bottom on section - * add can happen + * On layout update, emit `layoutUpdated$` so that side effects to layout updates can + * happen (such as scrolling to the bottom of the screen after adding a new section) */ useEffect(() => { layoutUpdated$.next(); @@ -135,6 +134,41 @@ export const GridExample = ({ [mockDashboardApi] ); + const onLayoutChange = useCallback( + (newLayout: GridLayoutData) => { + const { panels, rows } = gridLayoutToDashboardPanelMap( + mockDashboardApi.panels$.getValue(), + newLayout + ); + mockDashboardApi.panels$.next(panels); + mockDashboardApi.rows$.next(rows); + }, + [mockDashboardApi.panels$, mockDashboardApi.rows$] + ); + + const addNewSection = useCallback(() => { + mockDashboardApi.rows$.next([ + ...mockDashboardApi.rows$.getValue(), + { + title: i18n.translate('examples.gridExample.defaultSectionTitle', { + defaultMessage: 'New collapsible section', + }), + collapsed: false, + }, + ]); + + // scroll to bottom after row is added + layoutUpdated$.pipe(skip(1), take(1)).subscribe(() => { + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + }); + }, [mockDashboardApi.rows$, layoutUpdated$]); + + const resetUnsavedChanges = useCallback(() => { + const { panels, rows } = savedState.current; + mockDashboardApi.panels$.next(panels); + mockDashboardApi.rows$.next(rows); + }, [mockDashboardApi.panels$, mockDashboardApi.rows$]); + const customLayoutCss = useMemo(() => { const gridColor = transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.2); return css` @@ -212,111 +246,19 @@ export const GridExample = ({
- { - mockDashboardApi.rows$.next([ - ...mockDashboardApi.rows$.getValue(), - { - title: i18n.translate('examples.gridExample.defaultSectionTitle', { - defaultMessage: 'New collapsible section', - }), - collapsed: false, - }, - ]); - - // scroll to bottom after row is added - layoutUpdated$.pipe(skip(1), take(1)).subscribe(() => { - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); - }); - }} - disabled={viewMode !== 'edit'} - > + {i18n.translate('examples.gridExample.addRowButton', { defaultMessage: 'Add collapsible section', })} - setIsSettingsPopoverOpen(!isSettingsPopoverOpen)} - > - {i18n.translate('examples.gridExample.settingsPopover.title', { - defaultMessage: 'Layout settings', - })} - - } - isOpen={isSettingsPopoverOpen} - closePopover={() => setIsSettingsPopoverOpen(false)} - > - <> - - { - mockDashboardApi.viewMode$.next(id); - }} - /> - - - setGutterSize(parseInt(e.currentTarget.value, 10))} - showLabels - showValue - /> - - - setRowHeight(parseInt(e.currentTarget.value, 10))} - showLabels - showValue - /> - - - + @@ -332,13 +274,7 @@ export const GridExample = ({ )} - { - const { panels, rows } = savedState.current; - mockDashboardApi.panels$.next(panels); - mockDashboardApi.rows$.next(rows); - }} - > + {i18n.translate('examples.gridExample.resetLayoutButton', { defaultMessage: 'Reset', })} @@ -370,20 +306,9 @@ export const GridExample = ({ accessMode={viewMode === 'view' ? 'VIEW' : 'EDIT'} expandedPanelId={expandedPanelId} layout={currentLayout} - gridSettings={{ - gutterSize, - rowHeight, - columnCount: DASHBOARD_GRID_COLUMN_COUNT, - }} + gridSettings={gridSettings} renderPanelContents={renderPanelContents} - onLayoutChange={(newLayout) => { - const { panels, rows } = gridLayoutToDashboardPanelMap( - mockDashboardApi.panels$.getValue(), - newLayout - ); - mockDashboardApi.panels$.next(panels); - mockDashboardApi.rows$.next(rows); - }} + onLayoutChange={onLayoutChange} css={customLayoutCss} /> diff --git a/examples/grid_example/public/grid_layout_options.tsx b/examples/grid_example/public/grid_layout_options.tsx new file mode 100644 index 0000000000000..13893f8525dbb --- /dev/null +++ b/examples/grid_example/public/grid_layout_options.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useState } from 'react'; + +import { EuiButton, EuiButtonGroup, EuiFormRow, EuiPopover, EuiRange } from '@elastic/eui'; +import { GridSettings } from '@kbn/grid-layout'; +import { i18n } from '@kbn/i18n'; +import { ViewMode } from '@kbn/presentation-publishing'; +import { MockDashboardApi } from './types'; + +export const GridLayoutOptions = ({ + viewMode, + mockDashboardApi, + gridSettings, + setGridSettings, +}: { + viewMode: ViewMode; + mockDashboardApi: MockDashboardApi; + gridSettings: GridSettings; + setGridSettings: (settings: GridSettings) => void; +}) => { + const [isSettingsPopoverOpen, setIsSettingsPopoverOpen] = useState(false); + + return ( + setIsSettingsPopoverOpen(!isSettingsPopoverOpen)} + > + {i18n.translate('examples.gridExample.settingsPopover.title', { + defaultMessage: 'Layout settings', + })} + + } + isOpen={isSettingsPopoverOpen} + closePopover={() => setIsSettingsPopoverOpen(false)} + > + <> + + { + mockDashboardApi.viewMode$.next(id); + }} + /> + + + + setGridSettings({ ...gridSettings, gutterSize: parseInt(e.currentTarget.value, 10) }) + } + showLabels + showValue + /> + + + + setGridSettings({ ...gridSettings, rowHeight: parseInt(e.currentTarget.value, 10) }) + } + showLabels + showValue + /> + + + + ); +}; diff --git a/examples/grid_example/public/types.ts b/examples/grid_example/public/types.ts index 141ba96ed2a75..c9870572a6502 100644 --- a/examples/grid_example/public/types.ts +++ b/examples/grid_example/public/types.ts @@ -7,6 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { + CanAddNewPanel, + CanExpandPanels, + HasSerializedChildState, + PresentationContainer, +} from '@kbn/presentation-containers'; +import { PublishesViewMode } from '@kbn/presentation-publishing'; +import { BehaviorSubject } from 'rxjs'; + export interface DashboardGridData { w: number; h: number; @@ -32,3 +41,12 @@ export interface MockSerializedDashboardState { panels: MockedDashboardPanelMap; rows: MockedDashboardRowMap; } + +export type MockDashboardApi = PresentationContainer & + CanAddNewPanel & + HasSerializedChildState & + PublishesViewMode & + CanExpandPanels & { + panels$: BehaviorSubject; + rows$: BehaviorSubject; + }; diff --git a/examples/grid_example/public/use_mock_dashboard_api.tsx b/examples/grid_example/public/use_mock_dashboard_api.tsx index 5268c65184b6b..31a0686117dbf 100644 --- a/examples/grid_example/public/use_mock_dashboard_api.tsx +++ b/examples/grid_example/public/use_mock_dashboard_api.tsx @@ -15,7 +15,9 @@ import { v4 } from 'uuid'; import { TimeRange } from '@kbn/es-query'; import { PanelPackage } from '@kbn/presentation-containers'; +import { ViewMode } from '@kbn/presentation-publishing'; import { + MockDashboardApi, MockSerializedDashboardState, MockedDashboardPanelMap, MockedDashboardRowMap, @@ -29,7 +31,7 @@ export const useMockDashboardApi = ({ savedState, }: { savedState: MockSerializedDashboardState; -}) => { +}): MockDashboardApi => { const mockDashboardApi = useMemo(() => { const panels$ = new BehaviorSubject(savedState.panels); const expandedPanelId$ = new BehaviorSubject(undefined); @@ -48,8 +50,11 @@ export const useMockDashboardApi = ({ }), filters$: new BehaviorSubject([]), query$: new BehaviorSubject(''), - viewMode$: new BehaviorSubject('edit'), + viewMode$: new BehaviorSubject('edit'), panels$, + getPanelCount: () => { + return Object.keys(panels$.getValue()).length; + }, rows$: new BehaviorSubject(savedState.rows), expandedPanelId$, expandPanel: (id: string) => { @@ -64,7 +69,7 @@ export const useMockDashboardApi = ({ delete panels[id]; // the grid layout component will handle compacting, if necessary mockDashboardApi.panels$.next(panels); }, - replacePanel: (id: string, newPanel: PanelPackage) => { + replacePanel: async (id: string, newPanel: PanelPackage): Promise => { const currentPanels = mockDashboardApi.panels$.getValue(); const otherPanels = { ...currentPanels }; const oldPanel = currentPanels[id]; @@ -75,8 +80,9 @@ export const useMockDashboardApi = ({ explicitInput: { ...newPanel.initialState, id: newId }, }; mockDashboardApi.panels$.next(otherPanels); + return newId; }, - addNewPanel: async (panelPackage: PanelPackage) => { + addNewPanel: async (panelPackage: PanelPackage): Promise => { // we are only implementing "place at top" here, for demo purposes const currentPanels = mockDashboardApi.panels$.getValue(); const otherPanels = { ...currentPanels }; @@ -104,6 +110,7 @@ export const useMockDashboardApi = ({ }, }, }); + return undefined; }, canRemovePanels: () => true, }; 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 4f31e7153c3b1..f42d61321ad59 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 @@ -24,27 +24,23 @@ import { resolveGridRow } from './utils/resolve_grid_row'; export interface GridLayoutProps { layout: GridLayoutData; gridSettings: GridSettings; - expandedPanelId?: string; - accessMode?: GridAccessMode; - renderPanelContents: ( panelId: string, setDragHandles?: (refs: Array) => void ) => React.ReactNode; onLayoutChange: (newLayout: GridLayoutData) => void; - + expandedPanelId?: string; + accessMode?: GridAccessMode; className?: string; // this makes it so that custom CSS can be passed via Emotion } export const GridLayout = ({ layout, gridSettings, - expandedPanelId, - accessMode = 'EDIT', - renderPanelContents, onLayoutChange, - + expandedPanelId, + accessMode = 'EDIT', className, }: GridLayoutProps) => { const layoutRef = useRef(null); From 72957b41293feb2647fec8d93a21e71e6b45786c Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 5 Feb 2025 09:35:55 -0700 Subject: [PATCH 16/42] More code cleanup --- .../public/use_mock_dashboard_api.tsx | 1 - .../grid/grid_row/delete_grid_row_modal.tsx | 24 ++++++++++++++----- .../grid/grid_row/grid_row.tsx | 1 + .../grid/grid_row/grid_row_header.tsx | 16 ++++++++++--- .../grid/grid_row/grid_row_title.tsx | 7 +++--- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/examples/grid_example/public/use_mock_dashboard_api.tsx b/examples/grid_example/public/use_mock_dashboard_api.tsx index 31a0686117dbf..74193166db50e 100644 --- a/examples/grid_example/public/use_mock_dashboard_api.tsx +++ b/examples/grid_example/public/use_mock_dashboard_api.tsx @@ -110,7 +110,6 @@ export const useMockDashboardApi = ({ }, }, }); - return undefined; }, canRemovePanels: () => true, }; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx index 4b10794ec0aec..003f09fa0f199 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx @@ -20,6 +20,7 @@ import { import { GridLayoutStateManager } from '../types'; import { deleteRow, movePanelsToRow } from '../utils/row_management'; +import { i18n } from '@kbn/i18n'; export const DeleteGridRowModal = ({ rowIndex, @@ -40,9 +41,14 @@ export const DeleteGridRowModal = ({ Delete section - {`Are you sure you want to remove this section and its ${ - Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length - } panels?`} + {i18n.translate('kbnGridLayout.deleteGridRowModal.body', { + defaultMessage: + 'Are you sure you want to remove this section and its {panelCount} {panelCount, plural, one {panel} other {panels}}?', + values: { + panelCount: Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels) + .length, + }, + })} - Cancel + {i18n.translate('kbnGridLayout.deleteGridRowModal.cancelButton', { + defaultMessage: 'Cancel', + })} { @@ -61,7 +69,9 @@ export const DeleteGridRowModal = ({ fill color="danger" > - Yes + {i18n.translate('kbnGridLayout.deleteGridRowModal.confirmDeleteAllPanels', { + defaultMessage: 'Yes', + })} { @@ -76,7 +86,9 @@ export const DeleteGridRowModal = ({ }} fill > - Delete section only + {i18n.translate('kbnGridLayout.deleteGridRowModal.confirmDeleteSection', { + defaultMessage: 'Delete section only', + })} diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index bf5d838de8ff6..719d8f0665ed2 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -74,6 +74,7 @@ export const GridRow = ({ /** * This subscription ensures that the row will re-render when one of the following changes: + * - Collapsed state * - Panel IDs (adding/removing/replacing, but not reordering) */ const rowStateSubscription = combineLatest([ 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 097fbce619ed4..2f367f1707f2d 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 @@ -38,6 +38,10 @@ export const GridRowHeader = React.memo( ); useEffect(() => { + /** + * This subscription is responsible for controlling whether or not the section title is + * editable and hiding all other "edit mode" actions (delete section, move section, etc) + */ const accessModeSubscription = gridLayoutStateManager.accessMode$ .pipe(distinctUntilChanged()) .subscribe((accessMode) => { @@ -86,7 +90,7 @@ export const GridRowHeader = React.memo( {`(${ /** - * we can get away with grabbing the panel count without React state because this count + * We can get away with grabbing the panel count without React state because this count * is only rendered when the section is collapsed, and the count can only be updated when * the section isn't collapsed */ @@ -118,11 +122,17 @@ export const GridRowHeader = React.memo( /> - + /> */} )} diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index dc86564bff623..e16bb2dc6badd 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -34,6 +34,9 @@ export const GridRowTitle = React.memo( const [rowTitle, setRowTitle] = useState(currentRow.title); useEffect(() => { + /** + * This subscription ensures that the row will re-render when the section title changes + */ const titleSubscription = gridLayoutStateManager.gridLayout$ .pipe( map((gridLayout) => gridLayout[rowIndex]?.title ?? ''), @@ -58,10 +61,6 @@ export const GridRowTitle = React.memo( [rowIndex, setEditTitleOpen, gridLayoutStateManager.gridLayout$] ); - useEffect(() => { - if (!editTitleOpen) return; - }, [editTitleOpen]); - return ( <> {!readOnly && editTitleOpen ? ( From 791e9f355e5cc4abb01d6e485134364c8bd678da Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 5 Feb 2025 09:38:29 -0700 Subject: [PATCH 17/42] Update comment --- .../private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index e16bb2dc6badd..24141c58d11a2 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -35,7 +35,7 @@ export const GridRowTitle = React.memo( useEffect(() => { /** - * This subscription ensures that the row will re-render when the section title changes + * This subscription ensures that this component will re-render when the title changes */ const titleSubscription = gridLayoutStateManager.gridLayout$ .pipe( From a6f24d401d7d2b6386cc6888327687204c35f24e Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 5 Feb 2025 09:40:20 -0700 Subject: [PATCH 18/42] Final small cleanup --- .../kbn-grid-layout/grid/grid_row/grid_row_title.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 24141c58d11a2..db05532b1797a 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -70,12 +70,8 @@ export const GridRowTitle = React.memo( size="xs" heading="h2" defaultValue={rowTitle} - onCancel={() => setEditTitleOpen(false)} onSave={updateTitle} - editModeProps={{ - cancelButtonProps: { onClick: () => setEditTitleOpen(false) }, - formRowProps: { className: 'editModeFormRow ' }, - }} + onCancel={() => setEditTitleOpen(false)} startWithEditOpen inputAriaLabel="Edit title inline" /> From 12eec427ac4ee6f56e3933246cefb85dbad89b01 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 5 Feb 2025 10:05:57 -0700 Subject: [PATCH 19/42] Replace missed `i18n` --- .../private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index db05532b1797a..99c0ac54bbccb 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -11,6 +11,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { distinctUntilChanged, map } from 'rxjs'; import { EuiButtonIcon, EuiFlexItem, EuiInlineEditTitle, EuiLink, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { GridLayoutStateManager } from '../types'; @@ -73,7 +74,9 @@ export const GridRowTitle = React.memo( onSave={updateTitle} onCancel={() => setEditTitleOpen(false)} startWithEditOpen - inputAriaLabel="Edit title inline" + inputAriaLabel={i18n.translate('kbnGridLayout.row.editTitleAriaLabel', { + defaultMessage: 'Edit section title', + })} /> ) : ( From 77335abf8505ec99c21faafb28e046d3738e3002 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:33:05 +0000 Subject: [PATCH 20/42] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx index 003f09fa0f199..6a88fd3049ca6 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx @@ -18,9 +18,9 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { GridLayoutStateManager } from '../types'; import { deleteRow, movePanelsToRow } from '../utils/row_management'; -import { i18n } from '@kbn/i18n'; export const DeleteGridRowModal = ({ rowIndex, From 50b427c76984aae266287ce850acd6e8923abfc6 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 5 Feb 2025 10:40:32 -0700 Subject: [PATCH 21/42] Fix types --- examples/grid_example/public/grid_layout_options.tsx | 2 +- examples/grid_example/public/types.ts | 4 ++-- examples/grid_example/public/use_mock_dashboard_api.tsx | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/grid_example/public/grid_layout_options.tsx b/examples/grid_example/public/grid_layout_options.tsx index 13893f8525dbb..eccbf77185837 100644 --- a/examples/grid_example/public/grid_layout_options.tsx +++ b/examples/grid_example/public/grid_layout_options.tsx @@ -73,7 +73,7 @@ export const GridLayoutOptions = ({ ]} idSelected={viewMode} onChange={(id) => { - mockDashboardApi.viewMode$.next(id); + mockDashboardApi.setViewMode(id as ViewMode); }} /> diff --git a/examples/grid_example/public/types.ts b/examples/grid_example/public/types.ts index c9870572a6502..705b652e3d6bf 100644 --- a/examples/grid_example/public/types.ts +++ b/examples/grid_example/public/types.ts @@ -13,7 +13,7 @@ import { HasSerializedChildState, PresentationContainer, } from '@kbn/presentation-containers'; -import { PublishesViewMode } from '@kbn/presentation-publishing'; +import { PublishesWritableViewMode } from '@kbn/presentation-publishing'; import { BehaviorSubject } from 'rxjs'; export interface DashboardGridData { @@ -45,7 +45,7 @@ export interface MockSerializedDashboardState { export type MockDashboardApi = PresentationContainer & CanAddNewPanel & HasSerializedChildState & - PublishesViewMode & + PublishesWritableViewMode & CanExpandPanels & { panels$: BehaviorSubject; rows$: BehaviorSubject; diff --git a/examples/grid_example/public/use_mock_dashboard_api.tsx b/examples/grid_example/public/use_mock_dashboard_api.tsx index 74193166db50e..ec91fc762cabd 100644 --- a/examples/grid_example/public/use_mock_dashboard_api.tsx +++ b/examples/grid_example/public/use_mock_dashboard_api.tsx @@ -35,6 +35,7 @@ export const useMockDashboardApi = ({ const mockDashboardApi = useMemo(() => { const panels$ = new BehaviorSubject(savedState.panels); const expandedPanelId$ = new BehaviorSubject(undefined); + const viewMode$ = new BehaviorSubject('edit'); return { getSerializedStateForChild: (id: string) => { @@ -50,7 +51,8 @@ export const useMockDashboardApi = ({ }), filters$: new BehaviorSubject([]), query$: new BehaviorSubject(''), - viewMode$: new BehaviorSubject('edit'), + viewMode$, + setViewMode: (viewMode: ViewMode) => viewMode$.next(viewMode), panels$, getPanelCount: () => { return Object.keys(panels$.getValue()).length; From 9f5fae371fd90c6af7ee9cc5b4d1102107e6f348 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 5 Feb 2025 15:03:26 -0700 Subject: [PATCH 22/42] Add tests + add missed i18n --- .../grid/grid_row/grid_row_header.test.tsx | 128 ++++++++++++++++++ .../grid/grid_row/grid_row_header.tsx | 94 ++++++++----- .../grid/grid_row/grid_row_title.tsx | 7 +- 3 files changed, 193 insertions(+), 36 deletions(-) create mode 100644 src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx new file mode 100644 index 0000000000000..536e6d8bd806d --- /dev/null +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; +import { cloneDeep, omit } from 'lodash'; + +import { RenderResult, act, render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { gridLayoutStateManagerMock } from '../test_utils/mocks'; +import { getSampleLayout } from '../test_utils/sample_layout'; +import { GridLayoutStateManager } from '../types'; +import { GridRowHeader, GridRowHeaderProps } from './grid_row_header'; + +const toggleIsCollapsed = jest + .fn() + .mockImplementation((rowIndex: number, gridLayoutStateManager: GridLayoutStateManager) => { + const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value); + newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed; + gridLayoutStateManager.gridLayout$.next(newLayout); + }); + +describe('GridRowHeader', () => { + const renderGridRowHeader = (propsOverrides: Partial = {}) => { + const gridLayoutStateManager = + propsOverrides.gridLayoutStateManager ?? gridLayoutStateManagerMock; + return render( + toggleIsCollapsed(0, gridLayoutStateManager)} + gridLayoutStateManager={gridLayoutStateManager} + {...omit(propsOverrides, 'gridLayoutStateManager')} + /> + ); + }; + + it('renders the panel count', async () => { + const gridLayoutStateManager = gridLayoutStateManagerMock; + const component = renderGridRowHeader({ gridLayoutStateManager }); + const initialCount = component.getByTestId('kbnGridRowHeader--panelCount'); + expect(initialCount.textContent).toBe('(8 panels)'); + + act(() => { + gridLayoutStateManager.gridLayout$.next([ + { + title: 'Large section', + isCollapsed: false, + panels: { + panel1: { + id: 'panel1', + row: 0, + column: 0, + width: 12, + height: 6, + }, + }, + }, + ]); + }); + + await waitFor(() => { + const updatedCount = component.getByTestId('kbnGridRowHeader--panelCount'); + expect(updatedCount.textContent).toBe('(1 panel)'); + }); + }); + + describe('title editor', () => { + const gridLayoutStateManager = gridLayoutStateManagerMock; + + afterEach(() => { + act(() => { + gridLayoutStateManager.gridLayout$.next(getSampleLayout()); + }); + }); + + const setTitle = async (component: RenderResult) => { + const input = component.getByTestId('euiInlineEditModeInput'); + expect(input.getAttribute('value')).toBe('Large section'); + await userEvent.click(input); + await userEvent.keyboard(' 123'); + expect(input.getAttribute('value')).toBe('Large section 123'); + }; + + it('clicking on edit icon triggers inline title editor', async () => { + const component = renderGridRowHeader({ gridLayoutStateManager }); + const editIcon = component.getByTestId('kbnGridRowTitle--edit'); + + expect(component.queryByTestId('kbnGridRowTitle--editor')).not.toBeInTheDocument(); + await userEvent.click(editIcon); + expect(component.getByTestId('kbnGridRowTitle--editor')).toBeInTheDocument(); + }); + + it('can update the title', async () => { + const component = renderGridRowHeader({ gridLayoutStateManager }); + expect(component.getByTestId('kbnGridRowTitle').textContent).toBe('Large section'); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section'); + + const editIcon = component.getByTestId('kbnGridRowTitle--edit'); + await userEvent.click(editIcon); + await setTitle(component); + const saveButton = component.getByTestId('euiInlineEditModeSaveButton'); + await userEvent.click(saveButton); + + expect(component.queryByTestId('kbnGridRowTitle--editor')).not.toBeInTheDocument(); + expect(component.getByTestId('kbnGridRowTitle').textContent).toBe('Large section 123'); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section 123'); + }); + + it('clicking on cancel closes the inline title editor without updating title', async () => { + const component = renderGridRowHeader({ gridLayoutStateManager }); + const editIcon = component.getByTestId('kbnGridRowTitle--edit'); + await userEvent.click(editIcon); + + await setTitle(component); + const cancelButton = component.getByTestId('euiInlineEditModeCancelButton'); + await userEvent.click(cancelButton); + + expect(component.queryByTestId('kbnGridRowTitle--editor')).not.toBeInTheDocument(); + expect(component.getByTestId('kbnGridRowTitle').textContent).toBe('Large section'); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section'); + }); + }); +}); 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 2f367f1707f2d..4125b7a91f372 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 @@ -6,8 +6,8 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useEffect, useState } from 'react'; -import { distinctUntilChanged } from 'rxjs'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { distinctUntilChanged, map } from 'rxjs'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; @@ -19,16 +19,14 @@ import { DeleteGridRowModal } from './delete_grid_row_modal'; import { GridRowTitle } from './grid_row_title'; import { useGridRowHeaderStyles } from './use_grid_row_header_styles'; +export interface GridRowHeaderProps { + rowIndex: number; + gridLayoutStateManager: GridLayoutStateManager; + toggleIsCollapsed: () => void; +} + export const GridRowHeader = React.memo( - ({ - rowIndex, - gridLayoutStateManager, - toggleIsCollapsed, - }: { - rowIndex: number; - gridLayoutStateManager: GridLayoutStateManager; - toggleIsCollapsed: () => void; - }) => { + ({ rowIndex, gridLayoutStateManager, toggleIsCollapsed }: GridRowHeaderProps) => { const headerStyles = useGridRowHeaderStyles(); const [editTitleOpen, setEditTitleOpen] = useState(false); @@ -36,6 +34,9 @@ export const GridRowHeader = React.memo( const [readOnly, setReadOnly] = useState( gridLayoutStateManager.accessMode$.getValue() === 'VIEW' ); + const [panelCount, setPanelCount] = useState( + Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels).length + ); useEffect(() => { /** @@ -48,10 +49,40 @@ export const GridRowHeader = React.memo( setReadOnly(accessMode === 'VIEW'); }); + /** + * This subscription is responsible for updating the panel count as the grid layout + * gets updated so that the (X panels) label updates as expected + */ + const panelCountSubscription = gridLayoutStateManager.gridLayout$ + .pipe( + map((layout) => Object.keys(layout[rowIndex].panels).length), + distinctUntilChanged() + ) + .subscribe((count) => { + setPanelCount(count); + }); + return () => { accessModeSubscription.unsubscribe(); + panelCountSubscription.unsubscribe(); }; - }, [gridLayoutStateManager]); + }, [gridLayoutStateManager, rowIndex]); + + const confirmDeleteRow = useCallback(() => { + /** + * memoization of this callback does not need to be dependant on the React panel count + * state, so just grab the panel count via gridLayoutStateManager instead + */ + const count = Object.keys( + gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels + ).length; + if (!Boolean(count)) { + const newLayout = deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowIndex); + gridLayoutStateManager.gridLayout$.next(newLayout); + } else { + setDeleteModalVisible(true); + } + }, [gridLayoutStateManager.gridLayout$, rowIndex]); return ( <> @@ -88,15 +119,15 @@ export const GridRowHeader = React.memo( !editTitleOpen && ( <> - {`(${ - /** - * We can get away with grabbing the panel count without React state because this count - * is only rendered when the section is collapsed, and the count can only be updated when - * the section isn't collapsed - */ - Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels) - .length - } panels)`} + + {i18n.translate('kbnGridLayout.rowHeader.panelCount', { + defaultMessage: + '({panelCount} {panelCount, plural, one {panel} other {panels}})', + values: { + panelCount, + }, + })} + {!readOnly && ( <> @@ -105,20 +136,10 @@ export const GridRowHeader = React.memo( iconType="trash" color="danger" className="kbnGridLayout--deleteRowIcon" - onClick={() => { - const panelCount = Object.keys( - gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels - ).length; - if (!Boolean(panelCount)) { - const newLayout = deleteRow( - gridLayoutStateManager.gridLayout$.getValue(), - rowIndex - ); - gridLayoutStateManager.gridLayout$.next(newLayout); - } else { - setDeleteModalVisible(true); - } - }} + onClick={confirmDeleteRow} + aria-label={i18n.translate('kbnGridLayout.row.deleteRow', { + defaultMessage: 'Delete section', + })} /> @@ -132,6 +153,9 @@ export const GridRowHeader = React.memo( iconType="move" color="text" className="kbnGridLayout--moveRowIcon" + aria-label={i18n.translate('kbnGridLayout.row.moveRow', { + defaultMessage: 'Move section', + })} /> */} diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 99c0ac54bbccb..156a0b3d29e24 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -77,13 +77,14 @@ export const GridRowTitle = React.memo( inputAriaLabel={i18n.translate('kbnGridLayout.row.editTitleAriaLabel', { defaultMessage: 'Edit section title', })} + data-test-subj="kbnGridRowTitle--editor" /> ) : ( <> - +

{rowTitle}

@@ -94,6 +95,10 @@ export const GridRowTitle = React.memo( iconType="pencil" onClick={() => setEditTitleOpen(true)} color="text" + aria-label={i18n.translate('kbnGridLayout.row.editRowTitle', { + defaultMessage: 'Edit section title', + })} + data-test-subj="kbnGridRowTitle--edit" />
)} From 4a8903993430b696b562f0b3d7db6c28221afd09 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 5 Feb 2025 15:17:18 -0700 Subject: [PATCH 23/42] Add more tests --- .../grid/grid_row/grid_row_header.test.tsx | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx index 536e6d8bd806d..3bae5ccab0958 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx @@ -27,37 +27,35 @@ const toggleIsCollapsed = jest describe('GridRowHeader', () => { const renderGridRowHeader = (propsOverrides: Partial = {}) => { - const gridLayoutStateManager = - propsOverrides.gridLayoutStateManager ?? gridLayoutStateManagerMock; return render( toggleIsCollapsed(0, gridLayoutStateManager)} - gridLayoutStateManager={gridLayoutStateManager} - {...omit(propsOverrides, 'gridLayoutStateManager')} + toggleIsCollapsed={() => toggleIsCollapsed(0, gridLayoutStateManagerMock)} + gridLayoutStateManager={gridLayoutStateManagerMock} + {...propsOverrides} /> ); }; + beforeEach(() => { + toggleIsCollapsed.mockClear(); + act(() => { + gridLayoutStateManagerMock.gridLayout$.next(getSampleLayout()); + }); + }); + it('renders the panel count', async () => { - const gridLayoutStateManager = gridLayoutStateManagerMock; - const component = renderGridRowHeader({ gridLayoutStateManager }); + const component = renderGridRowHeader(); const initialCount = component.getByTestId('kbnGridRowHeader--panelCount'); expect(initialCount.textContent).toBe('(8 panels)'); act(() => { - gridLayoutStateManager.gridLayout$.next([ + const currentRow = gridLayoutStateManagerMock.gridLayout$.getValue()[0]; + gridLayoutStateManagerMock.gridLayout$.next([ { - title: 'Large section', - isCollapsed: false, + ...currentRow, panels: { - panel1: { - id: 'panel1', - row: 0, - column: 0, - width: 12, - height: 6, - }, + panel1: currentRow.panels.panel1, }, }, ]); @@ -69,15 +67,18 @@ describe('GridRowHeader', () => { }); }); - describe('title editor', () => { - const gridLayoutStateManager = gridLayoutStateManagerMock; + it('clicking title calls `toggleIsCollapsed`', async () => { + const component = renderGridRowHeader(); + const title = component.getByTestId('kbnGridRowTitle'); - afterEach(() => { - act(() => { - gridLayoutStateManager.gridLayout$.next(getSampleLayout()); - }); - }); + expect(toggleIsCollapsed).toBeCalledTimes(0); + expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(false); + await userEvent.click(title); + expect(toggleIsCollapsed).toBeCalledTimes(1); + expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(true); + }); + describe('title editor', () => { const setTitle = async (component: RenderResult) => { const input = component.getByTestId('euiInlineEditModeInput'); expect(input.getAttribute('value')).toBe('Large section'); @@ -86,19 +87,22 @@ describe('GridRowHeader', () => { expect(input.getAttribute('value')).toBe('Large section 123'); }; - it('clicking on edit icon triggers inline title editor', async () => { - const component = renderGridRowHeader({ gridLayoutStateManager }); + it('clicking on edit icon triggers inline title editor and does not toggle collapsed', async () => { + const component = renderGridRowHeader(); const editIcon = component.getByTestId('kbnGridRowTitle--edit'); expect(component.queryByTestId('kbnGridRowTitle--editor')).not.toBeInTheDocument(); + expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(false); await userEvent.click(editIcon); expect(component.getByTestId('kbnGridRowTitle--editor')).toBeInTheDocument(); + expect(toggleIsCollapsed).toBeCalledTimes(0); + expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(false); }); it('can update the title', async () => { - const component = renderGridRowHeader({ gridLayoutStateManager }); + const component = renderGridRowHeader(); expect(component.getByTestId('kbnGridRowTitle').textContent).toBe('Large section'); - expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section'); + expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].title).toBe('Large section'); const editIcon = component.getByTestId('kbnGridRowTitle--edit'); await userEvent.click(editIcon); @@ -108,11 +112,11 @@ describe('GridRowHeader', () => { expect(component.queryByTestId('kbnGridRowTitle--editor')).not.toBeInTheDocument(); expect(component.getByTestId('kbnGridRowTitle').textContent).toBe('Large section 123'); - expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section 123'); + expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].title).toBe('Large section 123'); }); it('clicking on cancel closes the inline title editor without updating title', async () => { - const component = renderGridRowHeader({ gridLayoutStateManager }); + const component = renderGridRowHeader(); const editIcon = component.getByTestId('kbnGridRowTitle--edit'); await userEvent.click(editIcon); @@ -122,7 +126,7 @@ describe('GridRowHeader', () => { expect(component.queryByTestId('kbnGridRowTitle--editor')).not.toBeInTheDocument(); expect(component.getByTestId('kbnGridRowTitle').textContent).toBe('Large section'); - expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section'); + expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].title).toBe('Large section'); }); }); }); From 31678768f75d0952ad7d1dfb5bc87409b9e85641 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 5 Feb 2025 22:43:14 +0000 Subject: [PATCH 24/42] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx | 2 +- .../private/kbn-grid-layout/grid/grid_row/grid_row_header.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx index 3bae5ccab0958..afea47392af85 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ import React from 'react'; -import { cloneDeep, omit } from 'lodash'; +import { cloneDeep } from 'lodash'; import { RenderResult, act, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; 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 4125b7a91f372..ff8394eba86fc 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 @@ -6,7 +6,7 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { distinctUntilChanged, map } from 'rxjs'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; From 0101879d4f6c6f662b322ff69d6413ac3f8bc1ac Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 12 Feb 2025 08:11:57 -0700 Subject: [PATCH 25/42] Small cleanups --- .../packages/private/kbn-grid-layout/grid/grid_layout.tsx | 3 +++ .../private/kbn-grid-layout/grid/grid_row/grid_row.tsx | 3 +-- .../kbn-grid-layout/grid/grid_row/grid_row_header.tsx | 5 ++--- .../private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) 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..8ac08c9aa86d7 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 @@ -201,6 +201,9 @@ const expandedPanelStyles = css` // targets the grid row container that contains the expanded panel .kbnGridRowHeader { height: 0px; // used instead of 'display: none' due to a11y concerns + padding: 0px; + display: block; + overflow: hidden; } .kbnGridRow { display: block !important; // overwrite grid display diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 719d8f0665ed2..5d66e9e28ae74 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -35,7 +35,6 @@ export const GridRow = ({ gridLayoutStateManager, }: GridRowProps) => { const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex]; - const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed); const [panelIds, setPanelIds] = useState(Object.keys(currentRow.panels)); const [panelIdsInOrder, setPanelIdsInOrder] = useState(() => @@ -127,8 +126,8 @@ export const GridRow = ({ return () => { interactionStyleSubscription.unsubscribe(); - gridLayoutSubscription.unsubscribe(); rowStateSubscription.unsubscribe(); + gridLayoutSubscription.unsubscribe(); }; }, // eslint-disable-next-line react-hooks/exhaustive-deps 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 ff8394eba86fc..6114f081ab0d4 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 @@ -50,8 +50,7 @@ export const GridRowHeader = React.memo( }); /** - * This subscription is responsible for updating the panel count as the grid layout - * gets updated so that the (X panels) label updates as expected + * This subscription is responsible for keeping the panel count in sync */ const panelCountSubscription = gridLayoutStateManager.gridLayout$ .pipe( @@ -70,7 +69,7 @@ export const GridRowHeader = React.memo( const confirmDeleteRow = useCallback(() => { /** - * memoization of this callback does not need to be dependant on the React panel count + * Memoization of this callback does not need to be dependant on the React panel count * state, so just grab the panel count via gridLayoutStateManager instead */ const count = Object.keys( diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 156a0b3d29e24..0b4966a39bb0d 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -83,7 +83,7 @@ export const GridRowTitle = React.memo( ) : ( <> - +

{rowTitle}

From 4237ef6a9fedb22882fbdcfc865889b272ed6ed0 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 12 Feb 2025 08:58:25 -0700 Subject: [PATCH 26/42] Move row count variable subscription --- .../grid/grid_row/grid_row.tsx | 20 ++++--------------- .../grid/use_grid_layout_state.ts | 3 ++- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 16537b4cf0fba..28b2ad399b3ec 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -9,7 +9,7 @@ import { cloneDeep } from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; -import { combineLatest, distinctUntilChanged, map, pairwise, skip } from 'rxjs'; +import { combineLatest, map, pairwise, skip } from 'rxjs'; import { css } from '@emotion/react'; @@ -110,22 +110,10 @@ export const GridRow = React.memo( } ); - const columnCountSubscription = gridLayoutStateManager.runtimeSettings$ - .pipe( - map(({ columnCount }) => columnCount), - distinctUntilChanged() - ) - .subscribe((columnCount) => { - const rowRef = gridLayoutStateManager.rowRefs.current[rowIndex]; - if (!rowRef) return; - rowRef.style.setProperty('--kbnGridRowColumnCount', `${columnCount}`); - }); - return () => { interactionStyleSubscription.unsubscribe(); gridLayoutSubscription.unsubscribe(); rowStateSubscription.unsubscribe(); - columnCountSubscription.unsubscribe(); }; }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -190,10 +178,10 @@ const styles = { gap: 'calc(var(--kbnGridGutterSize) * 1px)', gridAutoRows: 'calc(var(--kbnGridRowHeight) * 1px)', gridTemplateColumns: `repeat( - var(--kbnGridRowColumnCount), + var(--kbnGridColumnCount), calc( - (100% - (var(--kbnGridGutterSize) * (var(--kbnGridRowColumnCount) - 1) * 1px)) / - var(--kbnGridRowColumnCount) + (100% - (var(--kbnGridGutterSize) * (var(--kbnGridColumnCount) - 1) * 1px)) / + var(--kbnGridColumnCount) ) )`, }), diff --git a/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_state.ts b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_state.ts index 0294c82590d27..3bdd80ba1a1a3 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_state.ts +++ b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_state.ts @@ -146,11 +146,12 @@ export const useGridLayoutState = ({ */ const cssVariableSubscription = gridLayoutStateManager.runtimeSettings$ .pipe(distinctUntilChanged(deepEqual)) - .subscribe(({ gutterSize, columnPixelWidth, rowHeight }) => { + .subscribe(({ gutterSize, columnPixelWidth, rowHeight, columnCount }) => { if (!layoutRef.current) return; layoutRef.current.style.setProperty('--kbnGridGutterSize', `${gutterSize}`); layoutRef.current.style.setProperty('--kbnGridRowHeight', `${rowHeight}`); layoutRef.current.style.setProperty('--kbnGridColumnWidth', `${columnPixelWidth}`); + layoutRef.current.style.setProperty('--kbnGridColumnCount', `${columnCount}`); }); return () => { From a2cd88271b5ece48ddf8df6aad70933a6fcc30a9 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 12 Feb 2025 10:51:58 -0700 Subject: [PATCH 27/42] Add missed `i18n` + small cleanup --- .../kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx | 6 +++++- .../private/kbn-grid-layout/grid/grid_row/grid_row.tsx | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx index 6a88fd3049ca6..988d16356f5c9 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx @@ -38,7 +38,11 @@ export const DeleteGridRowModal = ({ }} > - Delete section + + {i18n.translate('kbnGridLayout.deleteGridRowModal.title', { + defaultMessage: 'Delete section', + })} + {i18n.translate('kbnGridLayout.deleteGridRowModal.body', { diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 28b2ad399b3ec..ee68438719404 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -128,7 +128,6 @@ export const GridRow = React.memo( return (
Date: Wed, 12 Feb 2025 14:28:06 -0700 Subject: [PATCH 28/42] Add `a11y` to sections --- .../kbn-grid-layout/grid/grid_row/grid_row.tsx | 15 ++++++++++++++- .../grid/grid_row/grid_row_header.tsx | 7 +++++-- .../grid/grid_row/grid_row_title.tsx | 15 ++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index ee68438719404..cb229420b0617 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -8,7 +8,7 @@ */ import { cloneDeep } from 'lodash'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { combineLatest, map, pairwise, skip } from 'rxjs'; import { css } from '@emotion/react'; @@ -31,6 +31,7 @@ export interface GridRowProps { export const GridRow = React.memo( ({ rowIndex, renderPanelContents, gridLayoutStateManager }: GridRowProps) => { + const headerRef = useRef(null); const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex]; const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed); @@ -126,6 +127,14 @@ export const GridRow = React.memo( gridLayoutStateManager.gridLayout$.next(newLayout); }, [rowIndex, gridLayoutStateManager.gridLayout$]); + useEffect(() => { + /** + * Set `aria-expanded` without passing as prop to `gridRowHeader` to prevent re-render + */ + if (!headerRef.current) return; + headerRef.current.ariaExpanded = `${!isCollapsed}`; + }, [isCollapsed]); + return (
)} {!isCollapsed && (
(gridLayoutStateManager.rowRefs.current[rowIndex] = element) } css={[styles.fullHeight, styles.grid]} + role="region" + aria-labelledby={`kbnGridRowHeader--${rowIndex}`} > {/* render the panels **in order** for accessibility, using the memoized panel components */} {panelIdsInOrder.map((panelId) => ( 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 6114f081ab0d4..4b9a387339297 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 @@ -23,12 +23,12 @@ export interface GridRowHeaderProps { rowIndex: number; gridLayoutStateManager: GridLayoutStateManager; toggleIsCollapsed: () => void; + headerRef: React.MutableRefObject; } export const GridRowHeader = React.memo( - ({ rowIndex, gridLayoutStateManager, toggleIsCollapsed }: GridRowHeaderProps) => { + ({ rowIndex, gridLayoutStateManager, toggleIsCollapsed, headerRef }: GridRowHeaderProps) => { const headerStyles = useGridRowHeaderStyles(); - const [editTitleOpen, setEditTitleOpen] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [readOnly, setReadOnly] = useState( @@ -86,10 +86,13 @@ export const GridRowHeader = React.memo( return ( <> void; gridLayoutStateManager: GridLayoutStateManager; }) => { + const inputRef = useRef(null); const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; const [rowTitle, setRowTitle] = useState(currentRow.title); @@ -52,6 +53,15 @@ export const GridRowTitle = React.memo( }; }, [rowIndex, gridLayoutStateManager]); + useEffect(() => { + /** + * Set focus on title input when edit mode is open + */ + if (editTitleOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [editTitleOpen]); + const updateTitle = useCallback( (title: string) => { const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.getValue()); @@ -74,6 +84,9 @@ export const GridRowTitle = React.memo( onSave={updateTitle} onCancel={() => setEditTitleOpen(false)} startWithEditOpen + editModeProps={{ + inputProps: { inputRef }, + }} inputAriaLabel={i18n.translate('kbnGridLayout.row.editTitleAriaLabel', { defaultMessage: 'Edit section title', })} From 401cba489224f277df560e39d789d5993bf13089 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 12 Feb 2025 14:30:47 -0700 Subject: [PATCH 29/42] Add `a11y` test --- .../private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx | 2 ++ .../private/kbn-grid-layout/grid/grid_row/grid_row_header.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx index 0605fd527a091..3ad3acbaad8ab 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx @@ -36,11 +36,13 @@ describe('GridRow', () => { it('does not show the panels in a row that is collapsed', async () => { renderGridRow({ rowIndex: 1 }); + expect(screen.getByTestId('kbnGridRowHeader--1').ariaExpanded).toBe('true'); expect(screen.getAllByText(/panel content/)).toHaveLength(1); const collapseButton = screen.getByRole('button', { name: /toggle collapse/i }); await userEvent.click(collapseButton); + expect(screen.getByTestId('kbnGridRowHeader--1').ariaExpanded).toBe('false'); expect(screen.queryAllByText(/panel content/)).toHaveLength(0); }); }); 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 4b9a387339297..4126ed50bc836 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 @@ -93,6 +93,7 @@ export const GridRowHeader = React.memo( className="kbnGridRowHeader" id={`kbnGridRowHeader--${rowIndex}`} aria-controls={`kbnGridRow--${rowIndex}`} + data-test-subj={`kbnGridRowHeader--${rowIndex}`} > Date: Wed, 12 Feb 2025 16:07:21 -0700 Subject: [PATCH 30/42] Fix Axe failures --- .../grid/grid_row/grid_row.tsx | 8 +-- .../grid/grid_row/grid_row_header.tsx | 31 +++------- .../grid/grid_row/grid_row_title.tsx | 59 ++++++++++++++++--- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index cb229420b0617..60dff4f7fc265 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -31,7 +31,7 @@ export interface GridRowProps { export const GridRow = React.memo( ({ rowIndex, renderPanelContents, gridLayoutStateManager }: GridRowProps) => { - const headerRef = useRef(null); + const collapseButtonRef = useRef(null); const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex]; const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed); @@ -131,8 +131,8 @@ export const GridRow = React.memo( /** * Set `aria-expanded` without passing as prop to `gridRowHeader` to prevent re-render */ - if (!headerRef.current) return; - headerRef.current.ariaExpanded = `${!isCollapsed}`; + if (!collapseButtonRef.current) return; + collapseButtonRef.current.ariaExpanded = `${!isCollapsed}`; }, [isCollapsed]); return ( @@ -147,7 +147,7 @@ export const GridRow = React.memo( rowIndex={rowIndex} gridLayoutStateManager={gridLayoutStateManager} toggleIsCollapsed={toggleIsCollapsed} - headerRef={headerRef} + collapseButtonRef={collapseButtonRef} /> )} {!isCollapsed && ( 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 4126ed50bc836..70f971644892e 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 @@ -23,11 +23,16 @@ export interface GridRowHeaderProps { rowIndex: number; gridLayoutStateManager: GridLayoutStateManager; toggleIsCollapsed: () => void; - headerRef: React.MutableRefObject; + collapseButtonRef: React.MutableRefObject; } export const GridRowHeader = React.memo( - ({ rowIndex, gridLayoutStateManager, toggleIsCollapsed, headerRef }: GridRowHeaderProps) => { + ({ + rowIndex, + gridLayoutStateManager, + toggleIsCollapsed, + collapseButtonRef, + }: GridRowHeaderProps) => { const headerStyles = useGridRowHeaderStyles(); const [editTitleOpen, setEditTitleOpen] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); @@ -86,26 +91,11 @@ export const GridRowHeader = React.memo( return ( <> - - - { /** @@ -180,12 +171,6 @@ export const GridRowHeader = React.memo( ); const styles = { - accordianArrow: css({ - transform: 'rotate(0deg)', - '.kbnGridRowContainer--collapsed &': { - transform: 'rotate(-90deg) !important', - }, - }), hiddenOnCollapsed: css({ display: 'none', '.kbnGridRowContainer--collapsed &': { diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 21362ac3495e1..00723301f2db7 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -10,9 +10,17 @@ import { cloneDeep } from 'lodash'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { distinctUntilChanged, map } from 'rxjs'; -import { EuiButtonIcon, EuiFlexItem, EuiInlineEditTitle, EuiLink, EuiTitle } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexItem, + EuiInlineEditTitle, + EuiTitle, + UseEuiTheme, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; import { GridLayoutStateManager } from '../types'; export const GridRowTitle = React.memo( @@ -23,6 +31,7 @@ export const GridRowTitle = React.memo( setEditTitleOpen, toggleIsCollapsed, gridLayoutStateManager, + collapseButtonRef, }: { readOnly: boolean; rowIndex: number; @@ -30,6 +39,7 @@ export const GridRowTitle = React.memo( setEditTitleOpen: (value: boolean) => void; toggleIsCollapsed: () => void; gridLayoutStateManager: GridLayoutStateManager; + collapseButtonRef: React.MutableRefObject; }) => { const inputRef = useRef(null); const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; @@ -74,6 +84,29 @@ export const GridRowTitle = React.memo( return ( <> + + + {editTitleOpen ? null : ( + +

{rowTitle}

+
+ )} +
+
{!readOnly && editTitleOpen ? ( {/* @ts-ignore - EUI typing issue that will be resolved with https://github.com/elastic/eui/pull/8307 */} @@ -91,17 +124,11 @@ export const GridRowTitle = React.memo( defaultMessage: 'Edit section title', })} data-test-subj="kbnGridRowTitle--editor" + css={nudgeInputStyles} /> ) : ( <> - - - -

{rowTitle}

-
-
-
{!readOnly && ( + css({ + svg: { + transition: `transform ${euiTheme.animation.fast} ease`, + transform: 'rotate(0deg)', + '.kbnGridRowContainer--collapsed &': { + transform: 'rotate(-90deg) !important', + }, + }, + }); + +const nudgeInputStyles = ({ euiTheme }: UseEuiTheme) => + css({ + marginLeft: `calc(${euiTheme.size.xxs} * -3)`, // 6px + }); + GridRowTitle.displayName = 'GridRowTitle'; From 5dab04a7f021451b7db770df34db06e0b9a98fb5 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 12 Feb 2025 16:11:34 -0700 Subject: [PATCH 31/42] Hide background color on focus --- .../kbn-grid-layout/grid/grid_row/grid_row_title.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 00723301f2db7..adf6ed37ddb75 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -93,7 +93,7 @@ export const GridRowTitle = React.memo( })} iconType={'arrowDown'} onClick={toggleIsCollapsed} - css={rotateAccordianArrowStyles} + css={accordianButtonStyles} size="m" id={`kbnGridRowHeader--${rowIndex}`} aria-controls={`kbnGridRow--${rowIndex}`} @@ -149,8 +149,11 @@ export const GridRowTitle = React.memo( } ); -const rotateAccordianArrowStyles = ({ euiTheme }: UseEuiTheme) => +const accordianButtonStyles = ({ euiTheme }: UseEuiTheme) => css({ + '&:focus': { + backgroundColor: 'unset', + }, svg: { transition: `transform ${euiTheme.animation.fast} ease`, transform: 'rotate(0deg)', From 42343a7a82bfcffdc718139a08c7c2da0db6a703 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 12 Feb 2025 17:06:56 -0700 Subject: [PATCH 32/42] Fix test types --- .../kbn-grid-layout/grid/grid_row/grid_row.tsx | 3 ++- .../grid/grid_row/grid_row_header.test.tsx | 18 +++++++++++------- .../grid/grid_row/grid_row_title.tsx | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 60dff4f7fc265..092f2cdcefa68 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -129,7 +129,8 @@ export const GridRow = React.memo( useEffect(() => { /** - * Set `aria-expanded` without passing as prop to `gridRowHeader` to prevent re-render + * Set `aria-expanded` without passing the expanded state as a prop to `GridRowHeader` in order + * to prevent `GridRowHeader` from rerendering when this state changes */ if (!collapseButtonRef.current) return; collapseButtonRef.current.ariaExpanded = `${!isCollapsed}`; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx index afea47392af85..e4ca76d8e90c8 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx @@ -6,9 +6,10 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; import { cloneDeep } from 'lodash'; +import React from 'react'; +import { EuiThemeProvider } from '@elastic/eui'; import { RenderResult, act, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -28,12 +29,15 @@ const toggleIsCollapsed = jest describe('GridRowHeader', () => { const renderGridRowHeader = (propsOverrides: Partial = {}) => { return render( - toggleIsCollapsed(0, gridLayoutStateManagerMock)} - gridLayoutStateManager={gridLayoutStateManagerMock} - {...propsOverrides} - /> + + toggleIsCollapsed(0, gridLayoutStateManagerMock)} + gridLayoutStateManager={gridLayoutStateManagerMock} + collapseButtonRef={React.createRef()} + {...propsOverrides} + /> + ); }; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index adf6ed37ddb75..c1fb8d70b4387 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -97,7 +97,7 @@ export const GridRowTitle = React.memo( size="m" id={`kbnGridRowHeader--${rowIndex}`} aria-controls={`kbnGridRow--${rowIndex}`} - data-test-subj={`kbnGridRowHeader--${rowIndex}`} + data-test-subj={`kbnGridRowTitle`} textProps={false} > {editTitleOpen ? null : ( From 3952b2f582e1d95a3d702b6638f196adaa38c0a4 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 12 Feb 2025 17:12:09 -0700 Subject: [PATCH 33/42] Small styling fix --- .../kbn-grid-layout/grid/grid_row/grid_row_title.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index c1fb8d70b4387..f4db3c6863b86 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -99,6 +99,7 @@ export const GridRowTitle = React.memo( aria-controls={`kbnGridRow--${rowIndex}`} data-test-subj={`kbnGridRowTitle`} textProps={false} + flush="both" > {editTitleOpen ? null : ( @@ -124,7 +125,6 @@ export const GridRowTitle = React.memo( defaultMessage: 'Edit section title', })} data-test-subj="kbnGridRowTitle--editor" - css={nudgeInputStyles} /> ) : ( @@ -163,9 +163,4 @@ const accordianButtonStyles = ({ euiTheme }: UseEuiTheme) => }, }); -const nudgeInputStyles = ({ euiTheme }: UseEuiTheme) => - css({ - marginLeft: `calc(${euiTheme.size.xxs} * -3)`, // 6px - }); - GridRowTitle.displayName = 'GridRowTitle'; From 226dbff74117a9865d713695f2a7962ac76b8571 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 13 Feb 2025 09:10:04 -0700 Subject: [PATCH 34/42] Fix failing tests --- .../kbn-grid-layout/grid/grid_layout.test.tsx | 13 +++++++++-- .../grid/grid_row/grid_row.test.tsx | 23 +++++++++++-------- .../grid/grid_row/grid_row.tsx | 2 +- .../grid/grid_row/grid_row_header.tsx | 1 + .../grid/grid_row/grid_row_title.tsx | 4 ++-- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx index 240870bd813f2..66f836c57b0fb 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx @@ -21,6 +21,7 @@ import { touchMoveTo, touchStart, } from './test_utils/events'; +import { EuiThemeProvider } from '@elastic/eui'; const onLayoutChange = jest.fn(); @@ -33,12 +34,20 @@ const renderGridLayout = (propsOverrides: Partial = {}) => { onLayoutChange, }; - const { rerender, ...rtlRest } = render(); + const { rerender, ...rtlRest } = render( + + + + ); return { ...rtlRest, rerender: (overrides: Partial) => - rerender(), + rerender( + + + + ), }; }; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx index 3ad3acbaad8ab..72c593325b57e 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx @@ -6,22 +6,25 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; +import { EuiThemeProvider } from '@elastic/eui'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { GridRow, GridRowProps } from './grid_row'; +import React from 'react'; import { gridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks'; import { getSampleLayout } from '../test_utils/sample_layout'; +import { GridRow, GridRowProps } from './grid_row'; describe('GridRow', () => { const renderGridRow = (propsOverrides: Partial = {}) => { return render( - + + + ); }; @@ -36,13 +39,13 @@ describe('GridRow', () => { it('does not show the panels in a row that is collapsed', async () => { renderGridRow({ rowIndex: 1 }); - expect(screen.getByTestId('kbnGridRowHeader--1').ariaExpanded).toBe('true'); + expect(screen.getByTestId('kbnGridRowTitle--1').ariaExpanded).toBe('true'); expect(screen.getAllByText(/panel content/)).toHaveLength(1); const collapseButton = screen.getByRole('button', { name: /toggle collapse/i }); await userEvent.click(collapseButton); - expect(screen.getByTestId('kbnGridRowHeader--1').ariaExpanded).toBe('false'); + expect(screen.getByTestId('kbnGridRowTitle--1').ariaExpanded).toBe('false'); expect(screen.queryAllByText(/panel content/)).toHaveLength(0); }); }); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 092f2cdcefa68..44e09ee7d2b38 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -160,7 +160,7 @@ export const GridRow = React.memo( } css={[styles.fullHeight, styles.grid]} role="region" - aria-labelledby={`kbnGridRowHeader--${rowIndex}`} + aria-labelledby={`kbnGridRowTile--${rowIndex}`} > {/* render the panels **in order** for accessibility, using the memoized panel components */} {panelIdsInOrder.map((panelId) => ( 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 70f971644892e..00dfe6895a4b2 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 @@ -95,6 +95,7 @@ export const GridRowHeader = React.memo( alignItems="center" css={headerStyles} className="kbnGridRowHeader" + data-test-subj={`kbnGridRowHeader--${rowIndex}`} > From 41c639c265ee2d73b16f55ca4c998efd5211fb99 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 18 Feb 2025 09:26:25 -0700 Subject: [PATCH 35/42] Fix tests --- .../grid/grid_row/grid_row.test.tsx | 4 +-- .../grid/grid_row/grid_row.tsx | 4 +-- .../grid/grid_row/grid_row_header.test.tsx | 26 +++++++++---------- .../grid/grid_row/grid_row_header.tsx | 8 ++++-- .../grid/grid_row/grid_row_title.tsx | 10 +++---- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx index 72c593325b57e..e5cc9b2759499 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx @@ -39,13 +39,13 @@ describe('GridRow', () => { it('does not show the panels in a row that is collapsed', async () => { renderGridRow({ rowIndex: 1 }); - expect(screen.getByTestId('kbnGridRowTitle--1').ariaExpanded).toBe('true'); + expect(screen.getByTestId('kbnGridRowTitle-1').ariaExpanded).toBe('true'); expect(screen.getAllByText(/panel content/)).toHaveLength(1); const collapseButton = screen.getByRole('button', { name: /toggle collapse/i }); await userEvent.click(collapseButton); - expect(screen.getByTestId('kbnGridRowTitle--1').ariaExpanded).toBe('false'); + expect(screen.getByTestId('kbnGridRowTitle-1').ariaExpanded).toBe('false'); expect(screen.queryAllByText(/panel content/)).toHaveLength(0); }); }); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 44e09ee7d2b38..4a4469543f19c 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -153,14 +153,14 @@ export const GridRow = React.memo( )} {!isCollapsed && (
(gridLayoutStateManager.rowRefs.current[rowIndex] = element) } css={[styles.fullHeight, styles.grid]} role="region" - aria-labelledby={`kbnGridRowTile--${rowIndex}`} + aria-labelledby={`kbnGridRowTile-${rowIndex}`} > {/* render the panels **in order** for accessibility, using the memoized panel components */} {panelIdsInOrder.map((panelId) => ( diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx index e4ca76d8e90c8..170a10bbaf7a7 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx @@ -50,7 +50,7 @@ describe('GridRowHeader', () => { it('renders the panel count', async () => { const component = renderGridRowHeader(); - const initialCount = component.getByTestId('kbnGridRowHeader--panelCount'); + const initialCount = component.getByTestId('kbnGridRowHeader-0--panelCount'); expect(initialCount.textContent).toBe('(8 panels)'); act(() => { @@ -66,14 +66,14 @@ describe('GridRowHeader', () => { }); await waitFor(() => { - const updatedCount = component.getByTestId('kbnGridRowHeader--panelCount'); + const updatedCount = component.getByTestId('kbnGridRowHeader-0--panelCount'); expect(updatedCount.textContent).toBe('(1 panel)'); }); }); it('clicking title calls `toggleIsCollapsed`', async () => { const component = renderGridRowHeader(); - const title = component.getByTestId('kbnGridRowTitle'); + const title = component.getByTestId('kbnGridRowTitle-0'); expect(toggleIsCollapsed).toBeCalledTimes(0); expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(false); @@ -93,43 +93,43 @@ describe('GridRowHeader', () => { it('clicking on edit icon triggers inline title editor and does not toggle collapsed', async () => { const component = renderGridRowHeader(); - const editIcon = component.getByTestId('kbnGridRowTitle--edit'); + const editIcon = component.getByTestId('kbnGridRowTitle-0--edit'); - expect(component.queryByTestId('kbnGridRowTitle--editor')).not.toBeInTheDocument(); + expect(component.queryByTestId('kbnGridRowTitle-0--editor')).not.toBeInTheDocument(); expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(false); await userEvent.click(editIcon); - expect(component.getByTestId('kbnGridRowTitle--editor')).toBeInTheDocument(); + expect(component.getByTestId('kbnGridRowTitle-0--editor')).toBeInTheDocument(); expect(toggleIsCollapsed).toBeCalledTimes(0); expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(false); }); it('can update the title', async () => { const component = renderGridRowHeader(); - expect(component.getByTestId('kbnGridRowTitle').textContent).toBe('Large section'); + expect(component.getByTestId('kbnGridRowTitle-0').textContent).toBe('Large section'); expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].title).toBe('Large section'); - const editIcon = component.getByTestId('kbnGridRowTitle--edit'); + const editIcon = component.getByTestId('kbnGridRowTitle-0--edit'); await userEvent.click(editIcon); await setTitle(component); const saveButton = component.getByTestId('euiInlineEditModeSaveButton'); await userEvent.click(saveButton); - expect(component.queryByTestId('kbnGridRowTitle--editor')).not.toBeInTheDocument(); - expect(component.getByTestId('kbnGridRowTitle').textContent).toBe('Large section 123'); + expect(component.queryByTestId('kbnGridRowTitle-0--editor')).not.toBeInTheDocument(); + expect(component.getByTestId('kbnGridRowTitle-0').textContent).toBe('Large section 123'); expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].title).toBe('Large section 123'); }); it('clicking on cancel closes the inline title editor without updating title', async () => { const component = renderGridRowHeader(); - const editIcon = component.getByTestId('kbnGridRowTitle--edit'); + const editIcon = component.getByTestId('kbnGridRowTitle-0--edit'); await userEvent.click(editIcon); await setTitle(component); const cancelButton = component.getByTestId('euiInlineEditModeCancelButton'); await userEvent.click(cancelButton); - expect(component.queryByTestId('kbnGridRowTitle--editor')).not.toBeInTheDocument(); - expect(component.getByTestId('kbnGridRowTitle').textContent).toBe('Large section'); + expect(component.queryByTestId('kbnGridRowTitle-0--editor')).not.toBeInTheDocument(); + expect(component.getByTestId('kbnGridRowTitle-0').textContent).toBe('Large section'); expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].title).toBe('Large section'); }); }); 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 00dfe6895a4b2..fff91ec405619 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 @@ -95,7 +95,7 @@ export const GridRowHeader = React.memo( alignItems="center" css={headerStyles} className="kbnGridRowHeader" - data-test-subj={`kbnGridRowHeader--${rowIndex}`} + data-test-subj={`kbnGridRowHeader-${rowIndex}`} > - + {i18n.translate('kbnGridLayout.rowHeader.panelCount', { defaultMessage: '({panelCount} {panelCount, plural, one {panel} other {panels}})', diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index c0e219264dd7c..ca70d5d01f734 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -95,9 +95,9 @@ export const GridRowTitle = React.memo( onClick={toggleIsCollapsed} css={accordianButtonStyles} size="m" - id={`kbnGridRowTitle--${rowIndex}`} - aria-controls={`kbnGridRow--${rowIndex}`} - data-test-subj={`kbnGridRowTitle--${rowIndex}`} + id={`kbnGridRowTitle-${rowIndex}`} + aria-controls={`kbnGridRow-${rowIndex}`} + data-test-subj={`kbnGridRowTitle-${rowIndex}`} textProps={false} flush="both" > @@ -124,7 +124,7 @@ export const GridRowTitle = React.memo( inputAriaLabel={i18n.translate('kbnGridLayout.row.editTitleAriaLabel', { defaultMessage: 'Edit section title', })} - data-test-subj="kbnGridRowTitle--editor" + data-test-subj={`kbnGridRowTitle-${rowIndex}--editor`} /> ) : ( @@ -138,7 +138,7 @@ export const GridRowTitle = React.memo( aria-label={i18n.translate('kbnGridLayout.row.editRowTitle', { defaultMessage: 'Edit section title', })} - data-test-subj="kbnGridRowTitle--edit" + data-test-subj={`kbnGridRowTitle-${rowIndex}--edit`} /> )} From 2a5daf5da0920c4f1b6cea43c1cc50029a4a2ee1 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 18 Feb 2025 09:53:56 -0700 Subject: [PATCH 36/42] Cleanups after merge --- .../grid/grid_row/grid_row.tsx | 2 -- .../grid/grid_row/grid_row_header.test.tsx | 31 +++++++++++++------ .../grid/grid_row/grid_row_header.tsx | 13 +++----- .../grid/grid_row/grid_row_title.tsx | 6 ++-- .../private/kbn-grid-layout/grid/types.ts | 2 +- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index e5cd2b91dfb3d..364e22a2ca142 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -32,7 +32,6 @@ export const GridRow = React.memo(({ rowIndex }: GridRowProps) => { const [panelIdsInOrder, setPanelIdsInOrder] = useState(() => getKeysInOrder(currentRow.panels) ); - const [rowTitle, setRowTitle] = useState(currentRow.title); const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed); useEffect( @@ -138,7 +137,6 @@ export const GridRow = React.memo(({ rowIndex }: GridRowProps) => { {rowIndex !== 0 && ( diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx index 170a10bbaf7a7..9922c34ec1f62 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx @@ -13,10 +13,11 @@ import { EuiThemeProvider } from '@elastic/eui'; import { RenderResult, act, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { gridLayoutStateManagerMock } from '../test_utils/mocks'; +import { gridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks'; import { getSampleLayout } from '../test_utils/sample_layout'; import { GridLayoutStateManager } from '../types'; import { GridRowHeader, GridRowHeaderProps } from './grid_row_header'; +import { GridLayoutContext, GridLayoutContextType } from '../use_grid_layout_context'; const toggleIsCollapsed = jest .fn() @@ -27,16 +28,28 @@ const toggleIsCollapsed = jest }); describe('GridRowHeader', () => { - const renderGridRowHeader = (propsOverrides: Partial = {}) => { + const renderGridRowHeader = ( + propsOverrides: Partial = {}, + contextOverrides: Partial = {} + ) => { return render( - toggleIsCollapsed(0, gridLayoutStateManagerMock)} - gridLayoutStateManager={gridLayoutStateManagerMock} - collapseButtonRef={React.createRef()} - {...propsOverrides} - /> + + toggleIsCollapsed(0, gridLayoutStateManagerMock)} + collapseButtonRef={React.createRef()} + {...propsOverrides} + /> + ); }; 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 fff91ec405619..c5bd86e1d3e92 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 @@ -13,7 +13,7 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui' import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { GridLayoutStateManager } from '../types'; +import { useGridLayoutContext } from '../use_grid_layout_context'; import { deleteRow } from '../utils/row_management'; import { DeleteGridRowModal } from './delete_grid_row_modal'; import { GridRowTitle } from './grid_row_title'; @@ -21,18 +21,14 @@ import { useGridRowHeaderStyles } from './use_grid_row_header_styles'; export interface GridRowHeaderProps { rowIndex: number; - gridLayoutStateManager: GridLayoutStateManager; toggleIsCollapsed: () => void; collapseButtonRef: React.MutableRefObject; } export const GridRowHeader = React.memo( - ({ - rowIndex, - gridLayoutStateManager, - toggleIsCollapsed, - collapseButtonRef, - }: GridRowHeaderProps) => { + ({ rowIndex, toggleIsCollapsed, collapseButtonRef }: GridRowHeaderProps) => { + const { gridLayoutStateManager } = useGridLayoutContext(); + const headerStyles = useGridRowHeaderStyles(); const [editTitleOpen, setEditTitleOpen] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); @@ -103,7 +99,6 @@ export const GridRowHeader = React.memo( toggleIsCollapsed={toggleIsCollapsed} editTitleOpen={editTitleOpen} setEditTitleOpen={setEditTitleOpen} - gridLayoutStateManager={gridLayoutStateManager} collapseButtonRef={collapseButtonRef} /> { diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index ca70d5d01f734..483724eb3b164 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -21,7 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import { GridLayoutStateManager } from '../types'; +import { useGridLayoutContext } from '../use_grid_layout_context'; export const GridRowTitle = React.memo( ({ @@ -30,7 +30,6 @@ export const GridRowTitle = React.memo( editTitleOpen, setEditTitleOpen, toggleIsCollapsed, - gridLayoutStateManager, collapseButtonRef, }: { readOnly: boolean; @@ -38,9 +37,10 @@ export const GridRowTitle = React.memo( editTitleOpen: boolean; setEditTitleOpen: (value: boolean) => void; toggleIsCollapsed: () => void; - gridLayoutStateManager: GridLayoutStateManager; collapseButtonRef: React.MutableRefObject; }) => { + const { gridLayoutStateManager } = useGridLayoutContext(); + const inputRef = useRef(null); const currentRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex]; const [rowTitle, setRowTitle] = useState(currentRow.title); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/types.ts b/src/platform/packages/private/kbn-grid-layout/grid/types.ts index ddd7cbdb3bcfb..0e11723f97d2a 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/types.ts +++ b/src/platform/packages/private/kbn-grid-layout/grid/types.ts @@ -8,7 +8,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import type { ObservedSize } from 'useßresize-observer/polyfilled'; +import type { ObservedSize } from 'use-resize-observer/polyfilled'; export interface GridCoordinate { column: number; From ad12ed29e7f197157a6269a58b05e676bb523ec6 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 18 Feb 2025 14:17:43 -0700 Subject: [PATCH 37/42] Clean up + fix some more styles --- .../grid/grid_row/delete_grid_row_modal.tsx | 8 +-- .../grid/grid_row/grid_row.test.tsx | 4 +- .../grid/grid_row/grid_row.tsx | 2 +- .../grid/grid_row/grid_row_header.tsx | 45 +++++++++++++---- .../grid/grid_row/grid_row_title.tsx | 2 +- .../grid_row/use_grid_row_header_styles.tsx | 49 ------------------- 6 files changed, 45 insertions(+), 65 deletions(-) delete mode 100644 src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx index 988d16356f5c9..4874584c21d26 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx @@ -17,20 +17,20 @@ import { EuiModalHeader, EuiModalHeaderTitle, } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; -import { GridLayoutStateManager } from '../types'; + import { deleteRow, movePanelsToRow } from '../utils/row_management'; +import { useGridLayoutContext } from '../use_grid_layout_context'; export const DeleteGridRowModal = ({ rowIndex, - gridLayoutStateManager, setDeleteModalVisible, }: { rowIndex: number; - gridLayoutStateManager: GridLayoutStateManager; setDeleteModalVisible: (visible: boolean) => void; }) => { + const { gridLayoutStateManager } = useGridLayoutContext(); + return ( { diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx index f839f546b333c..aab38cb4a9af8 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx @@ -6,10 +6,12 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ +import React from 'react'; + import { EuiThemeProvider } from '@elastic/eui'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import React from 'react'; + import { gridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks'; import { getSampleLayout } from '../test_utils/sample_layout'; import { GridLayoutContext, type GridLayoutContextType } from '../use_grid_layout_context'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx index 364e22a2ca142..5df877fcb13d9 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.tsx @@ -7,13 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import classNames from 'classnames'; import { cloneDeep } from 'lodash'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { combineLatest, map, pairwise, skip } from 'rxjs'; import { css } from '@emotion/react'; -import classNames from 'classnames'; import { DragPreview } from '../drag_preview'; import { GridPanel } from '../grid_panel'; import { useGridLayoutContext } from '../use_grid_layout_context'; 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 c5bd86e1d3e92..fc20207519c02 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 @@ -9,7 +9,14 @@ import React, { useCallback, useEffect, useState } from 'react'; import { distinctUntilChanged, map } from 'rxjs'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiText, + UseEuiTheme, + euiCanAnimate, +} from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; @@ -17,7 +24,6 @@ import { useGridLayoutContext } from '../use_grid_layout_context'; import { deleteRow } from '../utils/row_management'; import { DeleteGridRowModal } from './delete_grid_row_modal'; import { GridRowTitle } from './grid_row_title'; -import { useGridRowHeaderStyles } from './use_grid_row_header_styles'; export interface GridRowHeaderProps { rowIndex: number; @@ -29,7 +35,6 @@ export const GridRowHeader = React.memo( ({ rowIndex, toggleIsCollapsed, collapseButtonRef }: GridRowHeaderProps) => { const { gridLayoutStateManager } = useGridLayoutContext(); - const headerStyles = useGridRowHeaderStyles(); const [editTitleOpen, setEditTitleOpen] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [readOnly, setReadOnly] = useState( @@ -89,7 +94,7 @@ export const GridRowHeader = React.memo( @@ -159,11 +164,7 @@ export const GridRowHeader = React.memo( } {deleteModalVisible && ( - + )} ); @@ -180,6 +181,32 @@ const styles = { floatToRight: css({ marginLeft: 'auto', }), + headerStyles: ({ euiTheme }: UseEuiTheme) => + css({ + height: `calc(${euiTheme.size.xl} + (2 * ${euiTheme.size.s}))`, + padding: `${euiTheme.size.s} 0px`, + borderBottom: '1px solid transparent', // prevents layout shift + '.kbnGridRowContainer--collapsed &': { + borderBottom: euiTheme.border.thin, + }, + '.kbnGridLayout--deleteRowIcon': { + marginLeft: euiTheme.size.xs, + }, + // these styles hide the delete + move actions by default and only show them on hover + [`.kbnGridLayout--deleteRowIcon, + .kbnGridLayout--moveRowIcon`]: { + opacity: '0', + [`${euiCanAnimate}`]: { + transition: `opacity ${euiTheme.animation.extraFast} ease-in`, + }, + }, + [`&:hover .kbnGridLayout--deleteRowIcon, + &:hover .kbnGridLayout--moveRowIcon, + &:has(:focus-visible) .kbnGridLayout--deleteRowIcon, + &:has(:focus-visible) .kbnGridLayout--moveRowIcon`]: { + opacity: 1, + }, + }), }; GridRowHeader.displayName = 'GridRowHeader'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 483724eb3b164..1fea08f2aad10 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -19,8 +19,8 @@ import { UseEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import { css } from '@emotion/react'; + import { useGridLayoutContext } from '../use_grid_layout_context'; export const GridRowTitle = React.memo( diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx deleted file mode 100644 index 896caa08edb07..0000000000000 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/use_grid_row_header_styles.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ -import { useMemo } from 'react'; - -import { euiCanAnimate, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; - -export const useGridRowHeaderStyles = () => { - const { euiTheme } = useEuiTheme(); - - const headerStyles = useMemo(() => { - return css` - height: calc(${euiTheme.size.xl} + (2 * ${euiTheme.size.s})); - padding: ${euiTheme.size.s} 0px; - - border-bottom: 1px solid transparent; // prevents layout shift - .kbnGridRowContainer--collapsed & { - border-bottom: ${euiTheme.border.thin}; - } - - .kbnGridLayout--deleteRowIcon { - margin-left: ${euiTheme.size.xs}; - } - - // these styles hide the delete + move actions by default and only show them on hover - .kbnGridLayout--deleteRowIcon, - .kbnGridLayout--moveRowIcon { - opacity: 0; - ${euiCanAnimate} { - transition: opacity ${euiTheme.animation.extraFast} ease-in; - } - } - &:hover .kbnGridLayout--deleteRowIcon, - &:hover .kbnGridLayout--moveRowIcon, - &:has(:focus-visible) .kbnGridLayout--deleteRowIcon, - &:has(:focus-visible) .kbnGridLayout--moveRowIcon { - opacity: 1; - } - `; - }, [euiTheme]); - - return headerStyles; -}; From 253c9f4f9b3c1a959b15187528c5d71f4c2a6f78 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 19 Feb 2025 11:21:25 -0700 Subject: [PATCH 38/42] Fix bug on reset --- .../private/kbn-grid-layout/grid/grid_row/grid_row_header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fc20207519c02..7790a54f5fae5 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 @@ -60,7 +60,7 @@ export const GridRowHeader = React.memo( */ const panelCountSubscription = gridLayoutStateManager.gridLayout$ .pipe( - map((layout) => Object.keys(layout[rowIndex].panels).length), + map((layout) => Object.keys(layout[rowIndex]?.panels ?? {}).length), distinctUntilChanged() ) .subscribe((count) => { From a90288a99963f520a3702d4b94f602db3f6cd225 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 19 Feb 2025 15:03:05 -0700 Subject: [PATCH 39/42] Use `field-sizing` for title input when possible --- .../grid/grid_row/grid_row_title.tsx | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 1fea08f2aad10..34ac6ff8396fc 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -93,7 +93,7 @@ export const GridRowTitle = React.memo( })} iconType={'arrowDown'} onClick={toggleIsCollapsed} - css={accordianButtonStyles} + css={styles.accordianButton} size="m" id={`kbnGridRowTitle-${rowIndex}`} aria-controls={`kbnGridRow-${rowIndex}`} @@ -109,7 +109,7 @@ export const GridRowTitle = React.memo( {!readOnly && editTitleOpen ? ( - + {/* @ts-ignore - EUI typing issue that will be resolved with https://github.com/elastic/eui/pull/8307 */} - css({ - '&:focus': { - backgroundColor: 'unset', - }, - svg: { - transition: `transform ${euiTheme.animation.fast} ease`, - transform: 'rotate(0deg)', - '.kbnGridRowContainer--collapsed &': { - transform: 'rotate(-90deg) !important', +const styles = { + titleInput: { + // if field-sizing is supported, grow width to text; otherwise, fill available space + '@supports (field-sizing: content)': { + minWidth: 0, + '.euiFlexItem:has(input)': { + flexGrow: 0, + maxWidth: 'calc(100% - 80px)', // don't extend past parent + }, + input: { + fieldSizing: 'content', }, }, - }); + }, + accordianButton: ({ euiTheme }: UseEuiTheme) => + css({ + '&:focus': { + backgroundColor: 'unset', + }, + svg: { + transition: `transform ${euiTheme.animation.fast} ease`, + transform: 'rotate(0deg)', + '.kbnGridRowContainer--collapsed &': { + transform: 'rotate(-90deg) !important', + }, + }, + }), +}; GridRowTitle.displayName = 'GridRowTitle'; From 7a2a70dec3ad4f127fdbeeb8b7b3582d91660098 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 19 Feb 2025 17:40:26 -0700 Subject: [PATCH 40/42] Better responsive headers --- .../grid/grid_row/grid_row_header.tsx | 5 +++ .../grid/grid_row/grid_row_title.tsx | 42 +++++++++++-------- 2 files changed, 29 insertions(+), 18 deletions(-) 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 7790a54f5fae5..391a48aa4e2c0 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 @@ -93,6 +93,7 @@ export const GridRowHeader = React.memo( <> {i18n.translate('kbnGridLayout.rowHeader.panelCount', { defaultMessage: @@ -192,6 +194,9 @@ const styles = { '.kbnGridLayout--deleteRowIcon': { marginLeft: euiTheme.size.xs, }, + '.kbnGridLayout--panelCount': { + textWrapMode: 'nowrap', // prevent panel count from wrapping + }, // these styles hide the delete + move actions by default and only show them on hover [`.kbnGridLayout--deleteRowIcon, .kbnGridLayout--moveRowIcon`]: { diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx index 34ac6ff8396fc..b4a7e9bb6d210 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_title.tsx @@ -84,7 +84,7 @@ export const GridRowTitle = React.memo( return ( <> - + {!readOnly && editTitleOpen ? ( - + {/* @ts-ignore - EUI typing issue that will be resolved with https://github.com/elastic/eui/pull/8307 */} + css({ + minWidth: 0, + button: { + '&:focus': { + backgroundColor: 'unset', + }, + h2: { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + svg: { + transition: `transform ${euiTheme.animation.fast} ease`, + transform: 'rotate(0deg)', + '.kbnGridRowContainer--collapsed &': { + transform: 'rotate(-90deg) !important', + }, + }, + }, + }), + editTitleInput: css({ // if field-sizing is supported, grow width to text; otherwise, fill available space '@supports (field-sizing: content)': { minWidth: 0, @@ -162,20 +181,7 @@ const styles = { fieldSizing: 'content', }, }, - }, - accordianButton: ({ euiTheme }: UseEuiTheme) => - css({ - '&:focus': { - backgroundColor: 'unset', - }, - svg: { - transition: `transform ${euiTheme.animation.fast} ease`, - transform: 'rotate(0deg)', - '.kbnGridRowContainer--collapsed &': { - transform: 'rotate(-90deg) !important', - }, - }, - }), + }), }; GridRowTitle.displayName = 'GridRowTitle'; From f5c90e68f3b6ea4660f485a468fd0daefa2c11ac Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 21 Feb 2025 16:46:17 -0700 Subject: [PATCH 41/42] Change modal based on suggestion --- .../grid/grid_row/delete_grid_row_modal.tsx | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx index 4874584c21d26..9aba4f4230ad9 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/delete_grid_row_modal.tsx @@ -47,11 +47,7 @@ export const DeleteGridRowModal = ({ {i18n.translate('kbnGridLayout.deleteGridRowModal.body', { defaultMessage: - 'Are you sure you want to remove this section and its {panelCount} {panelCount, plural, one {panel} other {panels}}?', - values: { - panelCount: Object.keys(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels) - .length, - }, + 'Choose to remove the section, including its contents, or only the section.', })} @@ -67,31 +63,37 @@ export const DeleteGridRowModal = ({ { setDeleteModalVisible(false); - const newLayout = deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowIndex); + let newLayout = movePanelsToRow( + gridLayoutStateManager.gridLayout$.getValue(), + rowIndex, + 0 + ); + newLayout = deleteRow(newLayout, rowIndex); gridLayoutStateManager.gridLayout$.next(newLayout); }} - fill color="danger" > - {i18n.translate('kbnGridLayout.deleteGridRowModal.confirmDeleteAllPanels', { - defaultMessage: 'Yes', + {i18n.translate('kbnGridLayout.deleteGridRowModal.confirmDeleteSection', { + defaultMessage: 'Delete section only', })} { setDeleteModalVisible(false); - let newLayout = movePanelsToRow( - gridLayoutStateManager.gridLayout$.getValue(), - rowIndex, - 0 - ); - newLayout = deleteRow(newLayout, rowIndex); + const newLayout = deleteRow(gridLayoutStateManager.gridLayout$.getValue(), rowIndex); gridLayoutStateManager.gridLayout$.next(newLayout); }} fill + color="danger" > - {i18n.translate('kbnGridLayout.deleteGridRowModal.confirmDeleteSection', { - defaultMessage: 'Delete section only', + {i18n.translate('kbnGridLayout.deleteGridRowModal.confirmDeleteAllPanels', { + defaultMessage: + 'Delete section and {panelCount} {panelCount, plural, one {panel} other {panels}}', + values: { + panelCount: Object.keys( + gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels + ).length, + }, })} From 18f090d67ce62e8005ce3aff663daf46d7c6939b Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Mon, 24 Feb 2025 09:13:54 -0700 Subject: [PATCH 42/42] Fix tests based on feedback --- .../kbn-grid-layout/grid/grid_layout.test.tsx | 12 +---- .../grid/grid_panel/grid_panel.test.tsx | 4 +- .../grid/grid_row/grid_row.test.tsx | 27 +++++----- .../grid/grid_row/grid_row_header.test.tsx | 51 +++++++++---------- .../kbn-grid-layout/grid/test_utils/mocks.tsx | 36 ++++++------- 5 files changed, 60 insertions(+), 70 deletions(-) diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx index 5f36560316283..fa54d83019129 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx @@ -35,21 +35,13 @@ const renderGridLayout = (propsOverrides: Partial = {}) => { ...propsOverrides, } as GridLayoutProps; - const { rerender, ...rtlRest } = render( - - - - ); + const { rerender, ...rtlRest } = render(, { wrapper: EuiThemeProvider }); return { ...rtlRest, rerender: (overrides: Partial) => { const newProps = { ...props, ...overrides } as GridLayoutProps; - return rerender( - - - - ); + return rerender(); }, }; }; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.test.tsx index b3c22bad19331..5df0b7d831e5e 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { GridPanel, type GridPanelProps } from './grid_panel'; -import { gridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks'; +import { getGridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks'; import { GridLayoutContext, type GridLayoutContextType } from '../use_grid_layout_context'; describe('GridPanel', () => { @@ -20,7 +20,7 @@ describe('GridPanel', () => { }) => { const contextValue = { renderPanelContents: mockRenderPanelContents, - gridLayoutStateManager: gridLayoutStateManagerMock, + gridLayoutStateManager: getGridLayoutStateManagerMock(), ...(overrides?.contextOverrides ?? {}), } as GridLayoutContextType; const panelProps = { diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx index aab38cb4a9af8..523453d8dd1e4 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row.test.tsx @@ -12,7 +12,7 @@ import { EuiThemeProvider } from '@elastic/eui'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { gridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks'; +import { getGridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks'; import { getSampleLayout } from '../test_utils/sample_layout'; import { GridLayoutContext, type GridLayoutContextType } from '../use_grid_layout_context'; import { GridRow, GridRowProps } from './grid_row'; @@ -23,19 +23,18 @@ describe('GridRow', () => { contextOverrides: Partial = {} ) => { return render( - - - - - + + + , + { wrapper: EuiThemeProvider } ); }; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx index 9922c34ec1f62..e5d855c12fd7f 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/grid_row_header.test.tsx @@ -13,8 +13,7 @@ import { EuiThemeProvider } from '@elastic/eui'; import { RenderResult, act, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { gridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks'; -import { getSampleLayout } from '../test_utils/sample_layout'; +import { getGridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks'; import { GridLayoutStateManager } from '../types'; import { GridRowHeader, GridRowHeaderProps } from './grid_row_header'; import { GridLayoutContext, GridLayoutContextType } from '../use_grid_layout_context'; @@ -32,43 +31,43 @@ describe('GridRowHeader', () => { propsOverrides: Partial = {}, contextOverrides: Partial = {} ) => { - return render( - + const stateManagerMock = getGridLayoutStateManagerMock(); + return { + component: render( toggleIsCollapsed(0, gridLayoutStateManagerMock)} + toggleIsCollapsed={() => toggleIsCollapsed(0, stateManagerMock)} collapseButtonRef={React.createRef()} {...propsOverrides} /> - - - ); + , + { wrapper: EuiThemeProvider } + ), + gridLayoutStateManager: stateManagerMock, + }; }; beforeEach(() => { toggleIsCollapsed.mockClear(); - act(() => { - gridLayoutStateManagerMock.gridLayout$.next(getSampleLayout()); - }); }); it('renders the panel count', async () => { - const component = renderGridRowHeader(); + const { component, gridLayoutStateManager } = renderGridRowHeader(); const initialCount = component.getByTestId('kbnGridRowHeader-0--panelCount'); expect(initialCount.textContent).toBe('(8 panels)'); act(() => { - const currentRow = gridLayoutStateManagerMock.gridLayout$.getValue()[0]; - gridLayoutStateManagerMock.gridLayout$.next([ + const currentRow = gridLayoutStateManager.gridLayout$.getValue()[0]; + gridLayoutStateManager.gridLayout$.next([ { ...currentRow, panels: { @@ -85,14 +84,14 @@ describe('GridRowHeader', () => { }); it('clicking title calls `toggleIsCollapsed`', async () => { - const component = renderGridRowHeader(); + const { component, gridLayoutStateManager } = renderGridRowHeader(); const title = component.getByTestId('kbnGridRowTitle-0'); expect(toggleIsCollapsed).toBeCalledTimes(0); - expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(false); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].isCollapsed).toBe(false); await userEvent.click(title); expect(toggleIsCollapsed).toBeCalledTimes(1); - expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(true); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].isCollapsed).toBe(true); }); describe('title editor', () => { @@ -105,21 +104,21 @@ describe('GridRowHeader', () => { }; it('clicking on edit icon triggers inline title editor and does not toggle collapsed', async () => { - const component = renderGridRowHeader(); + const { component, gridLayoutStateManager } = renderGridRowHeader(); const editIcon = component.getByTestId('kbnGridRowTitle-0--edit'); expect(component.queryByTestId('kbnGridRowTitle-0--editor')).not.toBeInTheDocument(); - expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(false); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].isCollapsed).toBe(false); await userEvent.click(editIcon); expect(component.getByTestId('kbnGridRowTitle-0--editor')).toBeInTheDocument(); expect(toggleIsCollapsed).toBeCalledTimes(0); - expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].isCollapsed).toBe(false); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].isCollapsed).toBe(false); }); it('can update the title', async () => { - const component = renderGridRowHeader(); + const { component, gridLayoutStateManager } = renderGridRowHeader(); expect(component.getByTestId('kbnGridRowTitle-0').textContent).toBe('Large section'); - expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].title).toBe('Large section'); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section'); const editIcon = component.getByTestId('kbnGridRowTitle-0--edit'); await userEvent.click(editIcon); @@ -129,11 +128,11 @@ describe('GridRowHeader', () => { expect(component.queryByTestId('kbnGridRowTitle-0--editor')).not.toBeInTheDocument(); expect(component.getByTestId('kbnGridRowTitle-0').textContent).toBe('Large section 123'); - expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].title).toBe('Large section 123'); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section 123'); }); it('clicking on cancel closes the inline title editor without updating title', async () => { - const component = renderGridRowHeader(); + const { component, gridLayoutStateManager } = renderGridRowHeader(); const editIcon = component.getByTestId('kbnGridRowTitle-0--edit'); await userEvent.click(editIcon); @@ -143,7 +142,7 @@ describe('GridRowHeader', () => { expect(component.queryByTestId('kbnGridRowTitle-0--editor')).not.toBeInTheDocument(); expect(component.getByTestId('kbnGridRowTitle-0').textContent).toBe('Large section'); - expect(gridLayoutStateManagerMock.gridLayout$.getValue()[0].title).toBe('Large section'); + expect(gridLayoutStateManager.gridLayout$.getValue()[0].title).toBe('Large section'); }); }); }); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/test_utils/mocks.tsx b/src/platform/packages/private/kbn-grid-layout/grid/test_utils/mocks.tsx index 5efb50d9d5f04..b5190166bc94f 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/test_utils/mocks.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/test_utils/mocks.tsx @@ -29,26 +29,26 @@ export const gridSettings = { rowHeight: DASHBOARD_GRID_HEIGHT, columnCount: DASHBOARD_GRID_COLUMN_COUNT, }; - export const mockRenderPanelContents = jest.fn((panelId) => ( )); -const runtimeSettings$ = new BehaviorSubject({ - ...gridSettings, - columnPixelWidth: 0, -}); - -export const gridLayoutStateManagerMock: GridLayoutStateManager = { - expandedPanelId$: new BehaviorSubject(undefined), - isMobileView$: new BehaviorSubject(false), - gridLayout$: new BehaviorSubject(getSampleLayout()), - proposedGridLayout$: new BehaviorSubject(undefined), - runtimeSettings$, - panelRefs: { current: [] }, - rowRefs: { current: [] }, - accessMode$: new BehaviorSubject('EDIT'), - interactionEvent$: new BehaviorSubject(undefined), - activePanel$: new BehaviorSubject(undefined), - gridDimensions$: new BehaviorSubject({ width: 600, height: 900 }), +export const getGridLayoutStateManagerMock = (overrides?: Partial) => { + return { + expandedPanelId$: new BehaviorSubject(undefined), + isMobileView$: new BehaviorSubject(false), + gridLayout$: new BehaviorSubject(getSampleLayout()), + proposedGridLayout$: new BehaviorSubject(undefined), + runtimeSettings$: new BehaviorSubject({ + ...gridSettings, + columnPixelWidth: 0, + }), + panelRefs: { current: [] }, + rowRefs: { current: [] }, + accessMode$: new BehaviorSubject('EDIT'), + interactionEvent$: new BehaviorSubject(undefined), + activePanel$: new BehaviorSubject(undefined), + gridDimensions$: new BehaviorSubject({ width: 600, height: 900 }), + ...overrides, + }; };