UNPKG

@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
// 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