mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Collapsable Panels] Better Scrolling for kbn grid layout (#191120)
changes `kbn grid layout` to allow better scrolling behaviour.
This commit is contained in:
parent
7d632c5c8b
commit
b02f6dbcc5
11 changed files with 320 additions and 170 deletions
|
@ -33,6 +33,7 @@
|
|||
"presentationPanel": "src/plugins/presentation_panel",
|
||||
"embeddableExamples": "examples/embeddable_examples",
|
||||
"esQuery": "packages/kbn-es-query/src",
|
||||
"kbnGridLayout": "packages/kbn-grid-layout",
|
||||
"esUi": "src/plugins/es_ui_shared",
|
||||
"expandableFlyout": "packages/kbn-expandable-flyout",
|
||||
"expressionError": "src/plugins/expression_error",
|
||||
|
|
|
@ -15,9 +15,9 @@ import { EuiPageTemplate, EuiProvider } from '@elastic/eui';
|
|||
export const GridExample = () => {
|
||||
return (
|
||||
<EuiProvider>
|
||||
<EuiPageTemplate offset={0} restrictWidth={false}>
|
||||
<EuiPageTemplate grow={false} offset={0} restrictWidth={false}>
|
||||
<EuiPageTemplate.Header iconType={'dashboardApp'} pageTitle="Grid Layout Example" />
|
||||
<EuiPageTemplate.Section>
|
||||
<EuiPageTemplate.Section color="subdued">
|
||||
<GridLayout
|
||||
renderPanelContents={(id) => {
|
||||
return <div style={{ padding: 8 }}>{id}</div>;
|
||||
|
@ -41,7 +41,7 @@ export const GridExample = () => {
|
|||
{
|
||||
title: 'Small section',
|
||||
isCollapsed: false,
|
||||
panels: { panel9: { column: 0, row: 0, width: 12, height: 6, id: 'panel9' } },
|
||||
panels: { panel9: { column: 0, row: 0, width: 12, height: 16, id: 'panel9' } },
|
||||
},
|
||||
{
|
||||
title: 'Another small section',
|
||||
|
|
56
packages/kbn-grid-layout/grid/grid_height_smoother.tsx
Normal file
56
packages/kbn-grid-layout/grid/grid_height_smoother.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import React, { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { GridLayoutStateManager } from './types';
|
||||
|
||||
export const GridHeightSmoother = ({
|
||||
children,
|
||||
gridLayoutStateManager,
|
||||
}: PropsWithChildren<{ gridLayoutStateManager: GridLayoutStateManager }>) => {
|
||||
// set the parent div size directly to smooth out height changes.
|
||||
const smoothHeightRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
const subscription = combineLatest([
|
||||
gridLayoutStateManager.gridDimensions$,
|
||||
gridLayoutStateManager.interactionEvent$,
|
||||
]).subscribe(([dimensions, interactionEvent]) => {
|
||||
if (!smoothHeightRef.current) return;
|
||||
if (!interactionEvent) {
|
||||
smoothHeightRef.current.style.height = `${dimensions.height}px`;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user is interacting with an element, the page can grow, but it cannot
|
||||
* shrink. This is to stop a behaviour where the page would scroll up automatically
|
||||
* making the panel shrink or grow unpredictably.
|
||||
*/
|
||||
smoothHeightRef.current.style.height = `${Math.max(
|
||||
dimensions.height ?? 0,
|
||||
smoothHeightRef.current.getBoundingClientRect().height
|
||||
)}px`;
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={smoothHeightRef}
|
||||
css={css`
|
||||
overflow-anchor: none;
|
||||
transition: height 500ms linear;
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -6,11 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiPortal, transparentize } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import React from 'react';
|
||||
import { GridHeightSmoother } from './grid_height_smoother';
|
||||
import { GridOverlay } from './grid_overlay';
|
||||
import { GridRow } from './grid_row';
|
||||
import { GridLayoutData, GridSettings } from './types';
|
||||
import { useGridLayoutEvents } from './use_grid_layout_events';
|
||||
|
@ -23,7 +22,7 @@ export const GridLayout = ({
|
|||
getCreationOptions: () => { initialLayout: GridLayoutData; gridSettings: GridSettings };
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
}) => {
|
||||
const { gridLayoutStateManager, gridSizeRef } = useGridLayoutState({
|
||||
const { gridLayoutStateManager, setDimensionsRef } = useGridLayoutState({
|
||||
getCreationOptions,
|
||||
});
|
||||
useGridLayoutEvents({ gridLayoutStateManager });
|
||||
|
@ -35,58 +34,44 @@ export const GridLayout = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div ref={gridSizeRef}>
|
||||
{gridLayout.map((rowData, rowIndex) => {
|
||||
return (
|
||||
<GridRow
|
||||
rowData={rowData}
|
||||
key={rowData.title}
|
||||
rowIndex={rowIndex}
|
||||
runtimeSettings={runtimeSettings}
|
||||
activePanelId={interactionEvent?.id}
|
||||
renderPanelContents={renderPanelContents}
|
||||
targetRowIndex={interactionEvent?.targetRowIndex}
|
||||
toggleIsCollapsed={() => {
|
||||
const currentLayout = gridLayoutStateManager.gridLayout$.value;
|
||||
currentLayout[rowIndex].isCollapsed = !currentLayout[rowIndex].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(currentLayout);
|
||||
}}
|
||||
setInteractionEvent={(nextInteractionEvent) => {
|
||||
if (!nextInteractionEvent) {
|
||||
gridLayoutStateManager.hideDragPreview();
|
||||
}
|
||||
gridLayoutStateManager.interactionEvent$.next(nextInteractionEvent);
|
||||
}}
|
||||
ref={(element) => (gridLayoutStateManager.rowRefs.current[rowIndex] = element)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<EuiPortal>
|
||||
<>
|
||||
<GridHeightSmoother gridLayoutStateManager={gridLayoutStateManager}>
|
||||
<div
|
||||
css={css`
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: ${euiThemeVars.euiZModal};
|
||||
`}
|
||||
ref={(divElement) => {
|
||||
setDimensionsRef(divElement);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={gridLayoutStateManager.dragPreviewRef}
|
||||
css={css`
|
||||
pointer-events: none;
|
||||
z-index: ${euiThemeVars.euiZModal};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius};
|
||||
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.2)};
|
||||
transition: opacity 100ms linear;
|
||||
position: absolute;
|
||||
`}
|
||||
/>
|
||||
{gridLayout.map((rowData, rowIndex) => {
|
||||
return (
|
||||
<GridRow
|
||||
rowData={rowData}
|
||||
key={rowData.title}
|
||||
rowIndex={rowIndex}
|
||||
runtimeSettings={runtimeSettings}
|
||||
activePanelId={interactionEvent?.id}
|
||||
renderPanelContents={renderPanelContents}
|
||||
targetRowIndex={interactionEvent?.targetRowIndex}
|
||||
toggleIsCollapsed={() => {
|
||||
const currentLayout = gridLayoutStateManager.gridLayout$.value;
|
||||
currentLayout[rowIndex].isCollapsed = !currentLayout[rowIndex].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(currentLayout);
|
||||
}}
|
||||
setInteractionEvent={(nextInteractionEvent) => {
|
||||
if (!nextInteractionEvent) {
|
||||
gridLayoutStateManager.hideDragPreview();
|
||||
}
|
||||
gridLayoutStateManager.interactionEvent$.next(nextInteractionEvent);
|
||||
}}
|
||||
ref={(element) => (gridLayoutStateManager.rowRefs.current[rowIndex] = element)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</EuiPortal>
|
||||
</div>
|
||||
</GridHeightSmoother>
|
||||
<GridOverlay
|
||||
interactionEvent={interactionEvent}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
134
packages/kbn-grid-layout/grid/grid_overlay.tsx
Normal file
134
packages/kbn-grid-layout/grid/grid_overlay.tsx
Normal file
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiPortal, EuiText, transparentize } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { GridLayoutStateManager, PanelInteractionEvent } from './types';
|
||||
|
||||
type ScrollDirection = 'up' | 'down';
|
||||
|
||||
const scrollLabels: { [key in ScrollDirection]: string } = {
|
||||
up: i18n.translate('kbnGridLayout.overlays.scrollUpLabel', { defaultMessage: 'Scroll up' }),
|
||||
down: i18n.translate('kbnGridLayout.overlays.scrollDownLabel', { defaultMessage: 'Scroll down' }),
|
||||
};
|
||||
|
||||
const scrollOnInterval = (direction: ScrollDirection) => {
|
||||
const interval = setInterval(() => {
|
||||
window.scroll({
|
||||
top: window.scrollY + (direction === 'down' ? 50 : -50),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 100);
|
||||
return interval;
|
||||
};
|
||||
|
||||
const ScrollOnHover = ({ direction, hide }: { hide: boolean; direction: ScrollDirection }) => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const scrollInterval = useRef<NodeJS.Timeout | null>(null);
|
||||
const stopScrollInterval = () => {
|
||||
if (scrollInterval.current) {
|
||||
clearInterval(scrollInterval.current);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
setIsActive(true);
|
||||
scrollInterval.current = scrollOnInterval(direction);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsActive(false);
|
||||
stopScrollInterval();
|
||||
}}
|
||||
css={css`
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
opacity: ${hide ? 0 : 1};
|
||||
transition: opacity 100ms linear;
|
||||
padding: ${euiThemeVars.euiSizeM};
|
||||
${direction === 'down' ? 'bottom: 0;' : 'top: 0;'}
|
||||
`}
|
||||
>
|
||||
{direction === 'up' && (
|
||||
<div
|
||||
css={css`
|
||||
height: 96px;
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
background-color: ${isActive
|
||||
? euiThemeVars.euiColorSuccess
|
||||
: euiThemeVars.euiColorEmptyShade};
|
||||
height: ${euiThemeVars.euiButtonHeight};
|
||||
line-height: ${euiThemeVars.euiButtonHeight};
|
||||
border-radius: ${euiThemeVars.euiButtonHeight};
|
||||
outline: ${isActive ? 'none' : euiThemeVars.euiBorderThin};
|
||||
transition: background-color 100ms linear, color 100ms linear;
|
||||
padding: 0 ${euiThemeVars.euiSizeL} 0 ${euiThemeVars.euiSizeL};
|
||||
color: ${isActive ? euiThemeVars.euiColorEmptyShade : euiThemeVars.euiTextColor};
|
||||
`}
|
||||
>
|
||||
<EuiText size="m">
|
||||
<strong>{scrollLabels[direction]}</strong>
|
||||
</EuiText>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GridOverlay = ({
|
||||
interactionEvent,
|
||||
gridLayoutStateManager,
|
||||
}: {
|
||||
interactionEvent?: PanelInteractionEvent;
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}) => {
|
||||
return (
|
||||
<EuiPortal>
|
||||
<div
|
||||
css={css`
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
z-index: ${euiThemeVars.euiZModal};
|
||||
pointer-events: ${interactionEvent ? 'unset' : 'none'};
|
||||
`}
|
||||
>
|
||||
<ScrollOnHover hide={!interactionEvent} direction="up" />
|
||||
<ScrollOnHover hide={!interactionEvent} direction="down" />
|
||||
<div
|
||||
ref={gridLayoutStateManager.dragPreviewRef}
|
||||
css={css`
|
||||
pointer-events: none;
|
||||
z-index: ${euiThemeVars.euiZModal};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius};
|
||||
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.2)};
|
||||
transition: opacity 100ms linear;
|
||||
position: absolute;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
|
@ -30,15 +30,13 @@ export const GridPanel = ({
|
|||
setInteractionEvent: (interactionData?: Omit<PanelInteractionEvent, 'targetRowIndex'>) => void;
|
||||
}) => {
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const ghostRef = useRef<HTMLDivElement>(null);
|
||||
const thisPanelActive = activePanelId === panelData.id;
|
||||
|
||||
const interactionStart = useCallback(
|
||||
(type: 'drag' | 'resize', e: React.DragEvent) => {
|
||||
if (!panelRef.current || !ghostRef.current) return;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
e.dataTransfer.setDragImage(ghostRef.current, 0, 0);
|
||||
(type: 'drag' | 'resize', e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (!panelRef.current) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const panelRect = panelRef.current.getBoundingClientRect();
|
||||
setInteractionEvent({
|
||||
type,
|
||||
|
@ -83,22 +81,8 @@ export const GridPanel = ({
|
|||
}
|
||||
`}
|
||||
>
|
||||
{/* Hidden dragging ghost */}
|
||||
<div
|
||||
css={css`
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
`}
|
||||
ref={ghostRef}
|
||||
/>
|
||||
{/* drag handle */}
|
||||
<div
|
||||
draggable="true"
|
||||
className="dragHandle"
|
||||
css={css`
|
||||
opacity: 0;
|
||||
|
@ -116,15 +100,14 @@ export const GridPanel = ({
|
|||
background-color: ${euiThemeVars.euiColorEmptyShade};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius} ${euiThemeVars.euiBorderRadius} 0 0;
|
||||
`}
|
||||
onDragStart={(e: React.DragEvent<HTMLDivElement>) => interactionStart('drag', e)}
|
||||
onMouseDown={(e) => interactionStart('drag', e)}
|
||||
>
|
||||
<EuiIcon type="grabOmnidirectional" />
|
||||
</div>
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
draggable="true"
|
||||
className="resizeHandle"
|
||||
onDragStart={(e) => interactionStart('resize', e)}
|
||||
onMouseDown={(e) => interactionStart('resize', e)}
|
||||
css={css`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle, transparentize } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import React, { forwardRef, useMemo } from 'react';
|
||||
import { GridPanel } from './grid_panel';
|
||||
|
@ -69,6 +70,9 @@ export const GridRow = forwardRef<
|
|||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
aria-label={i18n.translate('kbnGridLayout.row.toggleCollapse', {
|
||||
defaultMessage: 'Toggle collapse',
|
||||
})}
|
||||
iconType={rowData.isCollapsed ? 'arrowRight' : 'arrowDown'}
|
||||
onClick={toggleIsCollapsed}
|
||||
/>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { ObservedSize } from 'use-resize-observer/polyfilled';
|
||||
export interface GridCoordinate {
|
||||
column: number;
|
||||
row: number;
|
||||
|
@ -53,6 +54,7 @@ export interface GridLayoutStateManager {
|
|||
right: number;
|
||||
}) => void;
|
||||
|
||||
gridDimensions$: BehaviorSubject<ObservedSize>;
|
||||
gridLayout$: BehaviorSubject<GridLayoutData>;
|
||||
runtimeSettings$: BehaviorSubject<RuntimeGridSettings>;
|
||||
rowRefs: React.MutableRefObject<Array<HTMLDivElement | null>>;
|
||||
|
|
|
@ -25,7 +25,7 @@ export const useGridLayoutEvents = ({
|
|||
}: {
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}) => {
|
||||
const dragEnterCount = useRef(0);
|
||||
const mouseClientPosition = useRef({ x: 0, y: 0 });
|
||||
const lastRequestedPanelPosition = useRef<GridPanelData | undefined>(undefined);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
|
@ -33,7 +33,8 @@ export const useGridLayoutEvents = ({
|
|||
// -----------------------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const { runtimeSettings$, interactionEvent$, gridLayout$ } = gridLayoutStateManager;
|
||||
const dragOver = (e: MouseEvent) => {
|
||||
const calculateUserEvent = (e: Event) => {
|
||||
if (!interactionEvent$.value) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -51,17 +52,14 @@ export const useGridLayoutEvents = ({
|
|||
}
|
||||
})();
|
||||
|
||||
if (
|
||||
!runtimeSettings$.value ||
|
||||
!interactionEvent ||
|
||||
!previewElement ||
|
||||
!gridRowElements ||
|
||||
!currentGridData
|
||||
) {
|
||||
if (!runtimeSettings$.value || !previewElement || !gridRowElements || !currentGridData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mouseTargetPixel = { x: e.clientX, y: e.clientY };
|
||||
const mouseTargetPixel = {
|
||||
x: mouseClientPosition.current.x,
|
||||
y: mouseClientPosition.current.y,
|
||||
};
|
||||
const panelRect = interactionEvent.panelDiv.getBoundingClientRect();
|
||||
const previewRect = {
|
||||
left: isResize ? panelRect.left : mouseTargetPixel.x - interactionEvent.mouseOffsets.left,
|
||||
|
@ -159,46 +157,27 @@ export const useGridLayoutEvents = ({
|
|||
}
|
||||
};
|
||||
|
||||
const onDrop = (e: MouseEvent) => {
|
||||
const onMouseUp = (e: MouseEvent) => {
|
||||
if (!interactionEvent$.value) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!interactionEvent$.value) return;
|
||||
|
||||
interactionEvent$.next(undefined);
|
||||
gridLayoutStateManager.hideDragPreview();
|
||||
dragEnterCount.current = 0;
|
||||
};
|
||||
|
||||
const onDragEnter = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!interactionEvent$.value) return;
|
||||
|
||||
dragEnterCount.current++;
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
mouseClientPosition.current = { x: e.clientX, y: e.clientY };
|
||||
calculateUserEvent(e);
|
||||
};
|
||||
|
||||
const onDragLeave = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!interactionEvent$.value) return;
|
||||
|
||||
dragEnterCount.current--;
|
||||
if (dragEnterCount.current === 0) {
|
||||
interactionEvent$.next(undefined);
|
||||
gridLayoutStateManager.hideDragPreview();
|
||||
dragEnterCount.current = 0;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('drop', onDrop);
|
||||
window.addEventListener('dragover', dragOver);
|
||||
window.addEventListener('dragenter', onDragEnter);
|
||||
window.addEventListener('dragleave', onDragLeave);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('scroll', calculateUserEvent);
|
||||
return () => {
|
||||
window.removeEventListener('drop', dragOver);
|
||||
window.removeEventListener('dragover', dragOver);
|
||||
window.removeEventListener('dragenter', onDragEnter);
|
||||
window.removeEventListener('dragleave', onDragLeave);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('scroll', calculateUserEvent);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import useResizeObserver from 'use-resize-observer/polyfilled';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { BehaviorSubject, debounceTime } from 'rxjs';
|
||||
import useResizeObserver, { type ObservedSize } from 'use-resize-observer/polyfilled';
|
||||
import {
|
||||
GridLayoutData,
|
||||
GridLayoutStateManager,
|
||||
|
@ -24,71 +23,77 @@ export const useGridLayoutState = ({
|
|||
getCreationOptions: () => { initialLayout: GridLayoutData; gridSettings: GridSettings };
|
||||
}): {
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
gridSizeRef: (instance: HTMLDivElement | null) => void;
|
||||
setDimensionsRef: (instance: HTMLDivElement | null) => void;
|
||||
} => {
|
||||
const rowRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { gridLayoutStateManager, onWidthChange } = useMemo(() => {
|
||||
const { initialLayout, gridSettings } = getCreationOptions();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const { initialLayout, gridSettings } = useMemo(() => getCreationOptions(), []);
|
||||
|
||||
const gridLayoutStateManager = useMemo(() => {
|
||||
const gridLayout$ = new BehaviorSubject<GridLayoutData>(initialLayout);
|
||||
const gridDimensions$ = new BehaviorSubject<ObservedSize>({ width: 0, height: 0 });
|
||||
const interactionEvent$ = new BehaviorSubject<PanelInteractionEvent | undefined>(undefined);
|
||||
const runtimeSettings$ = new BehaviorSubject<RuntimeGridSettings>({
|
||||
...gridSettings,
|
||||
columnPixelWidth: 0,
|
||||
});
|
||||
|
||||
// debounce width changes to avoid re-rendering too frequently when the browser is resizing
|
||||
const widthChange = debounce((elementWidth: number) => {
|
||||
const columnPixelWidth =
|
||||
(elementWidth - gridSettings.gutterSize * (gridSettings.columnCount - 1)) /
|
||||
gridSettings.columnCount;
|
||||
runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
|
||||
}, 250);
|
||||
|
||||
return {
|
||||
gridLayoutStateManager: {
|
||||
rowRefs,
|
||||
gridLayout$,
|
||||
dragPreviewRef,
|
||||
runtimeSettings$,
|
||||
interactionEvent$,
|
||||
updatePreviewElement: (previewRect: {
|
||||
top: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
right: number;
|
||||
}) => {
|
||||
if (!dragPreviewRef.current) return;
|
||||
dragPreviewRef.current.style.opacity = '1';
|
||||
dragPreviewRef.current.style.left = `${previewRect.left}px`;
|
||||
dragPreviewRef.current.style.top = `${previewRect.top}px`;
|
||||
dragPreviewRef.current.style.width = `${Math.max(
|
||||
previewRect.right - previewRect.left,
|
||||
runtimeSettings$.value.columnPixelWidth
|
||||
)}px`;
|
||||
dragPreviewRef.current.style.height = `${Math.max(
|
||||
previewRect.bottom - previewRect.top,
|
||||
runtimeSettings$.value.rowHeight
|
||||
)}px`;
|
||||
},
|
||||
hideDragPreview: () => {
|
||||
if (!dragPreviewRef.current) return;
|
||||
dragPreviewRef.current.style.opacity = '0';
|
||||
},
|
||||
rowRefs,
|
||||
gridLayout$,
|
||||
dragPreviewRef,
|
||||
gridDimensions$,
|
||||
runtimeSettings$,
|
||||
interactionEvent$,
|
||||
updatePreviewElement: (previewRect: {
|
||||
top: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
right: number;
|
||||
}) => {
|
||||
if (!dragPreviewRef.current) return;
|
||||
dragPreviewRef.current.style.opacity = '1';
|
||||
dragPreviewRef.current.style.left = `${previewRect.left}px`;
|
||||
dragPreviewRef.current.style.top = `${previewRect.top}px`;
|
||||
dragPreviewRef.current.style.width = `${Math.max(
|
||||
previewRect.right - previewRect.left,
|
||||
runtimeSettings$.value.columnPixelWidth
|
||||
)}px`;
|
||||
dragPreviewRef.current.style.height = `${Math.max(
|
||||
previewRect.bottom - previewRect.top,
|
||||
runtimeSettings$.value.rowHeight
|
||||
)}px`;
|
||||
},
|
||||
hideDragPreview: () => {
|
||||
if (!dragPreviewRef.current) return;
|
||||
dragPreviewRef.current.style.opacity = '0';
|
||||
},
|
||||
onWidthChange: widthChange,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { ref: gridSizeRef } = useResizeObserver<HTMLDivElement>({
|
||||
useEffect(() => {
|
||||
// debounce width changes to avoid unnecessary column width recalculation.
|
||||
const subscription = gridLayoutStateManager.gridDimensions$
|
||||
.pipe(debounceTime(250))
|
||||
.subscribe((dimensions) => {
|
||||
const elementWidth = dimensions.width ?? 0;
|
||||
const columnPixelWidth =
|
||||
(elementWidth - gridSettings.gutterSize * (gridSettings.columnCount - 1)) /
|
||||
gridSettings.columnCount;
|
||||
gridLayoutStateManager.runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { ref: setDimensionsRef } = useResizeObserver<HTMLDivElement>({
|
||||
onResize: (dimensions) => {
|
||||
if (dimensions.width) {
|
||||
onWidthChange(dimensions.width);
|
||||
}
|
||||
gridLayoutStateManager.gridDimensions$.next(dimensions);
|
||||
},
|
||||
});
|
||||
|
||||
return { gridLayoutStateManager, gridSizeRef };
|
||||
return { gridLayoutStateManager, setDimensionsRef };
|
||||
};
|
||||
|
|
|
@ -19,5 +19,6 @@
|
|||
"kbn_references": [
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/i18n",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue