Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,15 @@ function expectDirectionButtonToBeVisible(direction: Direction) {
).toBe(true);
}

function renderDragHandle(props: Omit<DragHandleWrapperProps, 'children'>) {
function renderDragHandle(props: Partial<Omit<DragHandleWrapperProps, 'children'>>) {
const mergedProps: Omit<DragHandleWrapperProps, 'children'> = {
directions: {},
hideButtonsOnDrag: false,
clickDragThreshold: 3,
...props,
};
const { container } = render(
<DragHandleWrapper {...props}>
<DragHandleWrapper {...mergedProps}>
<button type="button" id="drag-button">
Drag
</button>
Expand Down Expand Up @@ -343,7 +349,7 @@ test('shows direction buttons when dragged 2 pixels', () => {
expect(getDirectionButton('block-start')).toBeVisible();
});

test("doesn't show direction buttons when dragged more than 3 pixels", () => {
test("doesn't show direction buttons when dragged more than 3 pixels (default threshold)", () => {
const { dragHandle } = renderDragHandle({
directions: { 'block-start': 'active' },
tooltipText: 'Click me!',
Expand All @@ -355,6 +361,49 @@ test("doesn't show direction buttons when dragged more than 3 pixels", () => {
expectDirectionButtonToBeHidden('block-start');
});

test('shows direction buttons when dragged less than custom clickDragThreshold', () => {
const { dragHandle } = renderDragHandle({
directions: { 'block-start': 'active' },
tooltipText: 'Click me!',
clickDragThreshold: 10,
});

fireEvent.pointerDown(dragHandle, { clientX: 50, clientY: 50 });
fireEvent.pointerMove(dragHandle, { clientX: 55, clientY: 55 });
fireEvent.pointerUp(dragHandle);
expectDirectionButtonToBeVisible('block-start');
});

describe('hideButtonsOnDrag property', () => {
test('hides direction buttons when dragging with hideButtonsOnDrag=true', () => {
const { dragHandle, showButtons } = renderDragHandle({
directions: { 'block-start': 'active' },
tooltipText: 'Click me!',
hideButtonsOnDrag: true,
});

showButtons();
expectDirectionButtonToBeVisible('block-start');
fireEvent.pointerDown(dragHandle, { clientX: 50, clientY: 50 });
fireEvent.pointerMove(dragHandle, { clientX: 55, clientY: 55 });
expectDirectionButtonToBeHidden('block-start');
});

test('keeps direction buttons visible when dragging with hideButtonsOnDrag=false (default)', () => {
const { dragHandle, showButtons } = renderDragHandle({
directions: { 'block-start': 'active' },
tooltipText: 'Click me!',
hideButtonsOnDrag: false,
});

showButtons();
expectDirectionButtonToBeVisible('block-start');
fireEvent.pointerDown(dragHandle, { clientX: 50, clientY: 50 });
fireEvent.pointerMove(dragHandle, { clientX: 55, clientY: 55 }); // Move more than default threshold
expectDirectionButtonToBeVisible('block-start');
});
});

test('hides direction buttons on Escape keypress', () => {
const { dragHandle, showButtons } = renderDragHandle({
directions: { 'block-start': 'active' },
Expand Down
27 changes: 12 additions & 15 deletions src/internal/components/drag-handle-wrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,15 @@ import PortalOverlay from './portal-overlay';

import styles from './styles.css.js';

// The amount of distance after pointer down that the cursor is allowed to
// jitter for a subsequent mouseup to still register as a "press" instead of
// a drag. A little allowance is needed for usability reasons, but this number
// isn't set in stone.
const PRESS_DELTA_MAX = 3;

export default function DragHandleWrapper({
directions,
tooltipText,
children,
onDirectionClick,
triggerMode = 'focus',
initialShowButtons = false,
hideButtonsOnDrag,
clickDragThreshold,
}: DragHandleWrapperProps) {
const wrapperRef = useRef<HTMLDivElement | null>(null);
const dragHandleRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -70,25 +66,26 @@ export default function DragHandleWrapper({
useEffect(() => {
const controller = new AbortController();

// See `PRESS_DELTA_MAX` above. We need to differentiate between a "click"
// and a "drag" action. We can say a "click" happens when a "pointerdown"
// is followed by a "pointerup" with no "pointermove" between the two.
// We need to differentiate between a "click" and a "drag" action.
// We can say a "click" happens when a "pointerdown" is followed by
// a "pointerup" with no "pointermove" between the two.
// However, it would be a poor usability experience if a "click" isn't
// registered because, while pressing my mouse, I moved it by just one
// pixel, making it a "drag" instead. So we allow the pointer to move by
// `PRESS_DELTA_MAX` pixels before setting `didPointerDrag` to true.
// `clickDragThreshold` pixels before setting `didPointerDrag` to true.
document.addEventListener(
'pointermove',
event => {
if (
isPointerDown.current &&
initialPointerPosition.current &&
(event.clientX > initialPointerPosition.current.x + PRESS_DELTA_MAX ||
event.clientX < initialPointerPosition.current.x - PRESS_DELTA_MAX ||
event.clientY > initialPointerPosition.current.y + PRESS_DELTA_MAX ||
event.clientY < initialPointerPosition.current.y - PRESS_DELTA_MAX)
(event.clientX > initialPointerPosition.current.x + clickDragThreshold ||
event.clientX < initialPointerPosition.current.x - clickDragThreshold ||
event.clientY > initialPointerPosition.current.y + clickDragThreshold ||
event.clientY < initialPointerPosition.current.y - clickDragThreshold)
) {
didPointerDrag.current = true;
hideButtonsOnDrag && setShowButtons(false);
}
},
{ signal: controller.signal }
Expand Down Expand Up @@ -123,7 +120,7 @@ export default function DragHandleWrapper({
);

return () => controller.abort();
}, []);
}, [clickDragThreshold, hideButtonsOnDrag]);

const onHandlePointerDown: React.PointerEventHandler = event => {
// Tooltip behavior: the tooltip should appear on hover, but disappear when
Expand Down
2 changes: 2 additions & 0 deletions src/internal/components/drag-handle-wrapper/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export interface DragHandleWrapperProps {
children: React.ReactNode;
triggerMode?: TriggerMode;
initialShowButtons?: boolean;
hideButtonsOnDrag: boolean;
clickDragThreshold: number;
}
4 changes: 4 additions & 0 deletions src/internal/components/drag-handle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const InternalDragHandle = forwardRef(
onDirectionClick,
triggerMode,
initialShowButtons,
hideButtonsOnDrag = false,
clickDragThreshold = 3,
...rest
}: DragHandleProps,
ref: React.Ref<Element>
Expand All @@ -39,6 +41,8 @@ const InternalDragHandle = forwardRef(
onDirectionClick={onDirectionClick}
triggerMode={triggerMode}
initialShowButtons={initialShowButtons}
hideButtonsOnDrag={hideButtonsOnDrag}
clickDragThreshold={clickDragThreshold}
>
<DragHandleButton
ref={ref}
Expand Down
9 changes: 9 additions & 0 deletions src/internal/components/drag-handle/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export interface DragHandleProps {
onDirectionClick?: (direction: DragHandleProps.Direction) => void;
triggerMode?: TriggerMode;
initialShowButtons?: boolean;
/**
* Hide the UAP buttons when dragging is active.
*/
hideButtonsOnDrag?: boolean;
/**
* Max cursor movement (in pixels) that still counts as a press rather than
* a drag. Small threshold needed for usability.
*/
clickDragThreshold?: number;
}

export namespace DragHandleProps {
Expand Down
Loading