Skip to content

Commit

Permalink
add touch event
Browse files Browse the repository at this point in the history
  • Loading branch information
mbondyra committed Dec 13, 2024
1 parent 6bf5e68 commit a28959b
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 46 deletions.
43 changes: 35 additions & 8 deletions packages/kbn-grid-layout/grid/grid_panel/drag_handle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import { EuiIcon, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { i18n } from '@kbn/i18n';
import { GridLayoutStateManager, PanelInteractionEvent } from '../types';
import {
GridLayoutStateManager,
PanelInteractionEvent,
UserInteractionEvent,
UserMouseEvent,
UserTouchEvent,
} from '../types';

export interface DragHandleApi {
setDragHandles: (refs: Array<HTMLElement | null>) => void;
Expand All @@ -25,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) => {
Expand All @@ -36,11 +42,11 @@ 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 `onMouseDown`, `onTouchStart` and `onTouchEnd` 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>) => {
(e: UserMouseEvent) => {
if (gridLayoutStateManager.accessMode$.getValue() !== 'EDIT' || e.button !== 0) {
// ignore anything but left clicks, and ignore clicks when not in edit mode
return;
Expand All @@ -50,25 +56,46 @@ export const DragHandle = React.forwardRef<
},
[interactionStart, gridLayoutStateManager.accessMode$]
);
const onTouchStart = useCallback(
(e: UserTouchEvent) => {
e.stopPropagation();
interactionStart('drag', e);
},
[interactionStart]
);
const onTouchEnd = useCallback(
(e: UserTouchEvent) => {
e.stopPropagation();
interactionStart('drop', e);
},
[interactionStart]
);

const setDragHandles = useCallback(
(dragHandles: Array<HTMLElement | null>) => {
if (gridLayoutStateManager.accessMode$.getValue() !== 'EDIT') {
return;
}
setDragHandleCount(dragHandles.length);
dragHandleRefs.current = dragHandles;

for (const handle of dragHandles) {
if (handle === null) return;
handle.addEventListener('mousedown', onMouseDown, { passive: true });
handle.addEventListener('touchstart', onTouchStart, { passive: false });
handle.addEventListener('touchend', onTouchEnd, { passive: true });
}

removeEventListenersRef.current = () => {
for (const handle of dragHandles) {
if (handle === null) return;
handle.removeEventListener('mousedown', onMouseDown);
handle.removeEventListener('touchstart', onTouchStart);
handle.removeEventListener('touchend', onTouchEnd);
}
};
},
[onMouseDown]
[onMouseDown, onTouchStart, onTouchEnd, gridLayoutStateManager.accessMode$]
);

useEffect(() => {
Expand Down Expand Up @@ -125,12 +152,12 @@ export const DragHandle = React.forwardRef<
display: none;
}
`}
onMouseDown={(e) => {
interactionStart('drag', e);
}}
onMouseDown={onMouseDown}
onMouseUp={(e) => {
interactionStart('drop', e);
}}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
>
<EuiIcon type="grabOmnidirectional" />
</button>
Expand Down
7 changes: 2 additions & 5 deletions packages/kbn-grid-layout/grid/grid_panel/grid_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { combineLatest, skip } from 'rxjs';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';

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';
Expand All @@ -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;
}

Expand Down
13 changes: 8 additions & 5 deletions packages/kbn-grid-layout/grid/grid_panel/resize_handle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@ import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-theme';
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;
}) => {
return (
<button
Expand All @@ -31,6 +28,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',
})}
Expand Down
49 changes: 41 additions & 8 deletions packages/kbn-grid-layout/grid/grid_row/grid_row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import { euiThemeVars } from '@kbn/ui-theme';
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,
UserTouchEvent,
} from '../types';
import { getKeysInOrder } from '../utils/resolve_grid_row';
import { GridRowHeader } from './grid_row_header';

Expand Down Expand Up @@ -213,7 +219,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);
/**
Expand All @@ -225,17 +230,15 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
getKeysInOrder(gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels)
);
} else {
const panelRect = panelRef.getBoundingClientRect();
const sensorOffsets = getSensorOffsets(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,
},
sensorOffsets,
});
}
}}
Expand Down Expand Up @@ -284,3 +287,33 @@ export const GridRow = forwardRef<HTMLDivElement, GridRowProps>(
);
}
);

const isTouchEvent = (e: UserInteractionEvent): e is UserTouchEvent => {
return 'touches' in e;
};

const defaultSensorOffsets = {
top: 0,
left: 0,
right: 0,
bottom: 0,
};

function getSensorOffsets(e: UserInteractionEvent, panelRect: DOMRect) {
if (isTouchEvent(e)) {
if (e.touches.length > 1) return defaultSensorOffsets;
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,
};
}
return {
top: e.clientY - panelRect.top,
left: e.clientX - panelRect.left,
right: e.clientX - panelRect.right,
bottom: e.clientY - panelRect.bottom,
};
}
8 changes: 7 additions & 1 deletion packages/kbn-grid-layout/grid/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
sensorOffsets: {
top: number;
left: number;
right: number;
Expand All @@ -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 = UserMouseEvent | UserTouchEvent;
55 changes: 36 additions & 19 deletions packages/kbn-grid-layout/grid/use_grid_layout_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const useGridLayoutEvents = ({
}: {
gridLayoutStateManager: GridLayoutStateManager;
}) => {
const mouseClientPosition = useRef({ x: 0, y: 0 });
const sensorClientPosition = useRef({ x: 0, y: 0 });
const lastRequestedPanelPosition = useRef<GridPanelData | undefined>(undefined);
const scrollInterval = useRef<NodeJS.Timeout | null>(null);

Expand All @@ -74,12 +74,13 @@ export const useGridLayoutEvents = ({
}
};

const calculateUserEvent = (e: Event) => {
const calculateUserEvent = (e: Event, shouldAutoScroll = true) => {
if (!interactionEvent$.value) {
// if no interaction event, stop auto scroll (if necessary) and return early
stopAutoScrollIfNecessary();
return;
}

e.stopPropagation();

const gridRowElements = gridLayoutStateManager.rowRefs.current;
Expand All @@ -99,16 +100,16 @@ export const useGridLayoutEvents = ({
return;
}

const mouseTargetPixel = {
x: mouseClientPosition.current.x,
y: mouseClientPosition.current.y,
const sensorClientPixel = {
x: sensorClientPosition.current.x,
y: sensorClientPosition.current.y,
};
const panelRect = interactionEvent.panelDiv.getBoundingClientRect();
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 : sensorClientPixel.x - interactionEvent.sensorOffsets.left,
top: isResize ? panelRect.top : sensorClientPixel.y - interactionEvent.sensorOffsets.top,
bottom: sensorClientPixel.y - interactionEvent.sensorOffsets.bottom,
right: sensorClientPixel.x - interactionEvent.sensorOffsets.right,
};

gridLayoutStateManager.activePanel$.next({ id: interactionEvent.id, position: previewRect });
Expand Down Expand Up @@ -176,19 +177,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 - sensorClientPixel.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 (shouldAutoScroll) {
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
Expand Down Expand Up @@ -224,16 +227,30 @@ export const useGridLayoutEvents = ({
const onMouseMove = (e: MouseEvent) => {
// 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 };
sensorClientPosition.current = { x: e.clientX, y: e.clientY };
calculateUserEvent(e);
};

const onTouchMove = (e: TouchEvent) => {
if (!interactionEvent$.value) {
return;
}
if (e.touches.length > 1) return;
const touch = e.touches[0];
sensorClientPosition.current = { x: touch.clientX, y: touch.clientY };
e.preventDefault();
e.stopPropagation();
calculateUserEvent(e, false);
};

document.addEventListener('mousemove', onMouseMove, { passive: true });
document.addEventListener('scroll', calculateUserEvent, { passive: true });
document.addEventListener('touchmove', onTouchMove, { passive: false });

return () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('scroll', calculateUserEvent);
document.removeEventListener('touchmove', onTouchMove);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand Down

0 comments on commit a28959b

Please sign in to comment.