@awsui/components-react
Version:
On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en
142 lines • 9.37 kB
JavaScript
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useRef, useState } from 'react';
import clsx from 'clsx';
import { nodeContains } from '@awsui/component-toolkit/dom';
import Tooltip from '../tooltip';
import DirectionButton from './direction-button';
import PortalOverlay from './portal-overlay';
import styles from './styles.css.js';
import testUtilsStyles from './test-classes/styles.css.js';
export default function DragHandleWrapper({ directions, tooltipText, children, onDirectionClick, triggerMode = 'focus', initialShowButtons = false, controlledShowButtons = false, wrapperClassName, hideButtonsOnDrag, clickDragThreshold, }) {
const wrapperRef = useRef(null);
const dragHandleRef = useRef(null);
const [showTooltip, setShowTooltip] = useState(false);
const [uncontrolledShowButtons, setUncontrolledShowButtons] = useState(initialShowButtons);
const isPointerDown = useRef(false);
const initialPointerPosition = useRef();
const didPointerDrag = useRef(false);
// The tooltip ("Drag or select to move/resize") shouldn't show if clicking
// on the handle wouldn't do anything.
const isDisabled = !directions['block-start'] && !directions['block-end'] && !directions['inline-start'] && !directions['inline-end'];
const onWrapperFocusIn = event => {
// The drag handle is focused when it's either tabbed to, or the pointer
// is pressed on it. We exclude handling the pointer press in this handler,
// since it could be the start of a drag event - the pointer stuff is
// handled in the "pointerup" listener instead. In cases where focus is moved
// to the button (by manually calling `.focus()`), the buttons should only appear
// if the action that triggered the focus move was the result of a keypress.
if (document.body.dataset.awsuiFocusVisible && !nodeContains(wrapperRef.current, event.relatedTarget)) {
setShowTooltip(false);
if (triggerMode === 'focus') {
setUncontrolledShowButtons(true);
}
}
};
const onWrapperFocusOut = event => {
// Close the directional buttons when the focus leaves the drag handle.
// "focusout" is also triggered when the user switches to another tab, but
// since it'll be returned when they switch back anyway, we exclude that
// case by checking for `document.hasFocus()`.
if (document.hasFocus() && !nodeContains(wrapperRef.current, event.relatedTarget)) {
setUncontrolledShowButtons(false);
}
};
useEffect(() => {
const controller = new AbortController();
// 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
// `clickDragThreshold` pixels before setting `didPointerDrag` to true.
document.addEventListener('pointermove', event => {
if (isPointerDown.current &&
initialPointerPosition.current &&
(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;
if (hideButtonsOnDrag) {
setUncontrolledShowButtons(false);
}
}
}, { signal: controller.signal });
// Shared behavior when a "pointerdown" state ends. This is shared so it
// can be called for both "pointercancel" and "pointerup" events.
const resetPointerDownState = () => {
isPointerDown.current = false;
initialPointerPosition.current = undefined;
};
document.addEventListener('pointercancel', () => {
resetPointerDownState();
}, { signal: controller.signal });
document.addEventListener('pointerup', () => {
if (isPointerDown.current && !didPointerDrag.current) {
// The cursor didn't move much between "pointerdown" and "pointerup".
// Handle this as a "click" instead of a "drag".
setUncontrolledShowButtons(true);
}
resetPointerDownState();
}, { signal: controller.signal });
return () => controller.abort();
}, [clickDragThreshold, hideButtonsOnDrag]);
const onHandlePointerDown = event => {
// Tooltip behavior: the tooltip should appear on hover, but disappear when
// the pointer starts dragging (having the tooltip get in the way while
// you're trying to drag upwards is annoying). Additionally, the tooltip
// shouldn't reappear when dragging ends, but only when the pointer leaves
// the drag handle and comes back.
isPointerDown.current = true;
didPointerDrag.current = false;
initialPointerPosition.current = { x: event.clientX, y: event.clientY };
setShowTooltip(false);
};
// Tooltip behavior: the tooltip should stay open when the cursor moves
// from the drag handle into the tooltip content itself. This is why the
// handler is set on the wrapper for both the drag handle and the tooltip.
const onTooltipGroupPointerEnter = () => {
if (!isPointerDown.current) {
setShowTooltip(true);
}
};
const onTooltipGroupPointerLeave = () => {
setShowTooltip(false);
};
const onDragHandleKeyDown = event => {
// For accessibility reasons, pressing escape should always close the floating controls.
if (event.key === 'Escape') {
setUncontrolledShowButtons(false);
}
else if (triggerMode === 'keyboard-activate' && (event.key === 'Enter' || event.key === ' ')) {
// toggle buttons when Enter or space is pressed in 'keyboard-activate' triggerMode
setUncontrolledShowButtons(prevshowButtons => !prevshowButtons);
}
else if (event.key !== 'Alt' &&
event.key !== 'Control' &&
event.key !== 'Meta' &&
event.key !== 'Shift' &&
triggerMode === 'focus') {
// Pressing any other key will display the focus-visible ring around the
// drag handle if it's in focus, so we should also show the buttons now.
setUncontrolledShowButtons(true);
}
};
const showButtons = triggerMode === 'controlled' ? controlledShowButtons : uncontrolledShowButtons;
return (React.createElement(React.Fragment, null,
React.createElement("div", { className: clsx(testUtilsStyles.root, styles.contents), ref: wrapperRef, onFocus: onWrapperFocusIn, onBlur: onWrapperFocusOut },
React.createElement("div", { className: styles.contents, onPointerEnter: onTooltipGroupPointerEnter, onPointerLeave: onTooltipGroupPointerLeave },
React.createElement("div", { className: clsx(styles['drag-handle'], wrapperClassName), ref: dragHandleRef, onPointerDown: onHandlePointerDown, onKeyDown: onDragHandleKeyDown }, children),
!isDisabled && !showButtons && showTooltip && tooltipText && (
// Rendered in a portal but pointerenter/pointerleave events still propagate
// up the React DOM tree, which is why it's placed in this nested context.
React.createElement(Tooltip, { trackRef: dragHandleRef, value: tooltipText, onDismiss: () => setShowTooltip(false) })))),
React.createElement(PortalOverlay, { track: dragHandleRef, isDisabled: !showButtons },
directions['block-start'] && (React.createElement(DirectionButton, { show: !isDisabled && showButtons, direction: "block-start", state: directions['block-start'], onClick: () => onDirectionClick === null || onDirectionClick === void 0 ? void 0 : onDirectionClick('block-start') })),
directions['block-end'] && (React.createElement(DirectionButton, { show: !isDisabled && showButtons, direction: "block-end", state: directions['block-end'], onClick: () => onDirectionClick === null || onDirectionClick === void 0 ? void 0 : onDirectionClick('block-end') })),
directions['inline-start'] && (React.createElement(DirectionButton, { show: !isDisabled && showButtons, direction: "inline-start", state: directions['inline-start'], onClick: () => onDirectionClick === null || onDirectionClick === void 0 ? void 0 : onDirectionClick('inline-start') })),
directions['inline-end'] && (React.createElement(DirectionButton, { show: !isDisabled && showButtons, direction: "inline-end", state: directions['inline-end'], onClick: () => onDirectionClick === null || onDirectionClick === void 0 ? void 0 : onDirectionClick('inline-end') })))));
}
//# sourceMappingURL=index.js.map