mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Collapsable panels][A11y] Tabbing through panels in a correct order (#202365)
## Summary This is a preparatory step for keyboard navigation improvements. It ensures proper tabbing order by aligning grid positions with the sequence in the HTML structure, as recommended for accessibility. Manipulating the tabindex property is an alternative but it's not a good approach. Keeping grid layouts consistent with the HTML flow is a more sustainable and accessible approach, as outlined in [related documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Grid_layout_and_accessibility). https://github.com/user-attachments/assets/d41eac8d-1ee1-47b1-8f40-e3207796573b I also modified styles for drag and resize handles. hover: <img width="913" alt="Screenshot 2024-11-29 at 20 47 13" src="https://github.com/user-attachments/assets/8348e5ee-9712-4a2b-9135-80a98715dc58"> focus: <img width="803" alt="Screenshot 2024-11-29 at 20 47 40" src="https://github.com/user-attachments/assets/8ee65354-0f7e-4394-9718-44d7e2a46700"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3049e8984e
commit
2f1ef6f345
16 changed files with 739 additions and 191 deletions
167
packages/kbn-grid-layout/grid/grid_layout.test.tsx
Normal file
167
packages/kbn-grid-layout/grid/grid_layout.test.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { getSampleLayout } from './test_utils/sample_layout';
|
||||
import { GridLayout, GridLayoutProps } from './grid_layout';
|
||||
import { gridSettings, mockRenderPanelContents } from './test_utils/mocks';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
describe('GridLayout', () => {
|
||||
const renderGridLayout = (propsOverrides: Partial<GridLayoutProps> = {}) => {
|
||||
const defaultProps: GridLayoutProps = {
|
||||
accessMode: 'EDIT',
|
||||
layout: getSampleLayout(),
|
||||
gridSettings,
|
||||
renderPanelContents: mockRenderPanelContents,
|
||||
onLayoutChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { rerender, ...rtlRest } = render(<GridLayout {...defaultProps} {...propsOverrides} />);
|
||||
|
||||
return {
|
||||
...rtlRest,
|
||||
rerender: (overrides: Partial<GridLayoutProps>) =>
|
||||
rerender(<GridLayout {...defaultProps} {...overrides} />),
|
||||
};
|
||||
};
|
||||
const getAllThePanelIds = () =>
|
||||
screen
|
||||
.getAllByRole('button', { name: /panelId:panel/i })
|
||||
.map((el) => el.getAttribute('aria-label')?.replace(/panelId:/g, ''));
|
||||
|
||||
const startDragging = (handle: HTMLElement, options = { clientX: 0, clientY: 0 }) => {
|
||||
fireEvent.mouseDown(handle, options);
|
||||
};
|
||||
const moveTo = (options = { clientX: 256, clientY: 128 }) => {
|
||||
fireEvent.mouseMove(document, options);
|
||||
};
|
||||
const drop = (handle: HTMLElement) => {
|
||||
fireEvent.mouseUp(handle);
|
||||
};
|
||||
|
||||
const assertTabThroughPanel = async (panelId: string) => {
|
||||
await userEvent.tab(); // tab to drag handle
|
||||
await userEvent.tab(); // tab to the panel
|
||||
expect(screen.getByLabelText(`panelId:${panelId}`)).toHaveFocus();
|
||||
await userEvent.tab(); // tab to the resize handle
|
||||
};
|
||||
|
||||
const expectedInitialOrder = [
|
||||
'panel1',
|
||||
'panel5',
|
||||
'panel2',
|
||||
'panel3',
|
||||
'panel7',
|
||||
'panel6',
|
||||
'panel8',
|
||||
'panel4',
|
||||
'panel9',
|
||||
'panel10',
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it(`'renderPanelContents' is not called during dragging`, () => {
|
||||
renderGridLayout();
|
||||
|
||||
expect(mockRenderPanelContents).toHaveBeenCalledTimes(10); // renderPanelContents is called for each of 10 panels
|
||||
jest.clearAllMocks();
|
||||
|
||||
const panel1DragHandle = screen.getAllByRole('button', { name: /drag to move/i })[0];
|
||||
startDragging(panel1DragHandle);
|
||||
moveTo({ clientX: 256, clientY: 128 });
|
||||
expect(mockRenderPanelContents).toHaveBeenCalledTimes(0); // renderPanelContents should not be called during dragging
|
||||
|
||||
drop(panel1DragHandle);
|
||||
expect(mockRenderPanelContents).toHaveBeenCalledTimes(0); // renderPanelContents should not be called after reordering
|
||||
});
|
||||
|
||||
describe('panels order: panels are rendered from left to right, from top to bottom', () => {
|
||||
it('focus management - tabbing through the panels', async () => {
|
||||
renderGridLayout();
|
||||
// we only test a few panels because otherwise that test would execute for too long
|
||||
await assertTabThroughPanel('panel1');
|
||||
await assertTabThroughPanel('panel5');
|
||||
await assertTabThroughPanel('panel2');
|
||||
await assertTabThroughPanel('panel3');
|
||||
});
|
||||
it('on initializing', () => {
|
||||
renderGridLayout();
|
||||
expect(getAllThePanelIds()).toEqual(expectedInitialOrder);
|
||||
});
|
||||
|
||||
it('after reordering some panels', async () => {
|
||||
renderGridLayout();
|
||||
|
||||
const panel1DragHandle = screen.getAllByRole('button', { name: /drag to move/i })[0];
|
||||
startDragging(panel1DragHandle);
|
||||
|
||||
moveTo({ clientX: 256, clientY: 128 });
|
||||
expect(getAllThePanelIds()).toEqual(expectedInitialOrder); // the panels shouldn't be reordered till we drop
|
||||
|
||||
drop(panel1DragHandle);
|
||||
expect(getAllThePanelIds()).toEqual([
|
||||
'panel2',
|
||||
'panel5',
|
||||
'panel3',
|
||||
'panel7',
|
||||
'panel1',
|
||||
'panel8',
|
||||
'panel6',
|
||||
'panel4',
|
||||
'panel9',
|
||||
'panel10',
|
||||
]);
|
||||
});
|
||||
it('after removing a panel', async () => {
|
||||
const { rerender } = renderGridLayout();
|
||||
const sampleLayoutWithoutPanel1 = cloneDeep(getSampleLayout());
|
||||
delete sampleLayoutWithoutPanel1[0].panels.panel1;
|
||||
rerender({ layout: sampleLayoutWithoutPanel1 });
|
||||
|
||||
expect(getAllThePanelIds()).toEqual([
|
||||
'panel2',
|
||||
'panel5',
|
||||
'panel3',
|
||||
'panel7',
|
||||
'panel6',
|
||||
'panel8',
|
||||
'panel4',
|
||||
'panel9',
|
||||
'panel10',
|
||||
]);
|
||||
});
|
||||
it('after replacing a panel id', async () => {
|
||||
const { rerender } = renderGridLayout();
|
||||
const modifiedLayout = cloneDeep(getSampleLayout());
|
||||
const newPanel = { ...modifiedLayout[0].panels.panel1, id: 'panel11' };
|
||||
delete modifiedLayout[0].panels.panel1;
|
||||
modifiedLayout[0].panels.panel11 = newPanel;
|
||||
|
||||
rerender({ layout: modifiedLayout });
|
||||
|
||||
expect(getAllThePanelIds()).toEqual([
|
||||
'panel11',
|
||||
'panel5',
|
||||
'panel2',
|
||||
'panel3',
|
||||
'panel7',
|
||||
'panel6',
|
||||
'panel8',
|
||||
'panel4',
|
||||
'panel9',
|
||||
'panel10',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -21,7 +21,7 @@ import { useGridLayoutState } from './use_grid_layout_state';
|
|||
import { isLayoutEqual } from './utils/equality_checks';
|
||||
import { resolveGridRow } from './utils/resolve_grid_row';
|
||||
|
||||
interface GridLayoutProps {
|
||||
export interface GridLayoutProps {
|
||||
layout: GridLayoutData;
|
||||
gridSettings: GridSettings;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
|
@ -121,11 +121,6 @@ export const GridLayout = ({
|
|||
rowIndex={rowIndex}
|
||||
renderPanelContents={renderPanelContents}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
toggleIsCollapsed={() => {
|
||||
const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value);
|
||||
newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}}
|
||||
setInteractionEvent={(nextInteractionEvent) => {
|
||||
if (!nextInteractionEvent) {
|
||||
gridLayoutStateManager.activePanel$.next(undefined);
|
||||
|
|
74
packages/kbn-grid-layout/grid/grid_panel/drag_handle.tsx
Normal file
74
packages/kbn-grid-layout/grid/grid_panel/drag_handle.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { EuiIcon, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PanelInteractionEvent } from '../types';
|
||||
|
||||
export const DragHandle = ({
|
||||
interactionStart,
|
||||
}: {
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<button
|
||||
aria-label={i18n.translate('kbnGridLayout.dragHandle.ariaLabel', {
|
||||
defaultMessage: 'Drag to move',
|
||||
})}
|
||||
className="kbnGridPanel__dragHandle"
|
||||
css={css`
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
cursor: move;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: -${euiThemeVars.euiSizeL};
|
||||
width: ${euiThemeVars.euiSizeL};
|
||||
height: ${euiThemeVars.euiSizeL};
|
||||
z-index: ${euiThemeVars.euiZLevel3};
|
||||
margin-left: ${euiThemeVars.euiSizeS};
|
||||
border: 1px solid ${euiTheme.border.color};
|
||||
border-bottom: none;
|
||||
background-color: ${euiTheme.colors.emptyShade};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius} ${euiThemeVars.euiBorderRadius} 0 0;
|
||||
cursor: grab;
|
||||
transition: ${euiThemeVars.euiAnimSpeedSlow} opacity;
|
||||
.kbnGridPanel:hover &,
|
||||
.kbnGridPanel:focus-within &,
|
||||
&:active,
|
||||
&:focus {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.kbnGrid--static & {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
onMouseDown={(e) => {
|
||||
interactionStart('drag', e);
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
interactionStart('drop', e);
|
||||
}}
|
||||
>
|
||||
<EuiIcon type="grabOmnidirectional" />
|
||||
</button>
|
||||
);
|
||||
};
|
68
packages/kbn-grid-layout/grid/grid_panel/grid_panel.test.tsx
Normal file
68
packages/kbn-grid-layout/grid/grid_panel/grid_panel.test.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { GridPanel, GridPanelProps } from './grid_panel';
|
||||
import { gridLayoutStateManagerMock } from '../test_utils/mocks';
|
||||
|
||||
describe('GridPanel', () => {
|
||||
const mockRenderPanelContents = jest.fn((panelId) => <div>Panel Content {panelId}</div>);
|
||||
const mockInteractionStart = jest.fn();
|
||||
|
||||
const renderGridPanel = (propsOverrides: Partial<GridPanelProps> = {}) => {
|
||||
return render(
|
||||
<GridPanel
|
||||
panelId="panel1"
|
||||
rowIndex={0}
|
||||
renderPanelContents={mockRenderPanelContents}
|
||||
interactionStart={mockInteractionStart}
|
||||
gridLayoutStateManager={gridLayoutStateManagerMock}
|
||||
{...propsOverrides}
|
||||
/>
|
||||
);
|
||||
};
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders panel contents correctly', () => {
|
||||
renderGridPanel();
|
||||
expect(screen.getByText('Panel Content panel1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('drag handle interaction', () => {
|
||||
it('calls `drag` interactionStart on mouse down', () => {
|
||||
renderGridPanel();
|
||||
const dragHandle = screen.getByRole('button', { name: /drag to move/i });
|
||||
fireEvent.mouseDown(dragHandle);
|
||||
expect(mockInteractionStart).toHaveBeenCalledWith('drag', expect.any(Object));
|
||||
});
|
||||
it('calls `drop` interactionStart on mouse up', () => {
|
||||
renderGridPanel();
|
||||
const dragHandle = screen.getByRole('button', { name: /drag to move/i });
|
||||
fireEvent.mouseUp(dragHandle);
|
||||
expect(mockInteractionStart).toHaveBeenCalledWith('drop', expect.any(Object));
|
||||
});
|
||||
});
|
||||
describe('resize handle interaction', () => {
|
||||
it('calls `resize` interactionStart on mouse down', () => {
|
||||
renderGridPanel();
|
||||
const resizeHandle = screen.getByRole('button', { name: /resize/i });
|
||||
fireEvent.mouseDown(resizeHandle);
|
||||
expect(mockInteractionStart).toHaveBeenCalledWith('resize', expect.any(Object));
|
||||
});
|
||||
it('calls `drop` interactionStart on mouse up', () => {
|
||||
renderGridPanel();
|
||||
const resizeHandle = screen.getByRole('button', { name: /resize/i });
|
||||
fireEvent.mouseUp(resizeHandle);
|
||||
expect(mockInteractionStart).toHaveBeenCalledWith('drop', expect.any(Object));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,39 +10,30 @@
|
|||
import React, { forwardRef, useEffect, useMemo } from 'react';
|
||||
import { combineLatest, skip } from 'rxjs';
|
||||
|
||||
import {
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
euiFullHeight,
|
||||
transparentize,
|
||||
useEuiOverflowScroll,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { EuiPanel, euiFullHeight, useEuiOverflowScroll } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { GridLayoutStateManager, PanelInteractionEvent } from '../types';
|
||||
import { getKeysInOrder } from '../utils/resolve_grid_row';
|
||||
import { DragHandle } from './drag_handle';
|
||||
import { ResizeHandle } from './resize_handle';
|
||||
|
||||
import { GridLayoutStateManager, PanelInteractionEvent } from './types';
|
||||
import { getKeysInOrder } from './utils/resolve_grid_row';
|
||||
export interface GridPanelProps {
|
||||
panelId: string;
|
||||
rowIndex: number;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}
|
||||
|
||||
export const GridPanel = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
panelId: string;
|
||||
rowIndex: number;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
) => void;
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}
|
||||
>(
|
||||
export const GridPanel = forwardRef<HTMLDivElement, GridPanelProps>(
|
||||
(
|
||||
{ panelId, rowIndex, renderPanelContents, interactionStart, gridLayoutStateManager },
|
||||
panelRef
|
||||
) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
/** Set initial styles based on state at mount to prevent styles from "blipping" */
|
||||
const initialStyles = useMemo(() => {
|
||||
const initialPanel = gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels[panelId];
|
||||
|
@ -158,7 +149,7 @@ export const GridPanel = forwardRef<
|
|||
const panel = allPanels[panelId];
|
||||
if (!ref || !panel) return;
|
||||
|
||||
const sortedKeys = getKeysInOrder(gridLayout[rowIndex]);
|
||||
const sortedKeys = getKeysInOrder(gridLayout[rowIndex].panels);
|
||||
const currentPanelPosition = sortedKeys.indexOf(panelId);
|
||||
const sortedKeysBefore = sortedKeys.slice(0, currentPanelPosition);
|
||||
const responsiveGridRowStart = sortedKeysBefore.reduce(
|
||||
|
@ -180,7 +171,6 @@ export const GridPanel = forwardRef<
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Memoize panel contents to prevent unnecessary re-renders
|
||||
*/
|
||||
|
@ -189,93 +179,29 @@ export const GridPanel = forwardRef<
|
|||
}, [panelId, renderPanelContents]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={panelRef} css={initialStyles}>
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
<div ref={panelRef} css={initialStyles} className="kbnGridPanel">
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
css={css`
|
||||
padding: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<DragHandle interactionStart={interactionStart} />
|
||||
<div
|
||||
css={css`
|
||||
padding: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
${euiFullHeight()}
|
||||
${useEuiOverflowScroll('y', false)}
|
||||
${useEuiOverflowScroll('x', false)}
|
||||
`}
|
||||
>
|
||||
{/* drag handle */}
|
||||
<div
|
||||
className="kbnGridPanel__dragHandle"
|
||||
css={css`
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
cursor: move;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: -${euiThemeVars.euiSizeL};
|
||||
width: ${euiThemeVars.euiSizeL};
|
||||
height: ${euiThemeVars.euiSizeL};
|
||||
z-index: ${euiThemeVars.euiZLevel3};
|
||||
margin-left: ${euiThemeVars.euiSizeS};
|
||||
border: 1px solid ${euiTheme.border.color};
|
||||
background-color: ${euiTheme.colors.emptyShade};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius} ${euiThemeVars.euiBorderRadius} 0 0;
|
||||
&:hover {
|
||||
cursor: grab;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.kbnGrid--static & {
|
||||
opacity: 0 !important;
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
onMouseDown={(e) => interactionStart('drag', e)}
|
||||
onMouseUp={(e) => interactionStart('drop', e)}
|
||||
>
|
||||
<EuiIcon type="grabOmnidirectional" />
|
||||
</div>
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
className="kbnGridPanel__resizeHandle"
|
||||
onMouseDown={(e) => interactionStart('resize', e)}
|
||||
onMouseUp={(e) => interactionStart('drop', e)}
|
||||
css={css`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
margin: -2px;
|
||||
position: absolute;
|
||||
width: ${euiThemeVars.euiSizeL};
|
||||
height: ${euiThemeVars.euiSizeL};
|
||||
transition: opacity 0.2s, border 0.2s;
|
||||
border-radius: 7px 0 7px 0;
|
||||
border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
|
||||
border-right: 2px solid ${euiThemeVars.euiColorSuccess};
|
||||
:hover {
|
||||
opacity: 1;
|
||||
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
|
||||
cursor: se-resize;
|
||||
}
|
||||
.kbnGrid--static & {
|
||||
opacity: 0 !important;
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
css={css`
|
||||
${euiFullHeight()}
|
||||
${useEuiOverflowScroll('y', false)}
|
||||
${useEuiOverflowScroll('x', false)}
|
||||
`}
|
||||
>
|
||||
{panelContents}
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</div>
|
||||
</>
|
||||
{panelContents}
|
||||
</div>
|
||||
<ResizeHandle interactionStart={interactionStart} />
|
||||
</EuiPanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
10
packages/kbn-grid-layout/grid/grid_panel/index.tsx
Normal file
10
packages/kbn-grid-layout/grid/grid_panel/index.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
export { GridPanel } from './grid_panel';
|
70
packages/kbn-grid-layout/grid/grid_panel/resize_handle.tsx
Normal file
70
packages/kbn-grid-layout/grid/grid_panel/resize_handle.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { transparentize } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PanelInteractionEvent } from '../types';
|
||||
|
||||
export const ResizeHandle = ({
|
||||
interactionStart,
|
||||
}: {
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className="kbnGridPanel__resizeHandle"
|
||||
onMouseDown={(e) => {
|
||||
interactionStart('resize', e);
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
interactionStart('drop', e);
|
||||
}}
|
||||
aria-label={i18n.translate('kbnGridLayout.resizeHandle.ariaLabel', {
|
||||
defaultMessage: 'Resize panel',
|
||||
})}
|
||||
css={css`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
margin: -2px;
|
||||
position: absolute;
|
||||
width: ${euiThemeVars.euiSizeL};
|
||||
height: ${euiThemeVars.euiSizeL};
|
||||
transition: opacity 0.2s, border 0.2s;
|
||||
border-radius: 7px 0 7px 0;
|
||||
border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
|
||||
border-right: 2px solid ${euiThemeVars.euiColorSuccess};
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline-style: none !important;
|
||||
opacity: 1;
|
||||
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
|
||||
cursor: se-resize;
|
||||
}
|
||||
.kbnGrid--static & {
|
||||
opacity: 0 !important;
|
||||
display: none;
|
||||
}
|
||||
.kbnGridPanel__dragHandle:has(~ &:hover) {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.kbnGridPanel__dragHandle:has(~ &:focus) {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
49
packages/kbn-grid-layout/grid/grid_row/grid_row.test.tsx
Normal file
49
packages/kbn-grid-layout/grid/grid_row/grid_row.test.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { GridRow, GridRowProps } from './grid_row';
|
||||
import { gridLayoutStateManagerMock, mockRenderPanelContents } from '../test_utils/mocks';
|
||||
import { getSampleLayout } from '../test_utils/sample_layout';
|
||||
|
||||
describe('GridRow', () => {
|
||||
const setInteractionEvent = jest.fn();
|
||||
|
||||
const renderGridRow = (propsOverrides: Partial<GridRowProps> = {}) => {
|
||||
return render(
|
||||
<GridRow
|
||||
rowIndex={0}
|
||||
renderPanelContents={mockRenderPanelContents}
|
||||
setInteractionEvent={setInteractionEvent}
|
||||
gridLayoutStateManager={gridLayoutStateManagerMock}
|
||||
{...propsOverrides}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it('renders all the panels in a row', () => {
|
||||
renderGridRow();
|
||||
const firstRowPanels = Object.values(getSampleLayout()[0].panels);
|
||||
firstRowPanels.forEach((panel) => {
|
||||
expect(screen.getByLabelText(`panelId:${panel.id}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show the panels in a row that is collapsed', async () => {
|
||||
renderGridRow({ rowIndex: 1 });
|
||||
|
||||
expect(screen.getAllByText(/panel content/)).toHaveLength(1);
|
||||
|
||||
const collapseButton = screen.getByRole('button', { name: /toggle collapse/i });
|
||||
await userEvent.click(collapseButton);
|
||||
|
||||
expect(screen.queryAllByText(/panel content/)).toHaveLength(0);
|
||||
});
|
||||
});
|
|
@ -10,40 +10,41 @@
|
|||
import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { combineLatest, map, pairwise, skip } from 'rxjs';
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle, transparentize } from '@elastic/eui';
|
||||
import { transparentize } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
||||
import { DragPreview } from './drag_preview';
|
||||
import { GridPanel } from './grid_panel';
|
||||
import { GridLayoutStateManager, GridRowData, PanelInteractionEvent } from './types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { DragPreview } from '../drag_preview';
|
||||
import { GridPanel } from '../grid_panel';
|
||||
import { GridLayoutStateManager, GridRowData, PanelInteractionEvent } from '../types';
|
||||
import { getKeysInOrder } from '../utils/resolve_grid_row';
|
||||
import { GridRowHeader } from './grid_row_header';
|
||||
|
||||
export const GridRow = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
rowIndex: number;
|
||||
toggleIsCollapsed: () => void;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
setInteractionEvent: (interactionData?: PanelInteractionEvent) => void;
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
rowIndex,
|
||||
toggleIsCollapsed,
|
||||
renderPanelContents,
|
||||
setInteractionEvent,
|
||||
gridLayoutStateManager,
|
||||
},
|
||||
gridRef
|
||||
) => {
|
||||
export interface GridRowProps {
|
||||
rowIndex: number;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
setInteractionEvent: (interactionData?: PanelInteractionEvent) => void;
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}
|
||||
|
||||
export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
||||
({ rowIndex, renderPanelContents, setInteractionEvent, gridLayoutStateManager }, gridRef) => {
|
||||
const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex];
|
||||
const [panelIds, setPanelIds] = useState<string[]>(Object.keys(currentRow.panels));
|
||||
|
||||
const [panelIds, setPanelIds] = useState<string[]>(() => getKeysInOrder(currentRow.panels));
|
||||
const [rowTitle, setRowTitle] = useState<string>(currentRow.title);
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(currentRow.isCollapsed);
|
||||
|
||||
/** Syncs panel IDs in order after a change in the grid layout, such as adding, removing, or reordering panels. */
|
||||
const syncPanelIds = useCallback(() => {
|
||||
const newPanelIds = getKeysInOrder(gridLayoutStateManager.gridLayout$.value[rowIndex].panels);
|
||||
const hasOrderChanged = JSON.stringify(panelIds) !== JSON.stringify(newPanelIds);
|
||||
if (hasOrderChanged) {
|
||||
setPanelIds(newPanelIds);
|
||||
}
|
||||
}, [setPanelIds, gridLayoutStateManager.gridLayout$, rowIndex, panelIds]);
|
||||
|
||||
const getRowCount = useCallback(
|
||||
(row: GridRowData) => {
|
||||
const maxRow = Object.values(row.panels).reduce((acc, panel) => {
|
||||
|
@ -147,9 +148,14 @@ export const GridRow = forwardRef<
|
|||
});
|
||||
|
||||
/**
|
||||
* The things that should trigger a re-render are title, collapsed state, and panel ids - panel positions
|
||||
* are being controlled via CSS styles, so they do not need to trigger a re-render. This subscription ensures
|
||||
* that the row will re-render when one of those three things changes.
|
||||
* This subscription ensures that the row will re-render when one of the following changes:
|
||||
* - Title
|
||||
* - Collapsed state
|
||||
* - Panel IDs (adding/removing/replacing, but not reordering)
|
||||
*
|
||||
* Note: During dragging or resizing actions, the row should not re-render because panel positions are controlled via CSS styles for performance reasons.
|
||||
* However, once the user finishes the interaction, the order of rendered panels need to be aligned with how they are displayed in the grid for accessibility reasons (screen readers and focus management).
|
||||
* This is handled in the syncPanelIds callback.
|
||||
*/
|
||||
const rowStateSubscription = gridLayoutStateManager.gridLayout$
|
||||
.pipe(
|
||||
|
@ -157,7 +163,7 @@ export const GridRow = forwardRef<
|
|||
return {
|
||||
title: gridLayout[rowIndex].title,
|
||||
isCollapsed: gridLayout[rowIndex].isCollapsed,
|
||||
panelIds: Object.keys(gridLayout[rowIndex].panels),
|
||||
panelIds: getKeysInOrder(gridLayout[rowIndex].panels),
|
||||
};
|
||||
}),
|
||||
pairwise()
|
||||
|
@ -212,6 +218,9 @@ export const GridRow = forwardRef<
|
|||
const panelRect = panelRef.getBoundingClientRect();
|
||||
if (type === 'drop') {
|
||||
setInteractionEvent(undefined);
|
||||
// Ensure the row re-renders to reflect the new panel order after a drag-and-drop interaction.
|
||||
// the order of rendered panels need to be aligned with how they are displayed in the grid for accessibility reasons (screen readers and focus management).
|
||||
syncPanelIds();
|
||||
} else {
|
||||
setInteractionEvent({
|
||||
type,
|
||||
|
@ -235,14 +244,25 @@ export const GridRow = forwardRef<
|
|||
}}
|
||||
/>
|
||||
));
|
||||
}, [panelIds, rowIndex, gridLayoutStateManager, renderPanelContents, setInteractionEvent]);
|
||||
}, [
|
||||
panelIds,
|
||||
rowIndex,
|
||||
gridLayoutStateManager,
|
||||
renderPanelContents,
|
||||
setInteractionEvent,
|
||||
syncPanelIds,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div ref={rowContainer}>
|
||||
{rowIndex !== 0 && (
|
||||
<GridRowHeader
|
||||
isCollapsed={isCollapsed}
|
||||
toggleIsCollapsed={toggleIsCollapsed}
|
||||
toggleIsCollapsed={() => {
|
||||
const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value);
|
||||
newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}}
|
||||
rowTitle={rowTitle}
|
||||
/>
|
||||
)}
|
||||
|
@ -264,33 +284,3 @@ export const GridRow = forwardRef<
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
const GridRowHeader = ({
|
||||
isCollapsed,
|
||||
toggleIsCollapsed,
|
||||
rowTitle,
|
||||
}: {
|
||||
isCollapsed: boolean;
|
||||
toggleIsCollapsed: () => void;
|
||||
rowTitle?: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
aria-label={i18n.translate('kbnGridLayout.row.toggleCollapse', {
|
||||
defaultMessage: 'Toggle collapse',
|
||||
})}
|
||||
iconType={isCollapsed ? 'arrowRight' : 'arrowDown'}
|
||||
onClick={toggleIsCollapsed}
|
||||
/>
|
||||
<EuiTitle size="xs">
|
||||
<h2>{rowTitle}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
};
|
41
packages/kbn-grid-layout/grid/grid_row/grid_row_header.tsx
Normal file
41
packages/kbn-grid-layout/grid/grid_row/grid_row_header.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const GridRowHeader = ({
|
||||
isCollapsed,
|
||||
toggleIsCollapsed,
|
||||
rowTitle,
|
||||
}: {
|
||||
isCollapsed: boolean;
|
||||
toggleIsCollapsed: () => void;
|
||||
rowTitle?: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
aria-label={i18n.translate('kbnGridLayout.row.toggleCollapse', {
|
||||
defaultMessage: 'Toggle collapse',
|
||||
})}
|
||||
iconType={isCollapsed ? 'arrowRight' : 'arrowDown'}
|
||||
onClick={toggleIsCollapsed}
|
||||
/>
|
||||
<EuiTitle size="xs">
|
||||
<h2>{rowTitle}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
};
|
10
packages/kbn-grid-layout/grid/grid_row/index.ts
Normal file
10
packages/kbn-grid-layout/grid/grid_row/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
export { GridRow } from './grid_row';
|
52
packages/kbn-grid-layout/grid/test_utils/mocks.tsx
Normal file
52
packages/kbn-grid-layout/grid/test_utils/mocks.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ObservedSize } from 'use-resize-observer/polyfilled';
|
||||
import {
|
||||
ActivePanel,
|
||||
GridLayoutData,
|
||||
GridLayoutStateManager,
|
||||
PanelInteractionEvent,
|
||||
RuntimeGridSettings,
|
||||
} from '../types';
|
||||
import { getSampleLayout } from './sample_layout';
|
||||
|
||||
const DASHBOARD_MARGIN_SIZE = 8;
|
||||
const DASHBOARD_GRID_HEIGHT = 20;
|
||||
const DASHBOARD_GRID_COLUMN_COUNT = 48;
|
||||
const gridLayout$ = new BehaviorSubject<GridLayoutData>(getSampleLayout());
|
||||
|
||||
export const gridSettings = {
|
||||
gutterSize: DASHBOARD_MARGIN_SIZE,
|
||||
rowHeight: DASHBOARD_GRID_HEIGHT,
|
||||
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
|
||||
};
|
||||
|
||||
export const mockRenderPanelContents = jest.fn((panelId) => (
|
||||
<button aria-label={`panelId:${panelId}`}>panel content {panelId}</button>
|
||||
));
|
||||
|
||||
const runtimeSettings$ = new BehaviorSubject<RuntimeGridSettings>({
|
||||
...gridSettings,
|
||||
columnPixelWidth: 0,
|
||||
});
|
||||
|
||||
export const gridLayoutStateManagerMock: GridLayoutStateManager = {
|
||||
expandedPanelId$: new BehaviorSubject<string | undefined>(undefined),
|
||||
isMobileView$: new BehaviorSubject<boolean>(false),
|
||||
gridLayout$,
|
||||
runtimeSettings$,
|
||||
panelRefs: { current: [] },
|
||||
rowRefs: { current: [] },
|
||||
interactionEvent$: new BehaviorSubject<PanelInteractionEvent | undefined>(undefined),
|
||||
activePanel$: new BehaviorSubject<ActivePanel | undefined>(undefined),
|
||||
gridDimensions$: new BehaviorSubject<ObservedSize>({ width: 600, height: 900 }),
|
||||
};
|
101
packages/kbn-grid-layout/grid/test_utils/sample_layout.ts
Normal file
101
packages/kbn-grid-layout/grid/test_utils/sample_layout.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { GridLayoutData } from '../types';
|
||||
|
||||
export const getSampleLayout = (): GridLayoutData => [
|
||||
{
|
||||
title: 'Large section',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: {
|
||||
id: 'panel1',
|
||||
row: 0,
|
||||
column: 0,
|
||||
width: 12,
|
||||
height: 6,
|
||||
},
|
||||
panel2: {
|
||||
id: 'panel2',
|
||||
row: 6,
|
||||
column: 0,
|
||||
width: 8,
|
||||
height: 4,
|
||||
},
|
||||
panel3: {
|
||||
id: 'panel3',
|
||||
row: 6,
|
||||
column: 8,
|
||||
width: 12,
|
||||
height: 4,
|
||||
},
|
||||
panel4: {
|
||||
id: 'panel4',
|
||||
row: 10,
|
||||
column: 0,
|
||||
width: 48,
|
||||
height: 4,
|
||||
},
|
||||
panel5: {
|
||||
id: 'panel5',
|
||||
row: 0,
|
||||
column: 12,
|
||||
width: 36,
|
||||
height: 6,
|
||||
},
|
||||
panel6: {
|
||||
id: 'panel6',
|
||||
row: 6,
|
||||
column: 24,
|
||||
width: 24,
|
||||
height: 4,
|
||||
},
|
||||
panel7: {
|
||||
id: 'panel7',
|
||||
row: 6,
|
||||
column: 20,
|
||||
width: 4,
|
||||
height: 2,
|
||||
},
|
||||
panel8: {
|
||||
id: 'panel8',
|
||||
row: 8,
|
||||
column: 20,
|
||||
width: 4,
|
||||
height: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Small section',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel9: {
|
||||
id: 'panel9',
|
||||
row: 0,
|
||||
column: 0,
|
||||
width: 12,
|
||||
height: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Another small section',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel10: {
|
||||
id: 'panel10',
|
||||
row: 0,
|
||||
column: 24,
|
||||
width: 12,
|
||||
height: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -87,6 +87,7 @@ export const useGridLayoutEvents = ({
|
|||
bottom: mouseTargetPixel.y - interactionEvent.mouseOffsets.bottom,
|
||||
right: mouseTargetPixel.x - interactionEvent.mouseOffsets.right,
|
||||
};
|
||||
|
||||
gridLayoutStateManager.activePanel$.next({ id: interactionEvent.id, position: previewRect });
|
||||
|
||||
// find the grid that the preview rect is over
|
||||
|
|
|
@ -34,11 +34,11 @@ const getAllCollisionsWithPanel = (
|
|||
return collidingPanels;
|
||||
};
|
||||
|
||||
export const getKeysInOrder = (rowData: GridRowData, draggedId?: string): string[] => {
|
||||
const panelKeys = Object.keys(rowData.panels);
|
||||
export const getKeysInOrder = (panels: GridRowData['panels'], draggedId?: string): string[] => {
|
||||
const panelKeys = Object.keys(panels);
|
||||
return panelKeys.sort((panelKeyA, panelKeyB) => {
|
||||
const panelA = rowData.panels[panelKeyA];
|
||||
const panelB = rowData.panels[panelKeyB];
|
||||
const panelA = panels[panelKeyA];
|
||||
const panelB = panels[panelKeyB];
|
||||
|
||||
// sort by row first
|
||||
if (panelA.row > panelB.row) return 1;
|
||||
|
@ -60,7 +60,7 @@ export const getKeysInOrder = (rowData: GridRowData, draggedId?: string): string
|
|||
const compactGridRow = (originalLayout: GridRowData) => {
|
||||
const nextRowData = { ...originalLayout, panels: { ...originalLayout.panels } };
|
||||
// compact all vertical space.
|
||||
const sortedKeysAfterMove = getKeysInOrder(nextRowData);
|
||||
const sortedKeysAfterMove = getKeysInOrder(nextRowData.panels);
|
||||
for (const panelKey of sortedKeysAfterMove) {
|
||||
const panel = nextRowData.panels[panelKey];
|
||||
// try moving panel up one row at a time until it collides
|
||||
|
@ -90,7 +90,7 @@ export const resolveGridRow = (
|
|||
// return nextRowData;
|
||||
|
||||
// push all panels down if they collide with another panel
|
||||
const sortedKeys = getKeysInOrder(nextRowData, dragRequest?.id);
|
||||
const sortedKeys = getKeysInOrder(nextRowData.panels, dragRequest?.id);
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
const panel = nextRowData.panels[key];
|
||||
|
|
|
@ -2,12 +2,6 @@
|
|||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react",
|
||||
"@emotion/react/types/css-prop"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue