From feada584f2f674fab7673fa736f3206c7b0f8336 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 20 Nov 2024 15:09:55 +0100 Subject: [PATCH] responsive layout --- examples/grid_example/public/app.tsx | 32 ++++ packages/kbn-grid-layout/grid/grid_layout.tsx | 19 +- packages/kbn-grid-layout/grid/grid_panel.tsx | 164 ++++++++++-------- packages/kbn-grid-layout/grid/types.ts | 1 + .../grid/use_grid_layout_state.ts | 16 ++ .../grid/utils/resolve_grid_row.ts | 2 +- 6 files changed, 162 insertions(+), 72 deletions(-) diff --git a/examples/grid_example/public/app.tsx b/examples/grid_example/public/app.tsx index 85b762db85394..6ecadfa6901a9 100644 --- a/examples/grid_example/public/app.tsx +++ b/examples/grid_example/public/app.tsx @@ -16,6 +16,7 @@ import { EuiBadge, EuiButton, EuiButtonEmpty, + EuiButtonGroup, EuiButtonIcon, EuiCallOut, EuiFlexGroup, @@ -50,6 +51,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => { const [gridLayoutApi, setGridLayoutApi] = useState(); const savedLayout = useRef(getSerializedGridLayout()); const currentLayout = useRef(savedLayout.current); + const [isResponsive, setIsResponsive] = useState(true); return ( @@ -104,6 +106,34 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => { + + { + setIsResponsive(id === 'responsive'); + }} + /> + {hasUnsavedChanges && ( @@ -126,6 +156,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => { })} + { @@ -147,6 +178,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => { { currentLayout.current = cloneDeep(newLayout); diff --git a/packages/kbn-grid-layout/grid/grid_layout.tsx b/packages/kbn-grid-layout/grid/grid_layout.tsx index b5bcb5bb99054..d4db19b80397e 100644 --- a/packages/kbn-grid-layout/grid/grid_layout.tsx +++ b/packages/kbn-grid-layout/grid/grid_layout.tsx @@ -33,10 +33,14 @@ interface GridLayoutProps { renderPanelContents: (panelId: string) => React.ReactNode; onLayoutChange: (newLayout: GridLayoutData) => void; expandedPanelId?: string; + isResponsive?: boolean; } export const GridLayout = forwardRef( - ({ getCreationOptions, renderPanelContents, onLayoutChange, expandedPanelId }, ref) => { + ( + { getCreationOptions, renderPanelContents, onLayoutChange, expandedPanelId, isResponsive }, + ref + ) => { const expandedPanelId$ = useMemo( () => new BehaviorSubject(expandedPanelId), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -46,9 +50,20 @@ export const GridLayout = forwardRef( expandedPanelId$.next(expandedPanelId); }, [expandedPanelId, expandedPanelId$]); + const isResponsive$ = useMemo( + () => new BehaviorSubject(Boolean(isResponsive)), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + useEffect(() => { + isResponsive$.next(Boolean(isResponsive)); + }, [isResponsive, isResponsive$]); + const { gridLayoutStateManager, setDimensionsRef } = useGridLayoutState({ getCreationOptions, expandedPanelId$, + isResponsive$, }); useGridLayoutEvents({ gridLayoutStateManager }); @@ -99,7 +114,7 @@ export const GridLayout = forwardRef( }, []); const gridClassNames = classNames('kbnGrid', { - 'kbnGrid--nonInteractive': expandedPanelId, + 'kbnGrid--nonInteractive': expandedPanelId || isResponsive, }); return ( diff --git a/packages/kbn-grid-layout/grid/grid_panel.tsx b/packages/kbn-grid-layout/grid/grid_panel.tsx index 6bc46526722e9..9db95afefdb88 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, @@ -44,7 +45,11 @@ export const GridPanel = forwardRef< /** Set initial styles based on state at mount to prevent styles from "blipping" */ const initialStyles = useMemo(() => { - const initialPanel = gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels[panelId]; + const gridLayout = gridLayoutStateManager.gridLayout$.getValue(); + + const allPanels = gridLayout[rowIndex].panels; + const initialPanel = allPanels[panelId]; + return css` grid-column-start: ${initialPanel.column + 1}; grid-column-end: ${initialPanel.column + 1 + initialPanel.width}; @@ -61,81 +66,102 @@ export const GridPanel = forwardRef< gridLayoutStateManager.gridLayout$, gridLayoutStateManager.runtimeSettings$, gridLayoutStateManager.expandedPanelId$, + gridLayoutStateManager.isMobileView$, ]) .pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it - .subscribe(([activePanel, gridLayout, runtimeSettings, expandedPanelId]) => { - const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId]; - const panel = gridLayout[rowIndex].panels[panelId]; - if (!ref || !panel) return; - if (expandedPanelId && expandedPanelId === panelId) { - // adding the class to target the handles in css and disable them - ref.classList.add('kbnGridPanel--nonInteractive'); - ref.style.transform = 'translate(9999px, 9999px)'; - ref.style.gridArea = '1 / 1 / -1 / -1'; - ref.style.position = `absolute`; - ref.style.top = `0`; - ref.style.left = `0`; - ref.style.width = `100%`; - ref.style.height = `100%`; - return; - } else { - ref.classList.remove('kbnGridPanel--nonInteractive'); - ref.style.transform = ''; - } + .subscribe( + ([activePanel, gridLayout, runtimeSettings, expandedPanelId, isMobileView]) => { + const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId]; + const allPanels = gridLayout[rowIndex].panels; + const panel = allPanels[panelId]; + + if (!ref || !panel) return; + if (expandedPanelId && expandedPanelId === panelId) { + ref.style.transform = 'translate(9999px, 9999px)'; + // ref.style.gridArea = '1 / 1 / -1 / -1'; + ref.style.position = `absolute`; + ref.style.top = `0`; + ref.style.left = `0`; + ref.style.width = `100%`; + ref.style.height = `100%`; + return; + } else { + ref.style.transform = ''; + } + + 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 + ); - 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; - - ref.style.zIndex = `${euiThemeVars.euiZModal}`; - if (currentInteractionEvent?.type === 'resize') { - // if the current panel is being resized, ensure it is not shrunk past the size of a single cell - ref.style.width = `${Math.max( - draggingPosition.right - draggingPosition.left, - runtimeSettings.columnPixelWidth - )}px`; - ref.style.height = `${Math.max( - draggingPosition.bottom - draggingPosition.top, - runtimeSettings.rowHeight - )}px`; - - // undo any "lock to grid" styles **except** for the top left corner, which stays locked + 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; + + ref.style.zIndex = `${euiThemeVars.euiZModal}`; + if (currentInteractionEvent?.type === 'resize') { + // if the current panel is being resized, ensure it is not shrunk past the size of a single cell + ref.style.width = `${Math.max( + draggingPosition.right - draggingPosition.left, + runtimeSettings.columnPixelWidth + )}px`; + ref.style.height = `${Math.max( + draggingPosition.bottom - draggingPosition.top, + runtimeSettings.rowHeight + )}px`; + + // undo any "lock to grid" styles **except** for the top left corner, which stays locked + ref.style.gridColumnStart = `${panel.column + 1}`; + ref.style.gridRowStart = `${panel.row + 1}`; + ref.style.gridColumnEnd = ``; + ref.style.gridRowEnd = ``; + } else { + // if the current panel is being dragged, render it with a fixed position + size + ref.style.position = 'fixed'; + ref.style.left = `${draggingPosition.left}px`; + ref.style.top = `${draggingPosition.top}px`; + ref.style.width = `${draggingPosition.right - draggingPosition.left}px`; + ref.style.height = `${draggingPosition.bottom - draggingPosition.top}px`; + + // undo any "lock to grid" styles + ref.style.gridColumnStart = ``; + ref.style.gridRowStart = ``; + ref.style.gridColumnEnd = ``; + ref.style.gridRowEnd = ``; + } + } else { + ref.style.zIndex = '0'; + + // if the panel is not being dragged and/or resized, undo any fixed position styles + ref.style.position = ''; + ref.style.left = ``; + ref.style.top = ``; + ref.style.width = ``; + ref.style.height = ``; + + if (isMobileView) { + ref.style.gridColumnStart = `1`; + ref.style.gridColumnEnd = `-1`; + ref.style.gridRowStart = `${responsiveGridRowStart}`; + ref.style.gridRowEnd = `${responsiveGridRowStart + panel.height}`; + // we shouldn't allow interactions on mobile view so we can return early + return; + } + + // and render the panel locked to the grid ref.style.gridColumnStart = `${panel.column + 1}`; + ref.style.gridColumnEnd = `${panel.column + 1 + panel.width}`; ref.style.gridRowStart = `${panel.row + 1}`; - ref.style.gridColumnEnd = ``; - ref.style.gridRowEnd = ``; - } else { - // if the current panel is being dragged, render it with a fixed position + size - ref.style.position = 'fixed'; - ref.style.left = `${draggingPosition.left}px`; - ref.style.top = `${draggingPosition.top}px`; - ref.style.width = `${draggingPosition.right - draggingPosition.left}px`; - ref.style.height = `${draggingPosition.bottom - draggingPosition.top}px`; - - // undo any "lock to grid" styles - ref.style.gridColumnStart = ``; - ref.style.gridRowStart = ``; - ref.style.gridColumnEnd = ``; - ref.style.gridRowEnd = ``; + ref.style.gridRowEnd = `${panel.row + 1 + panel.height}`; } - } else { - ref.style.zIndex = '0'; - - // if the panel is not being dragged and/or resized, undo any fixed position styles - ref.style.position = ''; - ref.style.left = ``; - ref.style.top = ``; - ref.style.width = ``; - ref.style.height = ``; - - // and render the panel locked to the grid - ref.style.gridColumnStart = `${panel.column + 1}`; - ref.style.gridColumnEnd = `${panel.column + 1 + panel.width}`; - ref.style.gridRowStart = `${panel.row + 1}`; - ref.style.gridRowEnd = `${panel.row + 1 + panel.height}`; } - }); + ); return () => { styleSubscription.unsubscribe(); diff --git a/packages/kbn-grid-layout/grid/types.ts b/packages/kbn-grid-layout/grid/types.ts index fed91b69f353a..7ac77ac0bf387 100644 --- a/packages/kbn-grid-layout/grid/types.ts +++ b/packages/kbn-grid-layout/grid/types.ts @@ -60,6 +60,7 @@ export interface ActivePanel { export interface GridLayoutStateManager { gridLayout$: BehaviorSubject; expandedPanelId$: BehaviorSubject; + isMobileView$: BehaviorSubject; gridDimensions$: BehaviorSubject; runtimeSettings$: BehaviorSubject; 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 08a604eeaed34..d8b99f0886219 100644 --- a/packages/kbn-grid-layout/grid/use_grid_layout_state.ts +++ b/packages/kbn-grid-layout/grid/use_grid_layout_state.ts @@ -12,6 +12,7 @@ import { BehaviorSubject, debounceTime } from 'rxjs'; import useResizeObserver, { type ObservedSize } from 'use-resize-observer/polyfilled'; +import { euiThemeVars } from '@kbn/ui-theme'; import { ActivePanel, GridLayoutData, @@ -21,12 +22,23 @@ import { RuntimeGridSettings, } from './types'; +const getViewportWidth = () => + window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + +const shouldShowMobileView = () => getViewportWidth() < parseFloat(euiThemeVars.euiBreakpoints.m); + +const nextIsMobileView = (isMobileView$: BehaviorSubject, isResponsive: boolean) => { + isMobileView$.next(isResponsive && shouldShowMobileView()); +}; + export const useGridLayoutState = ({ getCreationOptions, expandedPanelId$, + isResponsive$, }: { getCreationOptions: () => { initialLayout: GridLayoutData; gridSettings: GridSettings }; expandedPanelId$: BehaviorSubject; + isResponsive$: BehaviorSubject; }): { gridLayoutStateManager: GridLayoutStateManager; setDimensionsRef: (instance: HTMLDivElement | null) => void; @@ -63,6 +75,9 @@ export const useGridLayoutState = ({ runtimeSettings$, interactionEvent$, expandedPanelId$, + isMobileView$: new BehaviorSubject( + isResponsive$.getValue() && shouldShowMobileView() + ), }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -80,6 +95,7 @@ export const useGridLayoutState = ({ gridSettings.columnCount; gridLayoutStateManager.runtimeSettings$.next({ ...gridSettings, columnPixelWidth }); + nextIsMobileView(gridLayoutStateManager.isMobileView$, isResponsive$.getValue()); }); return () => { 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 3037a52c27c69..ee2547df5b703 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];