{
setDimensionsRef(divElement);
}}
+ className={gridClassNames}
+ css={css`
+ &.kbnGrid--hasExpandedPanel {
+ height: 100%;
+ }
+ `}
>
{children}
diff --git a/packages/kbn-grid-layout/grid/grid_panel.tsx b/packages/kbn-grid-layout/grid/grid_panel.tsx
index a44a321a7b18d..91f935f4507f1 100644
--- a/packages/kbn-grid-layout/grid/grid_panel.tsx
+++ b/packages/kbn-grid-layout/grid/grid_panel.tsx
@@ -22,6 +22,7 @@ import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { GridLayoutStateManager, PanelInteractionEvent } from './types';
+import { getKeysInOrder } from './utils/resolve_grid_row';
export const GridPanel = forwardRef<
HTMLDivElement,
@@ -50,13 +51,21 @@ export const GridPanel = forwardRef<
grid-column-end: ${initialPanel.column + 1 + initialPanel.width};
grid-row-start: ${initialPanel.row + 1};
grid-row-end: ${initialPanel.row + 1 + initialPanel.height};
+ &.kbnGridPanel--isExpanded {
+ transform: translate(9999px, 9999px);
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
`;
}, [gridLayoutStateManager, rowIndex, panelId]);
useEffect(
() => {
/** Update the styles of the panel via a subscription to prevent re-renders */
- const styleSubscription = combineLatest([
+ const activePanelStyleSubscription = combineLatest([
gridLayoutStateManager.activePanel$,
gridLayoutStateManager.gridLayout$,
gridLayoutStateManager.runtimeSettings$,
@@ -68,6 +77,7 @@ export const GridPanel = forwardRef<
if (!ref || !panel) return;
const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
+
if (panelId === activePanel?.id) {
// if the current panel is active, give it fixed positioning depending on the interaction event
const { position: draggingPosition } = activePanel;
@@ -91,7 +101,7 @@ export const GridPanel = forwardRef<
ref.style.gridRowEnd = ``;
} else {
// if the current panel is being dragged, render it with a fixed position + size
- ref.style.position = 'fixed';
+ ref.style.position = `fixed`;
ref.style.left = `${draggingPosition.left}px`;
ref.style.top = `${draggingPosition.top}px`;
ref.style.width = `${draggingPosition.right - draggingPosition.left}px`;
@@ -121,8 +131,50 @@ export const GridPanel = forwardRef<
}
});
+ const expandedPanelStyleSubscription = gridLayoutStateManager.expandedPanelId$
+ .pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
+ .subscribe((expandedPanelId) => {
+ const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
+ const gridLayout = gridLayoutStateManager.gridLayout$.getValue();
+ const panel = gridLayout[rowIndex].panels[panelId];
+ if (!ref || !panel) return;
+
+ if (expandedPanelId && expandedPanelId === panelId) {
+ ref.classList.add('kbnGridPanel--isExpanded');
+ } else {
+ ref.classList.remove('kbnGridPanel--isExpanded');
+ }
+ });
+
+ const mobileViewStyleSubscription = gridLayoutStateManager.isMobileView$
+ .pipe(skip(1))
+ .subscribe((isMobileView) => {
+ if (!isMobileView) {
+ return;
+ }
+ const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
+ const gridLayout = gridLayoutStateManager.gridLayout$.getValue();
+ const allPanels = gridLayout[rowIndex].panels;
+ const panel = allPanels[panelId];
+ if (!ref || !panel) return;
+
+ const sortedKeys = getKeysInOrder(gridLayout[rowIndex]);
+ const currentPanelPosition = sortedKeys.indexOf(panelId);
+ const sortedKeysBefore = sortedKeys.slice(0, currentPanelPosition);
+ const responsiveGridRowStart = sortedKeysBefore.reduce(
+ (acc, key) => acc + allPanels[key].height,
+ 1
+ );
+ ref.style.gridColumnStart = `1`;
+ ref.style.gridColumnEnd = `-1`;
+ ref.style.gridRowStart = `${responsiveGridRowStart}`;
+ ref.style.gridRowEnd = `${responsiveGridRowStart + panel.height}`;
+ });
+
return () => {
- styleSubscription.unsubscribe();
+ expandedPanelStyleSubscription.unsubscribe();
+ mobileViewStyleSubscription.unsubscribe();
+ activePanelStyleSubscription.unsubscribe();
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -150,7 +202,7 @@ export const GridPanel = forwardRef<
>
{/* drag handle */}
interactionStart('drag', e)}
onMouseUp={(e) => interactionStart('drop', e)}
@@ -182,7 +238,7 @@ export const GridPanel = forwardRef<
{/* Resize handle */}
interactionStart('resize', e)}
onMouseUp={(e) => interactionStart('drop', e)}
css={css`
@@ -202,6 +258,10 @@ export const GridPanel = forwardRef<
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
cursor: se-resize;
}
+ .kbnGrid--static & {
+ opacity: 0 !important;
+ display: none;
+ }
`}
/>
(null);
/** Set initial styles based on state at mount to prevent styles from "blipping" */
const initialStyles = useMemo(() => {
@@ -74,7 +75,7 @@ export const GridRow = forwardRef<
useEffect(
() => {
/** Update the styles of the grid row via a subscription to prevent re-renders */
- const styleSubscription = combineLatest([
+ const interactionStyleSubscription = combineLatest([
gridLayoutStateManager.interactionEvent$,
gridLayoutStateManager.gridLayout$,
gridLayoutStateManager.runtimeSettings$,
@@ -115,6 +116,36 @@ export const GridRow = forwardRef<
}
});
+ const expandedPanelStyleSubscription = gridLayoutStateManager.expandedPanelId$
+ .pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
+ .subscribe((expandedPanelId) => {
+ const rowContainerRef = rowContainer.current;
+ if (!rowContainerRef) return;
+
+ if (expandedPanelId) {
+ // If any panel is expanded, move all rows with their panels out of the viewport.
+ // The expanded panel is repositioned to its original location in the GridPanel component
+ // and stretched to fill the viewport.
+
+ rowContainerRef.style.transform = 'translate(-9999px, -9999px)';
+
+ const panelsIds = Object.keys(
+ gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels
+ );
+ const includesExpandedPanel = panelsIds.includes(expandedPanelId);
+ if (includesExpandedPanel) {
+ // Stretch the row with the expanded panel to occupy the entire remaining viewport
+ rowContainerRef.style.height = '100%';
+ } else {
+ // Hide the row if it does not contain the expanded panel
+ rowContainerRef.style.height = '0';
+ }
+ } else {
+ rowContainerRef.style.transform = ``;
+ rowContainerRef.style.height = ``;
+ }
+ });
+
/**
* The things that should trigger a re-render are title, collapsed state, and panel ids - panel positions
* are being controlled via CSS styles, so they do not need to trigger a re-render. This subscription ensures
@@ -147,8 +178,9 @@ export const GridRow = forwardRef<
});
return () => {
- styleSubscription.unsubscribe();
+ interactionStyleSubscription.unsubscribe();
rowStateSubscription.unsubscribe();
+ expandedPanelStyleSubscription.unsubscribe();
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -169,6 +201,11 @@ export const GridRow = forwardRef<
interactionStart={(type, e) => {
e.preventDefault();
e.stopPropagation();
+
+ // Disable interactions when a panel is expanded
+ const isInteractive = gridLayoutStateManager.expandedPanelId$.value === undefined;
+ if (!isInteractive) return;
+
const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
if (!panelRef) return;
@@ -201,25 +238,13 @@ export const GridRow = forwardRef<
}, [panelIds, rowIndex, gridLayoutStateManager, renderPanelContents, setInteractionEvent]);
return (
- <>
+
{rowIndex !== 0 && (
- <>
-
-
-
-
- {rowTitle}
-
-
-
- >
+
)}
{!isCollapsed && (
)}
- >
+
);
}
);
+
+const GridRowHeader = ({
+ isCollapsed,
+ toggleIsCollapsed,
+ rowTitle,
+}: {
+ isCollapsed: boolean;
+ toggleIsCollapsed: () => void;
+ rowTitle?: string;
+}) => {
+ return (
+ <>
+
+
+
+
+ {rowTitle}
+
+
+
+ >
+ );
+};
diff --git a/packages/kbn-grid-layout/grid/types.ts b/packages/kbn-grid-layout/grid/types.ts
index 3979b86f05a09..cd24855d07646 100644
--- a/packages/kbn-grid-layout/grid/types.ts
+++ b/packages/kbn-grid-layout/grid/types.ts
@@ -58,6 +58,8 @@ export interface ActivePanel {
export interface GridLayoutStateManager {
gridLayout$: BehaviorSubject
;
+ expandedPanelId$: BehaviorSubject;
+ isMobileView$: BehaviorSubject;
gridDimensions$: BehaviorSubject;
runtimeSettings$: BehaviorSubject;
@@ -117,3 +119,5 @@ export interface PanelPlacementSettings {
height: number;
width: number;
}
+
+export type GridAccessMode = 'VIEW' | 'EDIT';
diff --git a/packages/kbn-grid-layout/grid/use_grid_layout_state.ts b/packages/kbn-grid-layout/grid/use_grid_layout_state.ts
index a107cbacef2f2..8617262829c48 100644
--- a/packages/kbn-grid-layout/grid/use_grid_layout_state.ts
+++ b/packages/kbn-grid-layout/grid/use_grid_layout_state.ts
@@ -8,25 +8,30 @@
*/
import { useEffect, useMemo, useRef } from 'react';
-import { BehaviorSubject, debounceTime } from 'rxjs';
-
+import { BehaviorSubject, combineLatest, debounceTime } from 'rxjs';
import useResizeObserver, { type ObservedSize } from 'use-resize-observer/polyfilled';
import {
ActivePanel,
+ GridAccessMode,
GridLayoutData,
GridLayoutStateManager,
GridSettings,
PanelInteractionEvent,
RuntimeGridSettings,
} from './types';
+import { shouldShowMobileView } from './utils/mobile_view';
export const useGridLayoutState = ({
layout,
gridSettings,
+ expandedPanelId,
+ accessMode,
}: {
layout: GridLayoutData;
gridSettings: GridSettings;
+ expandedPanelId?: string;
+ accessMode: GridAccessMode;
}): {
gridLayoutStateManager: GridLayoutStateManager;
setDimensionsRef: (instance: HTMLDivElement | null) => void;
@@ -34,6 +39,25 @@ export const useGridLayoutState = ({
const rowRefs = useRef>([]);
const panelRefs = useRef>([]);
+ const expandedPanelId$ = useMemo(
+ () => new BehaviorSubject(expandedPanelId),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ []
+ );
+ useEffect(() => {
+ expandedPanelId$.next(expandedPanelId);
+ }, [expandedPanelId, expandedPanelId$]);
+
+ const accessMode$ = useMemo(
+ () => new BehaviorSubject(accessMode),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ []
+ );
+
+ useEffect(() => {
+ accessMode$.next(accessMode);
+ }, [accessMode, accessMode$]);
+
const gridLayoutStateManager = useMemo(() => {
const gridLayout$ = new BehaviorSubject(layout);
const gridDimensions$ = new BehaviorSubject({ width: 0, height: 0 });
@@ -56,6 +80,8 @@ export const useGridLayoutState = ({
gridDimensions$,
runtimeSettings$,
interactionEvent$,
+ expandedPanelId$,
+ isMobileView$: new BehaviorSubject(shouldShowMobileView(accessMode)),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -64,15 +90,16 @@ export const useGridLayoutState = ({
/**
* debounce width changes to avoid unnecessary column width recalculation.
*/
- const resizeSubscription = gridLayoutStateManager.gridDimensions$
+ const resizeSubscription = combineLatest([gridLayoutStateManager.gridDimensions$, accessMode$])
.pipe(debounceTime(250))
- .subscribe((dimensions) => {
+ .subscribe(([dimensions, currentAccessMode]) => {
const elementWidth = dimensions.width ?? 0;
const columnPixelWidth =
(elementWidth - gridSettings.gutterSize * (gridSettings.columnCount - 1)) /
gridSettings.columnCount;
gridLayoutStateManager.runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
+ gridLayoutStateManager.isMobileView$.next(shouldShowMobileView(currentAccessMode));
});
return () => {
diff --git a/packages/kbn-grid-layout/grid/utils/mobile_view.ts b/packages/kbn-grid-layout/grid/utils/mobile_view.ts
new file mode 100644
index 0000000000000..863e5ee7c0e37
--- /dev/null
+++ b/packages/kbn-grid-layout/grid/utils/mobile_view.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { euiThemeVars } from '@kbn/ui-theme';
+import { GridAccessMode } from '../types';
+
+const getViewportWidth = () =>
+ window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
+
+export const shouldShowMobileView = (accessMode: GridAccessMode) =>
+ accessMode === 'VIEW' && getViewportWidth() < parseFloat(euiThemeVars.euiBreakpoints.m);
diff --git a/packages/kbn-grid-layout/grid/utils/resolve_grid_row.ts b/packages/kbn-grid-layout/grid/utils/resolve_grid_row.ts
index 29374a22356c7..9a6f28d006e0a 100644
--- a/packages/kbn-grid-layout/grid/utils/resolve_grid_row.ts
+++ b/packages/kbn-grid-layout/grid/utils/resolve_grid_row.ts
@@ -34,7 +34,7 @@ const getAllCollisionsWithPanel = (
return collidingPanels;
};
-const getKeysInOrder = (rowData: GridRowData, draggedId?: string): string[] => {
+export const getKeysInOrder = (rowData: GridRowData, draggedId?: string): string[] => {
const panelKeys = Object.keys(rowData.panels);
return panelKeys.sort((panelKeyA, panelKeyB) => {
const panelA = rowData.panels[panelKeyA];
diff --git a/packages/kbn-grid-layout/index.ts b/packages/kbn-grid-layout/index.ts
index be46f9d5a7b88..dc40cba5c7c92 100644
--- a/packages/kbn-grid-layout/index.ts
+++ b/packages/kbn-grid-layout/index.ts
@@ -8,6 +8,12 @@
*/
export { GridLayout } from './grid/grid_layout';
-export type { GridLayoutData, GridPanelData, GridRowData, GridSettings } from './grid/types';
+export type {
+ GridLayoutData,
+ GridPanelData,
+ GridRowData,
+ GridSettings,
+ GridAccessMode,
+} from './grid/types';
export { isLayoutEqual } from './grid/utils/equality_checks';