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 d2d20ca7dec26..1c4bff24d0b63 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 @@ -90,6 +90,14 @@ export const GridLayout = ({ } }); + const rowOrderSubscription = combineLatest([ + gridLayoutStateManager.proposedGridLayout$, + gridLayoutStateManager.gridLayout$, + ]).subscribe(([proposedGridLayout, gridLayout]) => { + const displayedGridLayout = proposedGridLayout ?? gridLayout; + setRowIdsInOrder(getRowKeysInOrder(displayedGridLayout)); + }); + /** * This subscription adds and/or removes the necessary class names related to styling for * mobile view and a static (non-interactable) grid layout @@ -115,6 +123,7 @@ export const GridLayout = ({ return () => { onLayoutChangeSubscription.unsubscribe(); + rowOrderSubscription.unsubscribe(); gridLayoutClassSubscription.unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -160,7 +169,7 @@ const styles = { padding: 'calc(var(--kbnGridGutterSize) * 1px)', }), hasActivePanel: css({ - '&:has(.kbnGridPanel--active)': { + '&:has(.kbnGridPanel--active), &:has(.kbnGridRowHeader--active)': { // disable pointer events and user select on drag + resize userSelect: 'none', pointerEvents: 'none', diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/default_drag_handle.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/default_drag_handle.tsx index b24ba1eb62ffa..906965ec9dc9c 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/default_drag_handle.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/default_drag_handle.tsx @@ -47,7 +47,7 @@ const styles = ({ euiTheme }: UseEuiTheme) => border: `1px solid ${euiTheme.border.color}`, borderBottom: 'none', backgroundColor: euiTheme.colors.backgroundBasePlain, - borderRadius: `${euiTheme.border.radius} ${euiTheme.border.radius} 0 0`, + borderRadius: `${euiTheme.border.radius.medium} ${euiTheme.border.radius.medium} 0 0`, transition: `${euiTheme.animation.slow} opacity`, touchAction: 'none', '.kbnGridPanel:hover &, .kbnGridPanel:focus-within &, &:active, &:focus': { diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/use_drag_handle_api.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/use_drag_handle_api.tsx index b3470018cd5b3..374ceb4b1e4b8 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/use_drag_handle_api.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle/use_drag_handle_api.tsx @@ -9,7 +9,7 @@ import { useCallback, useEffect, useRef } from 'react'; -import { useGridLayoutEvents } from '../../use_grid_layout_events'; +import { useGridLayoutPanelEvents } from '../../use_grid_layout_events/panel_events'; import { UserInteractionEvent } from '../../use_grid_layout_events/types'; import { useGridLayoutContext } from '../../use_grid_layout_context'; @@ -27,7 +27,7 @@ export const useDragHandleApi = ({ }): DragHandleApi => { const { useCustomDragHandle } = useGridLayoutContext(); - const startInteraction = useGridLayoutEvents({ + const startInteraction = useGridLayoutPanelEvents({ interactionType: 'drag', panelId, rowId, diff --git a/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_preview.tsx similarity index 94% rename from src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx rename to src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_preview.tsx index dd489bdc915be..7883197c3bc77 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_preview.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useRef } from 'react'; import { combineLatest, skip } from 'rxjs'; import { css } from '@emotion/react'; -import { useGridLayoutContext } from './use_grid_layout_context'; +import { useGridLayoutContext } from '../use_grid_layout_context'; export const DragPreview = React.memo(({ rowId }: { rowId: string }) => { const { gridLayoutStateManager } = useGridLayoutContext(); @@ -54,4 +54,4 @@ export const DragPreview = React.memo(({ rowId }: { rowId: string }) => { const styles = css({ display: 'none', pointerEvents: 'none' }); -DragPreview.displayName = 'KbnGridLayoutDragPreview'; +DragPreview.displayName = 'KbnGridLayoutDragPanelPreview'; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/resize_handle.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/resize_handle.tsx index 8fb0254bccba4..70847ff7df8a7 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/resize_handle.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/resize_handle.tsx @@ -13,10 +13,10 @@ import type { UseEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { useGridLayoutEvents } from '../use_grid_layout_events'; +import { useGridLayoutPanelEvents } from '../use_grid_layout_events/panel_events'; export const ResizeHandle = React.memo(({ rowId, panelId }: { rowId: string; panelId: string }) => { - const startInteraction = useGridLayoutEvents({ + const startInteraction = useGridLayoutPanelEvents({ interactionType: 'resize', panelId, rowId, 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 ef89fa659d88a..1d748e7a6b4a0 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 @@ -19,8 +19,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { deleteRow, movePanelsToRow } from '../utils/row_management'; import { useGridLayoutContext } from '../use_grid_layout_context'; +import { deleteRow, movePanelsToRow } from '../utils/row_management'; export const DeleteGridRowModal = ({ rowId, diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_row/drag_preview.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/drag_preview.tsx new file mode 100644 index 0000000000000..8e2c711588bb0 --- /dev/null +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_row/drag_preview.tsx @@ -0,0 +1,33 @@ +/* + * 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, { useRef } from 'react'; + +import { css } from '@emotion/react'; +import { UseEuiTheme } from '@elastic/eui'; +// import { useGridLayoutContext } from '../use_grid_layout_context'; + +export const DragPreview = React.memo(({ rowId }: { rowId: string }) => { + // const { gridLayoutStateManager } = useGridLayoutContext(); + + const dragPreviewRef = useRef(null); + + return
; +}); + +const styles = ({ euiTheme }: UseEuiTheme) => + css({ + width: '100%', + height: '32px', + margin: '8px 0px', + backgroundColor: euiTheme.components.dragDropDraggingBackground, + position: 'relative', + }); + +DragPreview.displayName = 'KbnGridLayoutDragRowPreview'; 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 57b6b5042b4e2..f14357bddd15d 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 @@ -14,11 +14,11 @@ import { combineLatest, map, pairwise, skip } from 'rxjs'; import { css } from '@emotion/react'; -import { DragPreview } from '../drag_preview'; +import { DragPreview as DragPanelPreview } from '../grid_panel/drag_preview'; import { GridPanel } from '../grid_panel'; import { useGridLayoutContext } from '../use_grid_layout_context'; -import { getPanelKeysInOrder } from '../utils/resolve_grid_row'; import { GridRowHeader } from './grid_row_header'; +import { getPanelKeysInOrder } from '../utils/resolve_grid_row'; export interface GridRowProps { rowId: string; @@ -156,7 +156,7 @@ export const GridRow = React.memo(({ rowId }: GridRowProps) => { {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 a54172cd77999..b7d225ae3a5c8 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,9 @@ * 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, useState } from 'react'; -import { distinctUntilChanged, map } from 'rxjs'; +import classNames from 'classnames'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { distinctUntilChanged, map, skip } from 'rxjs'; import { EuiButtonIcon, @@ -21,8 +22,10 @@ import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { useGridLayoutContext } from '../use_grid_layout_context'; +import { useGridLayoutRowEvents } from '../use_grid_layout_events/row_events'; import { deleteRow } from '../utils/row_management'; import { DeleteGridRowModal } from './delete_grid_row_modal'; +import { DragPreview } from './drag_preview'; import { GridRowTitle } from './grid_row_title'; export interface GridRowHeaderProps { @@ -34,7 +37,12 @@ export interface GridRowHeaderProps { export const GridRowHeader = React.memo( ({ rowId, toggleIsCollapsed, collapseButtonRef }: GridRowHeaderProps) => { const { gridLayoutStateManager } = useGridLayoutContext(); + const startInteraction = useGridLayoutRowEvents({ + interactionType: 'drag', + rowId, + }); + const [isActive, setIsActive] = useState(false); const [editTitleOpen, setEditTitleOpen] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [readOnly, setReadOnly] = useState( @@ -67,9 +75,31 @@ export const GridRowHeader = React.memo( setPanelCount(count); }); + const dragRowStyleSubscription = gridLayoutStateManager.activeRow$ + .pipe(skip(1)) + .subscribe((activeRow) => { + const headerRef = gridLayoutStateManager.headerRefs.current[rowId]; + if (!headerRef) return; + + if (activeRow?.id === rowId) { + setIsActive(true); + headerRef.style.position = 'fixed'; + headerRef.style.top = `${activeRow.startingPosition.top}px`; + headerRef.style.right = `${activeRow.startingPosition.right}px`; + headerRef.style.transform = `translate(${activeRow.translate.left}px, ${activeRow.translate.top}px)`; + } else { + setIsActive(false); + headerRef.style.position = 'relative'; + headerRef.style.top = ``; + headerRef.style.right = ``; + headerRef.style.transform = ``; + } + }); + return () => { accessModeSubscription.unsubscribe(); panelCountSubscription.unsubscribe(); + dragRowStyleSubscription.unsubscribe(); }; }, [gridLayoutStateManager, rowId]); @@ -94,11 +124,15 @@ export const GridRowHeader = React.memo( responsive={false} alignItems="center" css={styles.headerStyles} - className="kbnGridRowHeader" + className={classNames('kbnGridRowHeader', { 'kbnGridRowHeader--active': isActive })} data-test-subj={`kbnGridRowHeader-${rowId}`} + ref={(element: HTMLDivElement | null) => + (gridLayoutStateManager.headerRefs.current[rowId] = element) + } > - - - {i18n.translate('kbnGridLayout.rowHeader.panelCount', { - defaultMessage: - '({panelCount} {panelCount, plural, one {panel} other {panels}})', - values: { - panelCount, - }, - })} - - + {!isActive && ( + + + {i18n.translate('kbnGridLayout.rowHeader.panelCount', { + defaultMessage: + '({panelCount} {panelCount, plural, one {panel} other {panels}})', + values: { + panelCount, + }, + })} + + + )} {!readOnly && ( <> - - - + {!isActive && ( + + + + )} - {/* - This was added as a placeholder to get the desired UI here; however, since the - functionality will be implemented in https://github.com/elastic/kibana/issues/190381 - and this button doesn't do anything yet, I'm choosing to hide it for now. I am keeping - the `FlexItem` wrapper so that the UI still looks correct. - */} - {/* */} + onMouseDown={(e) => { + startInteraction(e); + }} + /> )} @@ -163,6 +198,8 @@ export const GridRowHeader = React.memo( ) } + {isActive && } + {/* {' '} */} {deleteModalVisible && ( )} @@ -183,9 +220,21 @@ const styles = { }), headerStyles: ({ euiTheme }: UseEuiTheme) => css({ + '&.kbnGridRowHeader--active': { + width: 'fit-content', + backgroundColor: euiTheme.colors.backgroundBasePlain, + border: `1px solid ${euiTheme.border.color}`, + borderRadius: `${euiTheme.border.radius.medium} ${euiTheme.border.radius.medium}`, + paddingLeft: '8px', + minWidth: '300px', + zIndex: euiTheme.levels.modal, + '.kbnGridLayout--moveRowIcon': { + opacity: 1, + }, + }, height: `calc(${euiTheme.size.xl} + (2 * ${euiTheme.size.s}))`, padding: `${euiTheme.size.s} 0px`, - borderBottom: '1px solid transparent', // prevents layout shift + border: '1px solid transparent', // prevents layout shift '.kbnGridRowContainer--collapsed &': { borderBottom: euiTheme.border.thin, }, @@ -195,6 +244,15 @@ const styles = { '.kbnGridLayout--panelCount': { textWrapMode: 'nowrap', // prevent panel count from wrapping }, + + '.kbnGridLayout--moveRowIcon': { + '&:active, &:hover': { + cursor: 'move', + backgroundColor: 'transparent', + transform: 'none !important', + }, + }, + // 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 9c0e69e1895be..289680e295e10 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 @@ -26,6 +26,7 @@ import { useGridLayoutContext } from '../use_grid_layout_context'; export const GridRowTitle = React.memo( ({ readOnly, + isActive, rowId, editTitleOpen, setEditTitleOpen, @@ -33,6 +34,7 @@ export const GridRowTitle = React.memo( collapseButtonRef, }: { readOnly: boolean; + isActive: boolean; rowId: string; editTitleOpen: boolean; setEditTitleOpen: (value: boolean) => void; @@ -91,7 +93,7 @@ export const GridRowTitle = React.memo( aria-label={i18n.translate('kbnGridLayout.row.toggleCollapse', { defaultMessage: 'Toggle collapse', })} - iconType={'arrowDown'} + iconType={isActive ? undefined : 'arrowDown'} onClick={toggleIsCollapsed} size="m" id={`kbnGridRowTitle-${rowId}`} @@ -128,7 +130,7 @@ export const GridRowTitle = React.memo( ) : ( <> - {!readOnly && ( + {!readOnly && !isActive && ( ; proposedGridLayout$: BehaviorSubject; // temporary state for layout during drag and drop operations @@ -69,14 +81,23 @@ export interface GridLayoutStateManager { gridDimensions$: BehaviorSubject; runtimeSettings$: BehaviorSubject; activePanel$: BehaviorSubject; + activeRow$: BehaviorSubject; interactionEvent$: BehaviorSubject; rowRefs: React.MutableRefObject<{ [rowId: string]: HTMLDivElement | null }>; + headerRefs: React.MutableRefObject<{ [rowId: string]: HTMLDivElement | null }>; panelRefs: React.MutableRefObject<{ [rowId: string]: { [panelId: string]: HTMLDivElement | null }; }>; } +export interface RowInteractionEvent { + type: 'drag'; + id: string; + targetOrder: number; + rowDiv: HTMLDivElement; +} + /** * The information required to start a panel interaction. */ diff --git a/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/panel_events.ts b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/panel_events.ts new file mode 100644 index 0000000000000..d0134afa329e4 --- /dev/null +++ b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/panel_events.ts @@ -0,0 +1,87 @@ +/* + * 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 { useCallback, useRef } from 'react'; +import { GridPanelData, GridLayoutStateManager, PanelInteractionEvent } from '../types'; +import { + getPointerPosition, + isMouseEvent, + isTouchEvent, + startMouseInteraction, + startTouchInteraction, +} from './sensors'; +import { commitAction, moveAction, startAction } from './state_manager_actions'; +import { UserInteractionEvent } from './types'; +import { useGridLayoutContext } from '../use_grid_layout_context'; + +/* + * This hook sets up and manages drag/resize interaction logic for grid panels. + * It initializes event handlers to start, move, and commit the interaction, + * ensuring responsive updates to the panel's position and grid layout state. + * The interaction behavior is dynamic and adapts to the input type (mouse or touch). + */ + +export const useGridLayoutPanelEvents = ({ + interactionType, + rowId, + panelId, +}: { + interactionType: PanelInteractionEvent['type']; + rowId: string; + panelId: string; +}) => { + const { gridLayoutStateManager } = useGridLayoutContext(); + + const lastRequestedPanelPosition = useRef(undefined); + const pointerPixel = useRef<{ clientX: number; clientY: number }>({ clientX: 0, clientY: 0 }); + + const startInteraction = useCallback( + (e: UserInteractionEvent) => { + if (!isLayoutInteractive(gridLayoutStateManager)) return; + + const onStart = () => startAction(e, gridLayoutStateManager, interactionType, rowId, panelId); + + const onMove = (ev: UserInteractionEvent) => { + if (isMouseEvent(ev) || isTouchEvent(ev)) { + pointerPixel.current = getPointerPosition(ev); + } + moveAction(gridLayoutStateManager, pointerPixel.current, lastRequestedPanelPosition); + }; + + const onEnd = () => commitAction(gridLayoutStateManager); + + if (isMouseEvent(e)) { + e.stopPropagation(); + startMouseInteraction({ + e, + onStart, + onMove, + onEnd, + }); + } else if (isTouchEvent(e)) { + startTouchInteraction({ + e, + onStart, + onMove, + onEnd, + }); + } + }, + [gridLayoutStateManager, rowId, panelId, interactionType] + ); + + return startInteraction; +}; + +const isLayoutInteractive = (gridLayoutStateManager: GridLayoutStateManager) => { + return ( + gridLayoutStateManager.expandedPanelId$.value === undefined && + gridLayoutStateManager.accessMode$.getValue() === 'EDIT' + ); +}; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/row_events.ts b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/row_events.ts new file mode 100644 index 0000000000000..3e0af02f3cad1 --- /dev/null +++ b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/row_events.ts @@ -0,0 +1,144 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { cloneDeep, pick } from 'lodash'; +import { useCallback, useRef } from 'react'; +import { GridLayoutStateManager, RowInteractionEvent } from '../types'; +import { useGridLayoutContext } from '../use_grid_layout_context'; +import { getRowKeysInOrder } from '../utils/resolve_grid_row'; +import { + getPointerPosition, + isMouseEvent, + isTouchEvent, + startMouseInteraction, + startTouchInteraction, +} from './sensors'; +import { UserInteractionEvent } from './types'; + +export const useGridLayoutRowEvents = ({ + interactionType, + rowId, +}: { + interactionType: RowInteractionEvent['type']; + rowId: string; +}) => { + const { gridLayoutStateManager } = useGridLayoutContext(); + + const pointerPixel = useRef<{ clientX: number; clientY: number }>({ clientX: 0, clientY: 0 }); + const startingMouse = useRef<{ clientX: number; clientY: number }>({ clientX: 0, clientY: 0 }); + const startingPosition = useRef<{ top: number; right: number }>({ top: 0, right: 0 }); + + const startInteraction = useCallback( + (e: UserInteractionEvent) => { + if (!isLayoutInteractive(gridLayoutStateManager)) return; + + const onStart = () => { + const headerRef = gridLayoutStateManager.headerRefs.current[rowId]; + if (!headerRef) return; + + const newStartingPosition = headerRef.getBoundingClientRect(); + startingPosition.current = { + top: newStartingPosition.top, + right: newStartingPosition.x, + }; + startingMouse.current = pick(getPointerPosition(e), ['clientX', 'clientY']); + + gridLayoutStateManager.activeRow$.next({ + id: rowId, + startingPosition: startingPosition.current, + translate: { + top: 0, + left: 0, + }, + }); + }; + + const onMove = (ev: UserInteractionEvent) => { + if (isMouseEvent(ev) || isTouchEvent(ev)) { + pointerPixel.current = getPointerPosition(ev); + } + + const headerRef = gridLayoutStateManager.headerRefs.current[rowId]; + if (!headerRef) return; + + const currentLayout = + gridLayoutStateManager.proposedGridLayout$.getValue() ?? + gridLayoutStateManager.gridLayout$.getValue(); + const currentRowOrder = getRowKeysInOrder(currentLayout); + currentRowOrder.shift(); // drop first row since nothing can go above it + const updatedRowOrder = Object.keys(gridLayoutStateManager.headerRefs.current).sort( + (idA, idB) => { + const rowRefA = gridLayoutStateManager.headerRefs.current[idA]; + const rowRefB = gridLayoutStateManager.headerRefs.current[idB]; + + const rectA = rowRefA?.getBoundingClientRect(); + const rectB = rowRefB?.getBoundingClientRect(); + return (rectA?.top ?? 0) - (rectB?.top ?? 0); + } + ); + if (!deepEqual(currentRowOrder, updatedRowOrder)) { + const updatedLayout = cloneDeep(currentLayout); + updatedRowOrder.forEach((id, index) => { + updatedLayout[id].order = index + 1; + }); + gridLayoutStateManager.proposedGridLayout$.next(updatedLayout); + } + + gridLayoutStateManager.activeRow$.next({ + id: rowId, + startingPosition: startingPosition.current, + translate: { + top: pointerPixel.current.clientY - startingMouse.current.clientY, + left: pointerPixel.current.clientX - startingMouse.current.clientX, + }, + }); + }; + + const onEnd = () => { + gridLayoutStateManager.activeRow$.next(undefined); + const proposedGridLayoutValue = gridLayoutStateManager.proposedGridLayout$.getValue(); + if ( + proposedGridLayoutValue && + !deepEqual(proposedGridLayoutValue, gridLayoutStateManager.gridLayout$.getValue()) + ) { + gridLayoutStateManager.gridLayout$.next(cloneDeep(proposedGridLayoutValue)); + } + gridLayoutStateManager.proposedGridLayout$.next(undefined); + }; + + if (isMouseEvent(e)) { + e.stopPropagation(); + startMouseInteraction({ + e, + onStart, + onMove, + onEnd, + }); + } else if (isTouchEvent(e)) { + startTouchInteraction({ + e, + onStart, + onMove, + onEnd, + }); + } + }, + [gridLayoutStateManager, rowId, interactionType] + ); + + return startInteraction; +}; + +const isLayoutInteractive = (gridLayoutStateManager: GridLayoutStateManager) => { + return ( + gridLayoutStateManager.expandedPanelId$.value === undefined && + gridLayoutStateManager.accessMode$.getValue() === 'EDIT' + ); +}; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/state_manager_actions.ts b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/state_manager_actions.ts index cbc847cc357ea..e39655bd95331 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/state_manager_actions.ts +++ b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/state_manager_actions.ts @@ -72,7 +72,7 @@ export const moveAction = ( return; } - const currentLayout = proposedGridLayout$.value; + const currentLayout = cloneDeep(proposedGridLayout$.value); const currentPanelData = currentLayout?.[interactionEvent.targetRow].panels[interactionEvent.id]; @@ -164,7 +164,6 @@ export const moveAction = ( ) { lastRequestedPanelPosition.current = { ...requestedPanelData }; - // remove the panel from the row it's currently in. const nextLayout = cloneDeep(currentLayout); Object.entries(nextLayout).forEach(([rowId, row]) => { const { [interactionEvent.id]: interactingPanel, ...otherPanels } = row.panels; 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 5c969590b057a..833ed5c0875c5 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 @@ -16,6 +16,7 @@ import useResizeObserver, { type ObservedSize } from 'use-resize-observer/polyfi import { ActivePanel, + ActiveRow, GridAccessMode, GridLayoutData, GridLayoutStateManager, @@ -43,6 +44,7 @@ export const useGridLayoutState = ({ setDimensionsRef: (instance: HTMLDivElement | null) => void; } => { const rowRefs = useRef<{ [rowId: string]: HTMLDivElement | null }>({}); + const headerRefs = useRef<{ [rowId: string]: HTMLDivElement | null }>({}); const panelRefs = useRef<{ [rowId: string]: { [panelId: string]: HTMLDivElement | null } }>({}); const { euiTheme } = useEuiTheme(); @@ -93,13 +95,16 @@ export const useGridLayoutState = ({ const gridDimensions$ = new BehaviorSubject({ width: 0, height: 0 }); const interactionEvent$ = new BehaviorSubject(undefined); const activePanel$ = new BehaviorSubject(undefined); + const activeRow$ = new BehaviorSubject(undefined); return { rowRefs, + headerRefs, panelRefs, proposedGridLayout$, gridLayout$, activePanel$, + activeRow$, accessMode$, gridDimensions$, runtimeSettings$,