addons/isl/src/DragHandle.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type * as stylex from '@stylexjs/stylex';
b69ab319import type {PointerEventHandler, ReactElement} from 'react';
b69ab3110
b69ab3111import {Icon} from 'isl-components/Icon';
b69ab3112import {stylexPropsWithClassName} from 'isl-components/utils';
b69ab3113
b69ab3114export type DragHandler = (x: number, y: number, isDragging: boolean) => void;
b69ab3115
b69ab3116/**
b69ab3117 * A drag handle that fires events on drag-n-drop.
b69ab3118 *
b69ab3119 * At the start of dragging, or during dragging, call `onDrag(x, y, true)`.
b69ab3120 * At the end of dragging, call `onDrag(x, y, false)`.
b69ab3121 * `x`, `y` are relative to viewport, comparable to `getBoundingClientRect()`.
b69ab3122 *
b69ab3123 * This component renders children or the "gripper" icon to grab and updates
b69ab3124 * the cursor style. It does not draw the element being dragged during
b69ab3125 * dragging. The callstie might use a `position: fixed; left: 0; top: 0`
b69ab3126 * element and move it using `transform: translate(x,y)` during dragging.
b69ab3127 */
b69ab3128export function DragHandle(props: {
b69ab3129 onDrag?: DragHandler;
b69ab3130 children?: ReactElement;
b69ab3131 xstyle?: stylex.StyleXStyles;
b69ab3132}): ReactElement {
b69ab3133 return (
b69ab3134 <span
b69ab3135 {...dragHandleProps(props.onDrag)}
b69ab3136 {...stylexPropsWithClassName(props.xstyle, 'drag-handle')}>
b69ab3137 {props.children ?? <Icon icon="gripper" />}
b69ab3138 </span>
b69ab3139 );
b69ab3140}
b69ab3141
b69ab3142/**
b69ab3143 * Return React properties to handle customized dragging.
b69ab3144 *
b69ab3145 * At the start of dragging, or during dragging, call `onDrag(x, y, true)`.
b69ab3146 * At the end of dragging, call `onDrag(x, y, false)`.
b69ab3147 * `x`, `y` are relative to viewport, comparable to `getBoundingClientRect()`.
b69ab3148 */
b69ab3149export function dragHandleProps(onDrag?: DragHandler): {
b69ab3150 onDragStart?: React.DragEventHandler<unknown>;
b69ab3151 onPointerDown?: PointerEventHandler<unknown>;
b69ab3152} {
b69ab3153 if (onDrag == null) {
b69ab3154 return {};
b69ab3155 }
b69ab3156 let pointerDown = false;
b69ab3157 const handlePointerDown: PointerEventHandler = e => {
b69ab3158 if (e.isPrimary && !pointerDown) {
b69ab3159 // e.target might be unmounted and lose events, listen on `document.body` instead.
b69ab3160 const body = (e.target as HTMLSpanElement).ownerDocument.body;
b69ab3161
b69ab3162 const handlePointerMove = (e: PointerEvent) => {
b69ab3163 onDrag(e.clientX, e.clientY, true);
b69ab3164 };
b69ab3165 const handlePointerUp = (e: PointerEvent) => {
b69ab3166 body.removeEventListener('pointermove', handlePointerMove as EventListener);
b69ab3167 body.removeEventListener('pointerup', handlePointerUp as EventListener);
b69ab3168 body.removeEventListener('pointerleave', handlePointerUp as EventListener);
b69ab3169 body.releasePointerCapture(e.pointerId);
b69ab3170 body.style.removeProperty('cursor');
b69ab3171 pointerDown = false;
b69ab3172 onDrag(e.clientX, e.clientY, false);
b69ab3173 };
b69ab3174
b69ab3175 body.setPointerCapture(e.pointerId);
b69ab3176 body.addEventListener('pointermove', handlePointerMove);
b69ab3177 body.addEventListener('pointerup', handlePointerUp);
b69ab3178 body.addEventListener('pointerleave', handlePointerUp);
b69ab3179
b69ab3180 body.style.cursor = 'grabbing';
b69ab3181 pointerDown = true;
b69ab3182
b69ab3183 onDrag(e.clientX, e.clientY, true);
b69ab3184 }
b69ab3185 };
b69ab3186
b69ab3187 return {
b69ab3188 onDragStart: e => e.preventDefault(),
b69ab3189 onPointerDown: handlePointerDown,
b69ab3190 };
b69ab3191}