diff --git a/examples/grid_example/public/app.tsx b/examples/grid_example/public/app.tsx index 635d116f2249a..21f05d613ad1a 100644 --- a/examples/grid_example/public/app.tsx +++ b/examples/grid_example/public/app.tsx @@ -8,7 +8,7 @@ */ import deepEqual from 'fast-deep-equal'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { combineLatest, debounceTime } from 'rxjs'; @@ -20,9 +20,15 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem, + EuiFormRow, EuiPageTemplate, + EuiPopover, + EuiRange, EuiSpacer, + transparentize, + useEuiTheme, } from '@elastic/eui'; +import { css } from '@emotion/react'; import { AppMountParameters } from '@kbn/core-application-browser'; import { CoreStart } from '@kbn/core-lifecycle-browser'; import { AddEmbeddableButton } from '@kbn/embeddable-examples-plugin/public'; @@ -53,11 +59,16 @@ export const GridExample = ({ coreStart: CoreStart; uiActions: UiActionsStart; }) => { + const { euiTheme } = useEuiTheme(); + const savedState = useRef(getSerializedDashboardState()); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [currentLayout, setCurrentLayout] = useState( dashboardInputToGridLayout(savedState.current) ); + const [isSettingsPopoverOpen, setIsSettingsPopoverOpen] = useState(false); + const [gutterSize, setGutterSize] = useState(DASHBOARD_MARGIN_SIZE); + const [rowHeight, setRowHeight] = useState(DASHBOARD_GRID_HEIGHT); const mockDashboardApi = useMockDashboardApi({ savedState: savedState.current }); const [viewMode, expandedPanelId] = useBatchedPublishingSubjects( @@ -111,6 +122,41 @@ export const GridExample = ({ [mockDashboardApi] ); + const customLayoutCss = useMemo(() => { + const gridColor = transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.2); + return css` + .kbnGridRow--targeted { + background-position: top calc((var(--kbnGridGutterSize) / 2) * -1px) left + calc((var(--kbnGridGutterSize) / 2) * -1px); + background-size: calc((var(--kbnGridColumnWidth) + var(--kbnGridGutterSize)) * 1px) + calc((var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) * 1px); + background-image: linear-gradient(to right, ${gridColor} 1px, transparent 1px), + linear-gradient(to bottom, ${gridColor} 1px, transparent 1px); + background-color: ${transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.1)}; + } + + .kbnGridPanel--dragPreview { + border-radius: ${euiTheme.border.radius}; + background-color: ${transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.2)}; + transition: opacity 100ms linear; + } + + .kbnGridPanel--resizeHandle { + opacity: 0; + transition: opacity 0.2s, border 0.2s; + border-radius: 7px 0 7px 0; + border-bottom: 2px solid ${euiTheme.colors.accentSecondary}; + border-right: 2px solid ${euiTheme.colors.accentSecondary}; + &:hover, + &:focus { + outline-style: none !important; + opacity: 1; + background-color: ${transparentize(euiTheme.colors.accentSecondary, 0.05)}; + } + } + `; + }, [euiTheme]); + return ( @@ -148,38 +194,96 @@ export const GridExample = ({ - + + + {' '} + + + setIsSettingsPopoverOpen(!isSettingsPopoverOpen)} + > + {i18n.translate('examples.gridExample.settingsPopover.title', { + defaultMessage: 'Layout settings', + })} + + } + isOpen={isSettingsPopoverOpen} + closePopover={() => setIsSettingsPopoverOpen(false)} + > + <> + + { + mockDashboardApi.viewMode.next(id); + }} + /> + + + setGutterSize(parseInt(e.currentTarget.value, 10))} + showLabels + showValue + /> + + + setRowHeight(parseInt(e.currentTarget.value, 10))} + showLabels + showValue + /> + + + + + - - { - mockDashboardApi.viewMode.next(id); - }} - /> - {hasUnsavedChanges && ( @@ -223,13 +327,14 @@ export const GridExample = ({ + diff --git a/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx b/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx index 83823bf80d64f..89e564636bc0d 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/drag_preview.tsx @@ -10,7 +10,6 @@ import React, { useEffect, useRef } from 'react'; import { combineLatest, skip } from 'rxjs'; -import { transparentize, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { GridLayoutStateManager } from './types'; @@ -23,7 +22,6 @@ export const DragPreview = ({ gridLayoutStateManager: GridLayoutStateManager; }) => { const dragPreviewRef = useRef(null); - const { euiTheme } = useEuiTheme(); useEffect( () => { @@ -59,12 +57,10 @@ export const DragPreview = ({ return (
); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_height_smoother.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_height_smoother.tsx index 1f3ebadbc26f1..8a95ea513e210 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_height_smoother.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_height_smoother.tsx @@ -9,7 +9,7 @@ import { css } from '@emotion/react'; import React, { PropsWithChildren, useEffect, useRef } from 'react'; -import { combineLatest, distinctUntilChanged, map } from 'rxjs'; +import { combineLatest } from 'rxjs'; import { GridLayoutStateManager } from './types'; export const GridHeightSmoother = ({ @@ -32,35 +32,19 @@ export const GridHeightSmoother = ({ if (!smoothHeightRef.current || gridLayoutStateManager.expandedPanelId$.getValue()) return; if (!interactionEvent) { - smoothHeightRef.current.style.height = `${dimensions.height}px`; + smoothHeightRef.current.style.minHeight = `${dimensions.height}px`; smoothHeightRef.current.style.userSelect = 'auto'; return; } - smoothHeightRef.current.style.height = `${Math.max( - dimensions.height ?? 0, + smoothHeightRef.current.style.minHeight = `${ smoothHeightRef.current.getBoundingClientRect().height - )}px`; + }px`; smoothHeightRef.current.style.userSelect = 'none'; }); - /** - * This subscription sets global CSS variables that can be used by all components contained within - * this wrapper; note that this is **currently** only used for the gutter size, but things like column - * count could be added here once we add the ability to change these values - */ - const globalCssVariableSubscription = gridLayoutStateManager.runtimeSettings$ - .pipe( - map(({ gutterSize }) => gutterSize), - distinctUntilChanged() - ) - .subscribe((gutterSize) => { - smoothHeightRef.current?.style.setProperty('--kbnGridGutterSize', `${gutterSize}`); - }); - return () => { interactionStyleSubscription.unsubscribe(); - globalCssVariableSubscription.unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -70,17 +54,14 @@ export const GridHeightSmoother = ({ ref={smoothHeightRef} className={'kbnGridWrapper'} css={css` - margin: calc(var(--kbnGridGutterSize) * 1px); + height: 100%; overflow-anchor: none; - transition: height 500ms linear; + transition: min-height 500ms linear; &:has(.kbnGridPanel--expanded) { - height: 100% !important; + min-height: 100% !important; position: relative; transition: none; - // switch to padding so that the panel does not extend the height of the parent - margin: 0px; - padding: calc(var(--kbnGridGutterSize) * 1px); } `} > 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 4aa93710f4a2f..4e978a6c92487 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 @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import classNames from 'classnames'; import { cloneDeep } from 'lodash'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { combineLatest, distinctUntilChanged, filter, map, pairwise, skip } from 'rxjs'; @@ -31,6 +32,7 @@ export interface GridLayoutProps { onLayoutChange: (newLayout: GridLayoutData) => void; expandedPanelId?: string; accessMode?: GridAccessMode; + className?: string; // this makes it so that custom CSS can be passed via Emotion } export const GridLayout = ({ @@ -40,15 +42,17 @@ export const GridLayout = ({ onLayoutChange, expandedPanelId, accessMode = 'EDIT', + className, }: GridLayoutProps) => { + const layoutRef = useRef(null); const { gridLayoutStateManager, setDimensionsRef } = useGridLayoutState({ layout, + layoutRef, gridSettings, expandedPanelId, accessMode, }); useGridLayoutEvents({ gridLayoutStateManager }); - const layoutRef = useRef(null); const [rowCount, setRowCount] = useState( gridLayoutStateManager.gridLayout$.getValue().length @@ -173,8 +177,10 @@ export const GridLayout = ({ layoutRef.current = divElement; setDimensionsRef(divElement); }} - className="kbnGrid" + className={classNames('kbnGrid', className)} css={css` + padding: calc(var(--kbnGridGutterSize) * 1px); + &:has(.kbnGridPanel--expanded) { ${expandedPanelStyles} } diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle.tsx index 63e909d5cfb8e..f14a56e184617 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/drag_handle.tsx @@ -19,7 +19,6 @@ import { UserMouseEvent, UserTouchEvent, } from '../types'; -import { isMouseEvent, isTouchEvent } from '../utils/sensors'; export interface DragHandleApi { setDragHandles: (refs: Array) => void; @@ -47,26 +46,13 @@ export const DragHandle = React.forwardRef< */ const onDragStart = useCallback( (e: UserMouseEvent | UserTouchEvent) => { - // ignore when not in edit mode - if (gridLayoutStateManager.accessMode$.getValue() !== 'EDIT') return; - - // ignore anything but left clicks for mouse events - if (isMouseEvent(e) && e.button !== 0) { - return; - } - // ignore multi-touch events for touch events - if (isTouchEvent(e) && e.touches.length > 1) { - return; - } - e.stopPropagation(); interactionStart('drag', e); }, - [interactionStart, gridLayoutStateManager.accessMode$] + [interactionStart] ); const onDragEnd = useCallback( (e: UserTouchEvent | UserMouseEvent) => { - e.stopPropagation(); interactionStart('drop', e); }, [interactionStart] @@ -118,7 +104,7 @@ export const DragHandle = React.forwardRef< aria-label={i18n.translate('kbnGridLayout.dragHandle.ariaLabel', { defaultMessage: 'Drag to move', })} - className="kbnGridPanel__dragHandle" + className="kbnGridPanel--dragHandle" css={css` opacity: 0; display: flex; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.tsx index a89b2230d0f13..b91f685007ddb 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_panel/grid_panel.tsx @@ -68,13 +68,12 @@ 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 { rowHeight } = gridLayoutStateManager.runtimeSettings$.getValue(); return css` position: relative; height: calc( 1px * ( - ${initialPanel.height} * (${rowHeight} + var(--kbnGridGutterSize)) - + ${initialPanel.height} * (var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) - var(--kbnGridGutterSize) ) ); @@ -91,10 +90,9 @@ export const GridPanel = forwardRef( const activePanelStyleSubscription = combineLatest([ gridLayoutStateManager.activePanel$, gridLayoutStateManager.gridLayout$, - gridLayoutStateManager.runtimeSettings$, ]) .pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it - .subscribe(([activePanel, gridLayout, runtimeSettings]) => { + .subscribe(([activePanel, gridLayout]) => { const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId]; const panel = gridLayout[rowIndex].panels[panelId]; if (!ref || !panel) return; @@ -102,8 +100,11 @@ export const GridPanel = forwardRef( const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue(); if (panelId === activePanel?.id) { + ref.classList.add('kbnGridPanel--active'); + // if the current panel is active, give it fixed positioning depending on the interaction event const { position: draggingPosition } = activePanel; + const runtimeSettings = gridLayoutStateManager.runtimeSettings$.getValue(); ref.style.zIndex = `${euiTheme.levels.modal}`; if (currentInteractionEvent?.type === 'resize') { @@ -135,7 +136,7 @@ export const GridPanel = forwardRef( ref.style.gridArea = `auto`; // shortcut to set all grid styles to `auto` } } else { - const { rowHeight } = gridLayoutStateManager.runtimeSettings$.getValue(); + ref.classList.remove('kbnGridPanel--active'); ref.style.zIndex = `auto`; @@ -145,7 +146,7 @@ export const GridPanel = forwardRef( ref.style.top = ``; ref.style.width = ``; // setting the height is necessary for mobile mode - ref.style.height = `calc(1px * (${panel.height} * (${rowHeight} + var(--kbnGridGutterSize)) - var(--kbnGridGutterSize)))`; + ref.style.height = `calc(1px * (${panel.height} * (var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) - var(--kbnGridGutterSize)))`; // and render the panel locked to the grid ref.style.gridColumnStart = `${panel.column + 1}`; 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 8ccfd4d44d96b..66f36aacb5748 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 @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { transparentize } from '@elastic/eui'; import { css } from '@emotion/react'; import { useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -22,7 +21,7 @@ export const ResizeHandle = ({ const { euiTheme } = useEuiTheme(); return (