mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Dashboard][Collapsable Panels] Respond to touch events (#204225)
## Summary Adds support to touch events. The difference between these ones and mouse events is that once they are active, the scroll is off (just like in the current Dashboard) https://github.com/user-attachments/assets/4cdcc850-7391-441e-ab9a-0abbe70e4e56 Fixes https://github.com/elastic/kibana/issues/202014
This commit is contained in:
parent
c398818d72
commit
ea6d7bef93
8 changed files with 220 additions and 70 deletions
|
@ -14,6 +14,13 @@ import { GridLayout, GridLayoutProps } from './grid_layout';
|
|||
import { gridSettings, mockRenderPanelContents } from './test_utils/mocks';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
class TouchEventFake extends Event {
|
||||
constructor(public touches: Array<{ clientX: number; clientY: number }>) {
|
||||
super('touchmove');
|
||||
this.touches = [{ clientX: 256, clientY: 128 }];
|
||||
}
|
||||
}
|
||||
|
||||
describe('GridLayout', () => {
|
||||
const renderGridLayout = (propsOverrides: Partial<GridLayoutProps> = {}) => {
|
||||
const defaultProps: GridLayoutProps = {
|
||||
|
@ -38,17 +45,30 @@ describe('GridLayout', () => {
|
|||
.getAllByRole('button', { name: /panelId:panel/i })
|
||||
.map((el) => el.getAttribute('aria-label')?.replace(/panelId:/g, ''));
|
||||
|
||||
const startDragging = (handle: HTMLElement, options = { clientX: 0, clientY: 0 }) => {
|
||||
const mouseStartDragging = (handle: HTMLElement, options = { clientX: 0, clientY: 0 }) => {
|
||||
fireEvent.mouseDown(handle, options);
|
||||
};
|
||||
|
||||
const moveTo = (options = { clientX: 256, clientY: 128 }) => {
|
||||
const mouseMoveTo = (options = { clientX: 256, clientY: 128 }) => {
|
||||
fireEvent.mouseMove(document, options);
|
||||
};
|
||||
|
||||
const drop = (handle: HTMLElement) => {
|
||||
const mouseDrop = (handle: HTMLElement) => {
|
||||
fireEvent.mouseUp(handle);
|
||||
};
|
||||
const touchStart = (handle: HTMLElement, options = { touches: [{ clientX: 0, clientY: 0 }] }) => {
|
||||
fireEvent.touchStart(handle, options);
|
||||
};
|
||||
const touchMoveTo = (options = { touches: [{ clientX: 256, clientY: 128 }] }) => {
|
||||
const realTouchEvent = window.TouchEvent;
|
||||
// @ts-expect-error
|
||||
window.TouchEvent = TouchEventFake;
|
||||
fireEvent.touchMove(document, new TouchEventFake(options.touches));
|
||||
window.TouchEvent = realTouchEvent;
|
||||
};
|
||||
const touchEnd = (handle: HTMLElement) => {
|
||||
fireEvent.touchEnd(handle);
|
||||
};
|
||||
|
||||
const assertTabThroughPanel = async (panelId: string) => {
|
||||
await userEvent.tab(); // tab to drag handle
|
||||
|
@ -81,11 +101,11 @@ describe('GridLayout', () => {
|
|||
jest.clearAllMocks();
|
||||
|
||||
const panel1DragHandle = screen.getAllByRole('button', { name: /drag to move/i })[0];
|
||||
startDragging(panel1DragHandle);
|
||||
moveTo({ clientX: 256, clientY: 128 });
|
||||
mouseStartDragging(panel1DragHandle);
|
||||
mouseMoveTo({ clientX: 256, clientY: 128 });
|
||||
expect(mockRenderPanelContents).toHaveBeenCalledTimes(0); // renderPanelContents should not be called during dragging
|
||||
|
||||
drop(panel1DragHandle);
|
||||
mouseDrop(panel1DragHandle);
|
||||
expect(mockRenderPanelContents).toHaveBeenCalledTimes(0); // renderPanelContents should not be called after reordering
|
||||
});
|
||||
|
||||
|
@ -107,12 +127,34 @@ describe('GridLayout', () => {
|
|||
renderGridLayout();
|
||||
|
||||
const panel1DragHandle = screen.getAllByRole('button', { name: /drag to move/i })[0];
|
||||
startDragging(panel1DragHandle);
|
||||
mouseStartDragging(panel1DragHandle);
|
||||
|
||||
moveTo({ clientX: 256, clientY: 128 });
|
||||
expect(getAllThePanelIds()).toEqual(expectedInitialOrder); // the panels shouldn't be reordered till we drop
|
||||
mouseMoveTo({ clientX: 256, clientY: 128 });
|
||||
expect(getAllThePanelIds()).toEqual(expectedInitialOrder); // the panels shouldn't be reordered till we mouseDrop
|
||||
|
||||
drop(panel1DragHandle);
|
||||
mouseDrop(panel1DragHandle);
|
||||
expect(getAllThePanelIds()).toEqual([
|
||||
'panel2',
|
||||
'panel5',
|
||||
'panel3',
|
||||
'panel7',
|
||||
'panel1',
|
||||
'panel8',
|
||||
'panel6',
|
||||
'panel4',
|
||||
'panel9',
|
||||
'panel10',
|
||||
]);
|
||||
});
|
||||
it('after reordering some panels via touch events', async () => {
|
||||
renderGridLayout();
|
||||
|
||||
const panel1DragHandle = screen.getAllByRole('button', { name: /drag to move/i })[0];
|
||||
touchStart(panel1DragHandle);
|
||||
touchMoveTo({ touches: [{ clientX: 256, clientY: 128 }] });
|
||||
expect(getAllThePanelIds()).toEqual(expectedInitialOrder); // the panels shouldn't be reordered till we mouseDrop
|
||||
|
||||
touchEnd(panel1DragHandle);
|
||||
expect(getAllThePanelIds()).toEqual([
|
||||
'panel2',
|
||||
'panel5',
|
||||
|
|
|
@ -12,7 +12,14 @@ import React, { useCallback, useEffect, useImperativeHandle, useRef, useState }
|
|||
import { EuiIcon, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { GridLayoutStateManager, PanelInteractionEvent } from '../types';
|
||||
import {
|
||||
GridLayoutStateManager,
|
||||
PanelInteractionEvent,
|
||||
UserInteractionEvent,
|
||||
UserMouseEvent,
|
||||
UserTouchEvent,
|
||||
} from '../types';
|
||||
import { isMouseEvent, isTouchEvent } from '../utils/sensors';
|
||||
|
||||
export interface DragHandleApi {
|
||||
setDragHandles: (refs: Array<HTMLElement | null>) => void;
|
||||
|
@ -24,7 +31,7 @@ export const DragHandle = React.forwardRef<
|
|||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: MouseEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
e: UserInteractionEvent
|
||||
) => void;
|
||||
}
|
||||
>(({ gridLayoutStateManager, interactionStart }, ref) => {
|
||||
|
@ -35,13 +42,20 @@ export const DragHandle = React.forwardRef<
|
|||
const dragHandleRefs = useRef<Array<HTMLElement | null>>([]);
|
||||
|
||||
/**
|
||||
* We need to memoize the `onMouseDown` callback so that we don't assign a new `onMouseDown` event handler
|
||||
* We need to memoize the `onDragStart` and `onDragEnd` callbacks so that we don't assign a new event handler
|
||||
* every time `setDragHandles` is called
|
||||
*/
|
||||
const onMouseDown = useCallback(
|
||||
(e: MouseEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if (gridLayoutStateManager.accessMode$.getValue() !== 'EDIT' || e.button !== 0) {
|
||||
// ignore anything but left clicks, and ignore clicks when not in edit mode
|
||||
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();
|
||||
|
@ -50,6 +64,14 @@ export const DragHandle = React.forwardRef<
|
|||
[interactionStart, gridLayoutStateManager.accessMode$]
|
||||
);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
(e: UserTouchEvent | UserMouseEvent) => {
|
||||
e.stopPropagation();
|
||||
interactionStart('drop', e);
|
||||
},
|
||||
[interactionStart]
|
||||
);
|
||||
|
||||
const setDragHandles = useCallback(
|
||||
(dragHandles: Array<HTMLElement | null>) => {
|
||||
setDragHandleCount(dragHandles.length);
|
||||
|
@ -57,17 +79,21 @@ export const DragHandle = React.forwardRef<
|
|||
|
||||
for (const handle of dragHandles) {
|
||||
if (handle === null) return;
|
||||
handle.addEventListener('mousedown', onMouseDown, { passive: true });
|
||||
handle.addEventListener('mousedown', onDragStart, { passive: true });
|
||||
handle.addEventListener('touchstart', onDragStart, { passive: false });
|
||||
handle.addEventListener('touchend', onDragEnd, { passive: true });
|
||||
}
|
||||
|
||||
removeEventListenersRef.current = () => {
|
||||
for (const handle of dragHandles) {
|
||||
if (handle === null) return;
|
||||
handle.removeEventListener('mousedown', onMouseDown);
|
||||
handle.removeEventListener('mousedown', onDragStart);
|
||||
handle.removeEventListener('touchstart', onDragStart);
|
||||
handle.removeEventListener('touchend', onDragEnd);
|
||||
}
|
||||
};
|
||||
},
|
||||
[onMouseDown]
|
||||
[onDragStart, onDragEnd]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -124,12 +150,10 @@ export const DragHandle = React.forwardRef<
|
|||
display: none;
|
||||
}
|
||||
`}
|
||||
onMouseDown={(e) => {
|
||||
interactionStart('drag', e);
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
interactionStart('drop', e);
|
||||
}}
|
||||
onMouseDown={onDragStart}
|
||||
onMouseUp={onDragEnd}
|
||||
onTouchStart={onDragStart}
|
||||
onTouchEnd={onDragEnd}
|
||||
>
|
||||
<EuiIcon type="grabOmnidirectional" />
|
||||
</button>
|
||||
|
|
|
@ -13,7 +13,7 @@ import { combineLatest, skip } from 'rxjs';
|
|||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { GridLayoutStateManager, PanelInteractionEvent } from '../types';
|
||||
import { GridLayoutStateManager, UserInteractionEvent, PanelInteractionEvent } from '../types';
|
||||
import { getKeysInOrder } from '../utils/resolve_grid_row';
|
||||
import { DragHandle, DragHandleApi } from './drag_handle';
|
||||
import { ResizeHandle } from './resize_handle';
|
||||
|
@ -25,10 +25,7 @@ export interface GridPanelProps {
|
|||
panelId: string,
|
||||
setDragHandles?: (refs: Array<HTMLElement | null>) => void
|
||||
) => React.ReactNode;
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: MouseEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
interactionStart: (type: PanelInteractionEvent['type'] | 'drop', e: UserInteractionEvent) => void;
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,15 +12,12 @@ import { css } from '@emotion/react';
|
|||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { PanelInteractionEvent } from '../types';
|
||||
import { UserInteractionEvent, PanelInteractionEvent } from '../types';
|
||||
|
||||
export const ResizeHandle = ({
|
||||
interactionStart,
|
||||
}: {
|
||||
interactionStart: (
|
||||
type: PanelInteractionEvent['type'] | 'drop',
|
||||
e: MouseEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void;
|
||||
interactionStart: (type: PanelInteractionEvent['type'] | 'drop', e: UserInteractionEvent) => void;
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
|
@ -32,6 +29,12 @@ export const ResizeHandle = ({
|
|||
onMouseUp={(e) => {
|
||||
interactionStart('drop', e);
|
||||
}}
|
||||
onTouchStart={(e) => {
|
||||
interactionStart('resize', e);
|
||||
}}
|
||||
onTouchEnd={(e) => {
|
||||
interactionStart('drop', e);
|
||||
}}
|
||||
aria-label={i18n.translate('kbnGridLayout.resizeHandle.ariaLabel', {
|
||||
defaultMessage: 'Resize panel',
|
||||
})}
|
||||
|
|
|
@ -16,9 +16,15 @@ import { css } from '@emotion/react';
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { DragPreview } from '../drag_preview';
|
||||
import { GridPanel } from '../grid_panel';
|
||||
import { GridLayoutStateManager, GridRowData, PanelInteractionEvent } from '../types';
|
||||
import {
|
||||
GridLayoutStateManager,
|
||||
GridRowData,
|
||||
UserInteractionEvent,
|
||||
PanelInteractionEvent,
|
||||
} from '../types';
|
||||
import { getKeysInOrder } from '../utils/resolve_grid_row';
|
||||
import { GridRowHeader } from './grid_row_header';
|
||||
import { isTouchEvent, isMouseEvent } from '../utils/sensors';
|
||||
|
||||
export interface GridRowProps {
|
||||
rowIndex: number;
|
||||
|
@ -214,7 +220,6 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
|||
const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
if (!panelRef) return;
|
||||
|
||||
const panelRect = panelRef.getBoundingClientRect();
|
||||
if (type === 'drop') {
|
||||
setInteractionEvent(undefined);
|
||||
/**
|
||||
|
@ -226,17 +231,15 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
|||
getKeysInOrder(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels)
|
||||
);
|
||||
} else {
|
||||
const panelRect = panelRef.getBoundingClientRect();
|
||||
const pointerOffsets = getPointerOffsets(e, panelRect);
|
||||
|
||||
setInteractionEvent({
|
||||
type,
|
||||
id: panelId,
|
||||
panelDiv: panelRef,
|
||||
targetRowIndex: rowIndex,
|
||||
mouseOffsets: {
|
||||
top: e.clientY - panelRect.top,
|
||||
left: e.clientX - panelRect.left,
|
||||
right: e.clientX - panelRect.right,
|
||||
bottom: e.clientY - panelRect.bottom,
|
||||
},
|
||||
pointerOffsets,
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
@ -285,3 +288,32 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
const defaultPointerOffsets = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
function getPointerOffsets(e: UserInteractionEvent, panelRect: DOMRect) {
|
||||
if (isTouchEvent(e)) {
|
||||
if (e.touches.length > 1) return defaultPointerOffsets;
|
||||
const touch = e.touches[0];
|
||||
return {
|
||||
top: touch.clientY - panelRect.top,
|
||||
left: touch.clientX - panelRect.left,
|
||||
right: touch.clientX - panelRect.right,
|
||||
bottom: touch.clientY - panelRect.bottom,
|
||||
};
|
||||
}
|
||||
if (isMouseEvent(e)) {
|
||||
return {
|
||||
top: e.clientY - panelRect.top,
|
||||
left: e.clientX - panelRect.left,
|
||||
right: e.clientX - panelRect.right,
|
||||
bottom: e.clientY - panelRect.bottom,
|
||||
};
|
||||
}
|
||||
throw new Error('Invalid event type');
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ export interface PanelInteractionEvent {
|
|||
* The pixel offsets from where the mouse was at drag start to the
|
||||
* edges of the panel
|
||||
*/
|
||||
mouseOffsets: {
|
||||
pointerOffsets: {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
|
@ -122,3 +122,9 @@ export interface PanelPlacementSettings {
|
|||
}
|
||||
|
||||
export type GridAccessMode = 'VIEW' | 'EDIT';
|
||||
|
||||
export type UserMouseEvent = MouseEvent | React.MouseEvent<HTMLButtonElement, MouseEvent>;
|
||||
|
||||
export type UserTouchEvent = TouchEvent | React.TouchEvent<HTMLButtonElement>;
|
||||
|
||||
export type UserInteractionEvent = React.UIEvent<HTMLElement> | Event;
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useEffect, useRef } from 'react';
|
|||
import { resolveGridRow } from './utils/resolve_grid_row';
|
||||
import { GridPanelData, GridLayoutStateManager } from './types';
|
||||
import { isGridDataEqual } from './utils/equality_checks';
|
||||
import { isMouseEvent, isTouchEvent } from './utils/sensors';
|
||||
|
||||
const MIN_SPEED = 50;
|
||||
const MAX_SPEED = 150;
|
||||
|
@ -57,7 +58,7 @@ export const useGridLayoutEvents = ({
|
|||
}: {
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}) => {
|
||||
const mouseClientPosition = useRef({ x: 0, y: 0 });
|
||||
const pointerClientPosition = useRef({ x: 0, y: 0 });
|
||||
const lastRequestedPanelPosition = useRef<GridPanelData | undefined>(undefined);
|
||||
const scrollInterval = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
|
@ -73,18 +74,22 @@ export const useGridLayoutEvents = ({
|
|||
scrollInterval.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const calculateUserEvent = (e: Event) => {
|
||||
if (!interactionEvent$.value) {
|
||||
const interactionEvent = interactionEvent$.value;
|
||||
if (!interactionEvent) {
|
||||
// if no interaction event, stop auto scroll (if necessary) and return early
|
||||
stopAutoScrollIfNecessary();
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
// make sure when the user is dragging through touchmove, the page doesn't scroll
|
||||
if (isTouchEvent(e)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const gridRowElements = gridLayoutStateManager.rowRefs.current;
|
||||
|
||||
const interactionEvent = interactionEvent$.value;
|
||||
const isResize = interactionEvent?.type === 'resize';
|
||||
|
||||
const currentLayout = gridLayout$.value;
|
||||
|
@ -99,16 +104,22 @@ export const useGridLayoutEvents = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const mouseTargetPixel = {
|
||||
x: mouseClientPosition.current.x,
|
||||
y: mouseClientPosition.current.y,
|
||||
const pointerClientPixel = {
|
||||
x: pointerClientPosition.current.x,
|
||||
y: pointerClientPosition.current.y,
|
||||
};
|
||||
const panelRect = interactionEvent.panelDiv.getBoundingClientRect();
|
||||
|
||||
const { columnCount, gutterSize, rowHeight, columnPixelWidth } = runtimeSettings$.value;
|
||||
const gridWidth = (gutterSize + columnPixelWidth) * columnCount + gutterSize * 2;
|
||||
|
||||
const previewRect = {
|
||||
left: isResize ? panelRect.left : mouseTargetPixel.x - interactionEvent.mouseOffsets.left,
|
||||
top: isResize ? panelRect.top : mouseTargetPixel.y - interactionEvent.mouseOffsets.top,
|
||||
bottom: mouseTargetPixel.y - interactionEvent.mouseOffsets.bottom,
|
||||
right: mouseTargetPixel.x - interactionEvent.mouseOffsets.right,
|
||||
left: isResize
|
||||
? panelRect.left
|
||||
: pointerClientPixel.x - interactionEvent.pointerOffsets.left,
|
||||
top: isResize ? panelRect.top : pointerClientPixel.y - interactionEvent.pointerOffsets.top,
|
||||
bottom: pointerClientPixel.y - interactionEvent.pointerOffsets.bottom,
|
||||
right: Math.min(pointerClientPixel.x - interactionEvent.pointerOffsets.right, gridWidth),
|
||||
};
|
||||
|
||||
gridLayoutStateManager.activePanel$.next({ id: interactionEvent.id, position: previewRect });
|
||||
|
@ -119,6 +130,10 @@ export const useGridLayoutEvents = ({
|
|||
const lastRowIndex = interactionEvent?.targetRowIndex;
|
||||
const targetRowIndex = (() => {
|
||||
if (isResize) return lastRowIndex;
|
||||
// TODO: a temporary workaround for the issue where the panel moves to a different row when the user uses touch events.
|
||||
// Touch events don't work properly when the DOM element is removed and replaced (which is how we handle moving to another row) so we blocked the ability to move panels to another row.
|
||||
// Reference: https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed
|
||||
if (isTouchEvent(e)) return lastRowIndex;
|
||||
|
||||
let highestOverlap = -Infinity;
|
||||
let highestOverlapRowIndex = -1;
|
||||
|
@ -145,7 +160,6 @@ export const useGridLayoutEvents = ({
|
|||
}
|
||||
|
||||
// calculate the requested grid position
|
||||
const { columnCount, gutterSize, rowHeight, columnPixelWidth } = runtimeSettings$.value;
|
||||
const targetedGridRow = gridRowElements[targetRowIndex];
|
||||
const targetedGridLeft = targetedGridRow?.getBoundingClientRect().left ?? 0;
|
||||
const targetedGridTop = targetedGridRow?.getBoundingClientRect().top ?? 0;
|
||||
|
@ -176,19 +190,21 @@ export const useGridLayoutEvents = ({
|
|||
|
||||
// auto scroll when an event is happening close to the top or bottom of the screen
|
||||
const heightPercentage =
|
||||
100 - ((window.innerHeight - mouseTargetPixel.y) / window.innerHeight) * 100;
|
||||
100 - ((window.innerHeight - pointerClientPixel.y) / window.innerHeight) * 100;
|
||||
const atTheTop = window.scrollY <= 0;
|
||||
const atTheBottom = window.innerHeight + window.scrollY >= document.body.scrollHeight;
|
||||
|
||||
const startScrollingUp = !isResize && heightPercentage < 5 && !atTheTop; // don't scroll up when resizing
|
||||
const startScrollingDown = heightPercentage > 95 && !atTheBottom;
|
||||
if (startScrollingUp || startScrollingDown) {
|
||||
if (!scrollInterval.current) {
|
||||
// only start scrolling if it's not already happening
|
||||
scrollInterval.current = scrollOnInterval(startScrollingUp ? 'up' : 'down');
|
||||
if (!isTouchEvent(e)) {
|
||||
const startScrollingUp = !isResize && heightPercentage < 5 && !atTheTop; // don't scroll up when resizing
|
||||
const startScrollingDown = heightPercentage > 95 && !atTheBottom;
|
||||
if (startScrollingUp || startScrollingDown) {
|
||||
if (!scrollInterval.current) {
|
||||
// only start scrolling if it's not already happening
|
||||
scrollInterval.current = scrollOnInterval(startScrollingUp ? 'up' : 'down');
|
||||
}
|
||||
} else {
|
||||
stopAutoScrollIfNecessary();
|
||||
}
|
||||
} else {
|
||||
stopAutoScrollIfNecessary();
|
||||
}
|
||||
|
||||
// resolve the new grid layout
|
||||
|
@ -221,20 +237,32 @@ export const useGridLayoutEvents = ({
|
|||
}
|
||||
};
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const onPointerMove = (e: Event) => {
|
||||
// Note: When an item is being interacted with, `mousemove` events continue to be fired, even when the
|
||||
// mouse moves out of the window (i.e. when a panel is being dragged around outside the window).
|
||||
mouseClientPosition.current = { x: e.clientX, y: e.clientY };
|
||||
pointerClientPosition.current = getPointerClientPosition(e);
|
||||
calculateUserEvent(e);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
||||
document.addEventListener('mousemove', onPointerMove, { passive: true });
|
||||
document.addEventListener('scroll', calculateUserEvent, { passive: true });
|
||||
document.addEventListener('touchmove', onPointerMove, { passive: false });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mousemove', onPointerMove);
|
||||
document.removeEventListener('scroll', calculateUserEvent);
|
||||
document.removeEventListener('touchmove', onPointerMove);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
};
|
||||
|
||||
function getPointerClientPosition(e: Event) {
|
||||
if (isTouchEvent(e)) {
|
||||
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||
}
|
||||
if (isMouseEvent(e)) {
|
||||
return { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
throw new Error('Unknown event type');
|
||||
}
|
||||
|
|
18
packages/kbn-grid-layout/grid/utils/sensors.ts
Normal file
18
packages/kbn-grid-layout/grid/utils/sensors.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { UserMouseEvent, UserTouchEvent } from '../types';
|
||||
|
||||
export const isTouchEvent = (e: Event | React.UIEvent<HTMLElement>): e is UserTouchEvent => {
|
||||
return 'touches' in e;
|
||||
};
|
||||
|
||||
export const isMouseEvent = (e: Event | React.UIEvent<HTMLElement>): e is UserMouseEvent => {
|
||||
return 'clientX' in e;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue