3.1 KB92 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type * as stylex from '@stylexjs/stylex';
9import type {PointerEventHandler, ReactElement} from 'react';
10
11import {Icon} from 'isl-components/Icon';
12import {stylexPropsWithClassName} from 'isl-components/utils';
13
14export type DragHandler = (x: number, y: number, isDragging: boolean) => void;
15
16/**
17 * A drag handle that fires events on drag-n-drop.
18 *
19 * At the start of dragging, or during dragging, call `onDrag(x, y, true)`.
20 * At the end of dragging, call `onDrag(x, y, false)`.
21 * `x`, `y` are relative to viewport, comparable to `getBoundingClientRect()`.
22 *
23 * This component renders children or the "gripper" icon to grab and updates
24 * the cursor style. It does not draw the element being dragged during
25 * dragging. The callstie might use a `position: fixed; left: 0; top: 0`
26 * element and move it using `transform: translate(x,y)` during dragging.
27 */
28export function DragHandle(props: {
29 onDrag?: DragHandler;
30 children?: ReactElement;
31 xstyle?: stylex.StyleXStyles;
32}): ReactElement {
33 return (
34 <span
35 {...dragHandleProps(props.onDrag)}
36 {...stylexPropsWithClassName(props.xstyle, 'drag-handle')}>
37 {props.children ?? <Icon icon="gripper" />}
38 </span>
39 );
40}
41
42/**
43 * Return React properties to handle customized dragging.
44 *
45 * At the start of dragging, or during dragging, call `onDrag(x, y, true)`.
46 * At the end of dragging, call `onDrag(x, y, false)`.
47 * `x`, `y` are relative to viewport, comparable to `getBoundingClientRect()`.
48 */
49export function dragHandleProps(onDrag?: DragHandler): {
50 onDragStart?: React.DragEventHandler<unknown>;
51 onPointerDown?: PointerEventHandler<unknown>;
52} {
53 if (onDrag == null) {
54 return {};
55 }
56 let pointerDown = false;
57 const handlePointerDown: PointerEventHandler = e => {
58 if (e.isPrimary && !pointerDown) {
59 // e.target might be unmounted and lose events, listen on `document.body` instead.
60 const body = (e.target as HTMLSpanElement).ownerDocument.body;
61
62 const handlePointerMove = (e: PointerEvent) => {
63 onDrag(e.clientX, e.clientY, true);
64 };
65 const handlePointerUp = (e: PointerEvent) => {
66 body.removeEventListener('pointermove', handlePointerMove as EventListener);
67 body.removeEventListener('pointerup', handlePointerUp as EventListener);
68 body.removeEventListener('pointerleave', handlePointerUp as EventListener);
69 body.releasePointerCapture(e.pointerId);
70 body.style.removeProperty('cursor');
71 pointerDown = false;
72 onDrag(e.clientX, e.clientY, false);
73 };
74
75 body.setPointerCapture(e.pointerId);
76 body.addEventListener('pointermove', handlePointerMove);
77 body.addEventListener('pointerup', handlePointerUp);
78 body.addEventListener('pointerleave', handlePointerUp);
79
80 body.style.cursor = 'grabbing';
81 pointerDown = true;
82
83 onDrag(e.clientX, e.clientY, true);
84 }
85 };
86
87 return {
88 onDragStart: e => e.preventDefault(),
89 onPointerDown: handlePointerDown,
90 };
91}
92