Skip to content

Commit

Permalink
responsive layout
Browse files Browse the repository at this point in the history
  • Loading branch information
mbondyra committed Nov 20, 2024
1 parent 75f3dc0 commit feada58
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 72 deletions.
32 changes: 32 additions & 0 deletions examples/grid_example/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiButtonGroup,
EuiButtonIcon,
EuiCallOut,
EuiFlexGroup,
Expand Down Expand Up @@ -50,6 +51,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
const [gridLayoutApi, setGridLayoutApi] = useState<GridLayoutApi | null>();
const savedLayout = useRef<GridLayoutData>(getSerializedGridLayout());
const currentLayout = useRef<GridLayoutData>(savedLayout.current);
const [isResponsive, setIsResponsive] = useState<boolean>(true);

return (
<EuiProvider>
Expand Down Expand Up @@ -104,6 +106,34 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonGroup
legend={i18n.translate('examples.gridExample.layoutOptionsLegend', {
defaultMessage: 'Layout options',
})}
options={[
{
id: 'responsive',
label: i18n.translate('examples.gridExample.responsiveLayoutOption', {
defaultMessage: 'Responsive layout',
}),
toolTipContent:
'The layout adjusts when the window is resized. Panel interactivity, such as moving and resizing within the grid, is disabled.',
},
{
id: 'fixed',
label: i18n.translate('examples.gridExample.fixedLayoutOption', {
defaultMessage: 'Fixed layout',
}),
toolTipContent: 'The layout does not adjust when the window is resized.',
},
]}
idSelected={isResponsive ? 'responsive' : 'fixed'}
onChange={(id) => {
setIsResponsive(id === 'responsive');
}}
/>
</EuiFlexItem>
{hasUnsavedChanges && (
<EuiFlexItem grow={false}>
<EuiBadge color="warning">
Expand All @@ -126,6 +156,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
})}
</EuiButtonEmpty>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<EuiButton
onClick={() => {
Expand All @@ -147,6 +178,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
</EuiFlexGroup>
<EuiSpacer size="m" />
<GridLayout
isResponsive={isResponsive}
key={layoutKey}
onLayoutChange={(newLayout) => {
currentLayout.current = cloneDeep(newLayout);
Expand Down
19 changes: 17 additions & 2 deletions packages/kbn-grid-layout/grid/grid_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ interface GridLayoutProps {
renderPanelContents: (panelId: string) => React.ReactNode;
onLayoutChange: (newLayout: GridLayoutData) => void;
expandedPanelId?: string;
isResponsive?: boolean;
}

export const GridLayout = forwardRef<GridLayoutApi, GridLayoutProps>(
({ getCreationOptions, renderPanelContents, onLayoutChange, expandedPanelId }, ref) => {
(
{ getCreationOptions, renderPanelContents, onLayoutChange, expandedPanelId, isResponsive },
ref
) => {
const expandedPanelId$ = useMemo(
() => new BehaviorSubject<string | undefined>(expandedPanelId),
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -46,9 +50,20 @@ export const GridLayout = forwardRef<GridLayoutApi, GridLayoutProps>(
expandedPanelId$.next(expandedPanelId);
}, [expandedPanelId, expandedPanelId$]);

const isResponsive$ = useMemo(
() => new BehaviorSubject<boolean>(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 });

Expand Down Expand Up @@ -99,7 +114,7 @@ export const GridLayout = forwardRef<GridLayoutApi, GridLayoutProps>(
}, []);

const gridClassNames = classNames('kbnGrid', {
'kbnGrid--nonInteractive': expandedPanelId,
'kbnGrid--nonInteractive': expandedPanelId || isResponsive,
});

return (
Expand Down
164 changes: 95 additions & 69 deletions packages/kbn-grid-layout/grid/grid_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-grid-layout/grid/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface ActivePanel {
export interface GridLayoutStateManager {
gridLayout$: BehaviorSubject<GridLayoutData>;
expandedPanelId$: BehaviorSubject<string | undefined>;
isMobileView$: BehaviorSubject<boolean>;

gridDimensions$: BehaviorSubject<ObservedSize>;
runtimeSettings$: BehaviorSubject<RuntimeGridSettings>;
Expand Down
16 changes: 16 additions & 0 deletions packages/kbn-grid-layout/grid/use_grid_layout_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<boolean>, isResponsive: boolean) => {
isMobileView$.next(isResponsive && shouldShowMobileView());
};

export const useGridLayoutState = ({
getCreationOptions,
expandedPanelId$,
isResponsive$,
}: {
getCreationOptions: () => { initialLayout: GridLayoutData; gridSettings: GridSettings };
expandedPanelId$: BehaviorSubject<string | undefined>;
isResponsive$: BehaviorSubject<boolean>;
}): {
gridLayoutStateManager: GridLayoutStateManager;
setDimensionsRef: (instance: HTMLDivElement | null) => void;
Expand Down Expand Up @@ -63,6 +75,9 @@ export const useGridLayoutState = ({
runtimeSettings$,
interactionEvent$,
expandedPanelId$,
isMobileView$: new BehaviorSubject<boolean>(
isResponsive$.getValue() && shouldShowMobileView()
),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand All @@ -80,6 +95,7 @@ export const useGridLayoutState = ({
gridSettings.columnCount;

gridLayoutStateManager.runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
nextIsMobileView(gridLayoutStateManager.isMobileView$, isResponsive$.getValue());
});

return () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-grid-layout/grid/utils/resolve_grid_row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down

0 comments on commit feada58

Please sign in to comment.