Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[kbn-grid-layout] Allow rows to be reordered #213166

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ export const GridLayout = ({
}
});

const rowOrderSubscription = combineLatest([
gridLayoutStateManager.proposedGridLayout$,
gridLayoutStateManager.gridLayout$,
]).subscribe(([proposedGridLayout, gridLayout]) => {
const displayedGridLayout = proposedGridLayout ?? gridLayout;
setRowIdsInOrder(getRowKeysInOrder(displayedGridLayout));
});

/**
* This subscription adds and/or removes the necessary class names related to styling for
* mobile view and a static (non-interactable) grid layout
Expand All @@ -115,6 +123,7 @@ export const GridLayout = ({

return () => {
onLayoutChangeSubscription.unsubscribe();
rowOrderSubscription.unsubscribe();
gridLayoutClassSubscription.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -160,7 +169,7 @@ const styles = {
padding: 'calc(var(--kbnGridGutterSize) * 1px)',
}),
hasActivePanel: css({
'&:has(.kbnGridPanel--active)': {
'&:has(.kbnGridPanel--active), &:has(.kbnGridRowHeader--active)': {
// disable pointer events and user select on drag + resize
userSelect: 'none',
pointerEvents: 'none',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const styles = ({ euiTheme }: UseEuiTheme) =>
border: `1px solid ${euiTheme.border.color}`,
borderBottom: 'none',
backgroundColor: euiTheme.colors.backgroundBasePlain,
borderRadius: `${euiTheme.border.radius} ${euiTheme.border.radius} 0 0`,
borderRadius: `${euiTheme.border.radius.medium} ${euiTheme.border.radius.medium} 0 0`,
transition: `${euiTheme.animation.slow} opacity`,
touchAction: 'none',
'.kbnGridPanel:hover &, .kbnGridPanel:focus-within &, &:active, &:focus': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { useCallback, useEffect, useRef } from 'react';

import { useGridLayoutEvents } from '../../use_grid_layout_events';
import { useGridLayoutPanelEvents } from '../../use_grid_layout_events/panel_events';
import { UserInteractionEvent } from '../../use_grid_layout_events/types';
import { useGridLayoutContext } from '../../use_grid_layout_context';

Expand All @@ -27,7 +27,7 @@ export const useDragHandleApi = ({
}): DragHandleApi => {
const { useCustomDragHandle } = useGridLayoutContext();

const startInteraction = useGridLayoutEvents({
const startInteraction = useGridLayoutPanelEvents({
interactionType: 'drag',
panelId,
rowId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import React, { useEffect, useRef } from 'react';
import { combineLatest, skip } from 'rxjs';

import { css } from '@emotion/react';
import { useGridLayoutContext } from './use_grid_layout_context';
import { useGridLayoutContext } from '../use_grid_layout_context';

export const DragPreview = React.memo(({ rowId }: { rowId: string }) => {
const { gridLayoutStateManager } = useGridLayoutContext();
Expand Down Expand Up @@ -54,4 +54,4 @@ export const DragPreview = React.memo(({ rowId }: { rowId: string }) => {

const styles = css({ display: 'none', pointerEvents: 'none' });

DragPreview.displayName = 'KbnGridLayoutDragPreview';
DragPreview.displayName = 'KbnGridLayoutDragPanelPreview';
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import type { UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';

import { useGridLayoutEvents } from '../use_grid_layout_events';
import { useGridLayoutPanelEvents } from '../use_grid_layout_events/panel_events';

export const ResizeHandle = React.memo(({ rowId, panelId }: { rowId: string; panelId: string }) => {
const startInteraction = useGridLayoutEvents({
const startInteraction = useGridLayoutPanelEvents({
interactionType: 'resize',
panelId,
rowId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { deleteRow, movePanelsToRow } from '../utils/row_management';
import { useGridLayoutContext } from '../use_grid_layout_context';
import { deleteRow, movePanelsToRow } from '../utils/row_management';

export const DeleteGridRowModal = ({
rowId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 React, { useRef } from 'react';

import { css } from '@emotion/react';
import { UseEuiTheme } from '@elastic/eui';
// import { useGridLayoutContext } from '../use_grid_layout_context';

export const DragPreview = React.memo(({ rowId }: { rowId: string }) => {
// const { gridLayoutStateManager } = useGridLayoutContext();

const dragPreviewRef = useRef<HTMLDivElement | null>(null);

return <div ref={dragPreviewRef} className={'kbnGridPanel--rowDragPreview'} css={styles} />;
});

const styles = ({ euiTheme }: UseEuiTheme) =>
css({
width: '100%',
height: '32px',
margin: '8px 0px',
backgroundColor: euiTheme.components.dragDropDraggingBackground,
position: 'relative',
});

DragPreview.displayName = 'KbnGridLayoutDragRowPreview';
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import { combineLatest, map, pairwise, skip } from 'rxjs';

import { css } from '@emotion/react';

import { DragPreview } from '../drag_preview';
import { DragPreview as DragPanelPreview } from '../grid_panel/drag_preview';
import { GridPanel } from '../grid_panel';
import { useGridLayoutContext } from '../use_grid_layout_context';
import { getPanelKeysInOrder } from '../utils/resolve_grid_row';
import { GridRowHeader } from './grid_row_header';
import { getPanelKeysInOrder } from '../utils/resolve_grid_row';

export interface GridRowProps {
rowId: string;
Expand Down Expand Up @@ -156,7 +156,7 @@ export const GridRow = React.memo(({ rowId }: GridRowProps) => {
{panelIdsInOrder.map((panelId) => (
<GridPanel key={panelId} panelId={panelId} rowId={rowId} />
))}
<DragPreview rowId={rowId} />
<DragPanelPreview rowId={rowId} />
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* 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 React, { useCallback, useEffect, useState } from 'react';
import { distinctUntilChanged, map } from 'rxjs';
import classNames from 'classnames';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { distinctUntilChanged, map, skip } from 'rxjs';

import {
EuiButtonIcon,
Expand All @@ -21,8 +22,10 @@ import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';

import { useGridLayoutContext } from '../use_grid_layout_context';
import { useGridLayoutRowEvents } from '../use_grid_layout_events/row_events';
import { deleteRow } from '../utils/row_management';
import { DeleteGridRowModal } from './delete_grid_row_modal';
import { DragPreview } from './drag_preview';
import { GridRowTitle } from './grid_row_title';

export interface GridRowHeaderProps {
Expand All @@ -34,7 +37,12 @@ export interface GridRowHeaderProps {
export const GridRowHeader = React.memo(
({ rowId, toggleIsCollapsed, collapseButtonRef }: GridRowHeaderProps) => {
const { gridLayoutStateManager } = useGridLayoutContext();
const startInteraction = useGridLayoutRowEvents({
interactionType: 'drag',
rowId,
});

const [isActive, setIsActive] = useState<boolean>(false);
const [editTitleOpen, setEditTitleOpen] = useState<boolean>(false);
const [deleteModalVisible, setDeleteModalVisible] = useState<boolean>(false);
const [readOnly, setReadOnly] = useState<boolean>(
Expand Down Expand Up @@ -67,9 +75,31 @@ export const GridRowHeader = React.memo(
setPanelCount(count);
});

const dragRowStyleSubscription = gridLayoutStateManager.activeRow$
.pipe(skip(1))
.subscribe((activeRow) => {
const headerRef = gridLayoutStateManager.headerRefs.current[rowId];
if (!headerRef) return;

if (activeRow?.id === rowId) {
setIsActive(true);
headerRef.style.position = 'fixed';
headerRef.style.top = `${activeRow.startingPosition.top}px`;
headerRef.style.right = `${activeRow.startingPosition.right}px`;
headerRef.style.transform = `translate(${activeRow.translate.left}px, ${activeRow.translate.top}px)`;
} else {
setIsActive(false);
headerRef.style.position = 'relative';
headerRef.style.top = ``;
headerRef.style.right = ``;
headerRef.style.transform = ``;
}
});

return () => {
accessModeSubscription.unsubscribe();
panelCountSubscription.unsubscribe();
dragRowStyleSubscription.unsubscribe();
};
}, [gridLayoutStateManager, rowId]);

Expand All @@ -94,11 +124,15 @@ export const GridRowHeader = React.memo(
responsive={false}
alignItems="center"
css={styles.headerStyles}
className="kbnGridRowHeader"
className={classNames('kbnGridRowHeader', { 'kbnGridRowHeader--active': isActive })}
data-test-subj={`kbnGridRowHeader-${rowId}`}
ref={(element: HTMLDivElement | null) =>
(gridLayoutStateManager.headerRefs.current[rowId] = element)
}
>
<GridRowTitle
rowId={rowId}
isActive={isActive}
readOnly={readOnly}
toggleIsCollapsed={toggleIsCollapsed}
editTitleOpen={editTitleOpen}
Expand All @@ -112,57 +146,60 @@ export const GridRowHeader = React.memo(
*/
!editTitleOpen && (
<>
<EuiFlexItem grow={false} css={styles.hiddenOnCollapsed}>
<EuiText
color="subdued"
size="s"
data-test-subj={`kbnGridRowHeader-${rowId}--panelCount`}
className={'kbnGridLayout--panelCount'}
>
{i18n.translate('kbnGridLayout.rowHeader.panelCount', {
defaultMessage:
'({panelCount} {panelCount, plural, one {panel} other {panels}})',
values: {
panelCount,
},
})}
</EuiText>
</EuiFlexItem>
{!isActive && (
<EuiFlexItem grow={false} css={styles.hiddenOnCollapsed}>
<EuiText
color="subdued"
size="s"
data-test-subj={`kbnGridRowHeader-${rowId}--panelCount`}
className={'kbnGridLayout--panelCount'}
>
{i18n.translate('kbnGridLayout.rowHeader.panelCount', {
defaultMessage:
'({panelCount} {panelCount, plural, one {panel} other {panels}})',
values: {
panelCount,
},
})}
</EuiText>
</EuiFlexItem>
)}
{!readOnly && (
<>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="trash"
color="danger"
className="kbnGridLayout--deleteRowIcon"
onClick={confirmDeleteRow}
aria-label={i18n.translate('kbnGridLayout.row.deleteRow', {
defaultMessage: 'Delete section',
})}
/>
</EuiFlexItem>
{!isActive && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="trash"
color="danger"
className="kbnGridLayout--deleteRowIcon"
onClick={confirmDeleteRow}
aria-label={i18n.translate('kbnGridLayout.row.deleteRow', {
defaultMessage: 'Delete section',
})}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false} css={[styles.hiddenOnCollapsed, styles.floatToRight]}>
{/*
This was added as a placeholder to get the desired UI here; however, since the
functionality will be implemented in https://github.com/elastic/kibana/issues/190381
and this button doesn't do anything yet, I'm choosing to hide it for now. I am keeping
the `FlexItem` wrapper so that the UI still looks correct.
*/}
{/* <EuiButtonIcon
<EuiButtonIcon
iconType="move"
color="text"
className="kbnGridLayout--moveRowIcon"
aria-label={i18n.translate('kbnGridLayout.row.moveRow', {
defaultMessage: 'Move section',
})}
/> */}
onMouseDown={(e) => {
startInteraction(e);
}}
/>
</EuiFlexItem>
</>
)}
</>
)
}
</EuiFlexGroup>
{isActive && <DragPreview rowId={rowId} />}
{/* <DragPreview rowId={rowId} />{' '} */}
{deleteModalVisible && (
<DeleteGridRowModal rowId={rowId} setDeleteModalVisible={setDeleteModalVisible} />
)}
Expand All @@ -183,9 +220,21 @@ const styles = {
}),
headerStyles: ({ euiTheme }: UseEuiTheme) =>
css({
'&.kbnGridRowHeader--active': {
width: 'fit-content',
backgroundColor: euiTheme.colors.backgroundBasePlain,
border: `1px solid ${euiTheme.border.color}`,
borderRadius: `${euiTheme.border.radius.medium} ${euiTheme.border.radius.medium}`,
paddingLeft: '8px',
minWidth: '300px',
zIndex: euiTheme.levels.modal,
'.kbnGridLayout--moveRowIcon': {
opacity: 1,
},
},
height: `calc(${euiTheme.size.xl} + (2 * ${euiTheme.size.s}))`,
padding: `${euiTheme.size.s} 0px`,
borderBottom: '1px solid transparent', // prevents layout shift
border: '1px solid transparent', // prevents layout shift
'.kbnGridRowContainer--collapsed &': {
borderBottom: euiTheme.border.thin,
},
Expand All @@ -195,6 +244,15 @@ const styles = {
'.kbnGridLayout--panelCount': {
textWrapMode: 'nowrap', // prevent panel count from wrapping
},

'.kbnGridLayout--moveRowIcon': {
'&:active, &:hover': {
cursor: 'move',
backgroundColor: 'transparent',
transform: 'none !important',
},
},

// these styles hide the delete + move actions by default and only show them on hover
[`.kbnGridLayout--deleteRowIcon,
.kbnGridLayout--moveRowIcon`]: {
Expand Down
Loading