UNPKG

@itwin/core-react

Version:

A react component library of iTwin.js UI general purpose components

163 lines 7.48 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module ElementSeparator */ import "./ElementSeparator.scss"; import * as React from "react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react"; import classnames from "classnames"; import { Orientation } from "../enums/Orientation.js"; import { useThrottledFn } from "../utils/hooks/useThrottledFn.js"; import { useTranslation } from "../l10n/useTranslation.js"; function getCurrentGlobalPosition(orientation, e) { return orientation === Orientation.Horizontal ? e.clientX : e.clientY; } const useConditionalCleanup = (condition, cleanup) => { const conditionRef = useRef(condition); const cleanupRef = useRef(cleanup); conditionRef.current = condition; cleanupRef.current = cleanup; useEffect(() => { return () => { if (conditionRef.current) cleanupRef.current(); }; }, []); }; const useElementSeparatorPointerHandler = ({ onResizeHandleDragChanged, onResizeHandleHoverChanged, isResizeHandleBeingDragged, isResizeHandleHovered, movableArea, ratio, orientation, onRatioChanged, }) => { const globalPosition = useRef(0); const pointerOutOfBounds = useRef(false); const [isElementDragged, setIsDragged] = useState(false); const [isElementHovered, setIsHovered] = useState(false); const isGroupDragged = isResizeHandleBeingDragged ?? isElementDragged; const isGroupHovered = isResizeHandleHovered ?? isElementHovered; useConditionalCleanup(isElementDragged && !!onResizeHandleDragChanged, () => onResizeHandleDragChanged(false)); useConditionalCleanup(isElementHovered && !!onResizeHandleHoverChanged, () => onResizeHandleHoverChanged(false)); if (isGroupHovered && pointerOutOfBounds.current) pointerOutOfBounds.current = false; const stopDrag = useCallback(() => { if (isGroupDragged) { setIsDragged(false); if (onResizeHandleDragChanged) onResizeHandleDragChanged(false); } }, [isGroupDragged, onResizeHandleDragChanged]); const startDrag = useCallback((e) => { globalPosition.current = getCurrentGlobalPosition(orientation, e); if (!isGroupDragged) { setIsDragged(true); if (onResizeHandleDragChanged) onResizeHandleDragChanged(true); } }, [isGroupDragged, orientation, onResizeHandleDragChanged]); const onPointerUp = useCallback(() => { stopDrag(); }, [stopDrag]); const onThrottledPointerMove = useThrottledFn((e) => { if (!movableArea) { stopDrag(); return; } const currentPosition = getCurrentGlobalPosition(orientation, e); const positionChange = currentPosition - globalPosition.current; // Should not need to recalculate if position on our movement axis does not change if (Math.abs(positionChange) < 1) return; const currentLocalPosition = movableArea * ratio + positionChange; const newRatio = currentLocalPosition / movableArea; globalPosition.current = currentPosition; if (pointerOutOfBounds.current || !onRatioChanged) return; const result = onRatioChanged(newRatio); if (result && result.ratio === ratio && !isGroupHovered && !pointerOutOfBounds.current) pointerOutOfBounds.current = true; }, 16, [stopDrag, isGroupHovered, ratio, movableArea, onRatioChanged, orientation]); useEffect(() => { return () => onThrottledPointerMove.cancel(); }, [onThrottledPointerMove]); useLayoutEffect(() => { if (isElementDragged) { document.addEventListener("pointerup", onPointerUp); document.addEventListener("pointermove", onThrottledPointerMove); } return () => { document.removeEventListener("pointerup", onPointerUp); document.removeEventListener("pointermove", onThrottledPointerMove); }; }, [isElementDragged, onPointerUp, onThrottledPointerMove]); const onPointerDown = useCallback((e) => { if (!isGroupDragged) { startDrag(e); } else { stopDrag(); } }, [isGroupDragged, startDrag, stopDrag]); const onPointerOver = useCallback(() => { if (isGroupHovered) return; setIsHovered(true); if (onResizeHandleHoverChanged) onResizeHandleHoverChanged(true); }, [isGroupHovered, onResizeHandleHoverChanged]); const onPointerOut = useCallback(() => { if (!isGroupHovered) return; setIsHovered(false); if (onResizeHandleHoverChanged) onResizeHandleHoverChanged(false); }, [isGroupHovered, onResizeHandleHoverChanged]); return { isHovered: isGroupHovered, isDragged: isGroupDragged, onPointerDown, onPointerOver, onPointerOut, }; }; function getStyle(orientation, separatorSize) { separatorSize = separatorSize || 30; if (orientation === Orientation.Horizontal) return { width: separatorSize, margin: `0px ${-Math.floor(separatorSize / 2)}px`, }; return { height: separatorSize, margin: `${-Math.floor(separatorSize / 2)}px 1px`, }; } /** A movable button, which allows to change the ratio between left element and right element * @public * @deprecated in 4.12.0. Basic components that need to be user-resized support this out of the box. Use {@link https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent pointer events} API to implement a custom resizer. */ export const ElementSeparator = (props) => { const { translate } = useTranslation(); const [hasHoverHappened, setHasHoverHappened] = useState(false); const { isDragged, isHovered, onPointerDown, onPointerOver, onPointerOut } = useElementSeparatorPointerHandler(props); const isHoverNeeded = isHovered || isDragged; if (!hasHoverHappened && isHoverNeeded) setHasHoverHappened(isHoverNeeded); // This is done to avoid fade-out animation when first rendering. const unhoverClass = hasHoverHappened ? "core-element-separator-group-unhovered" : ""; const classNames = classnames("core-element-separator", props.orientation === Orientation.Horizontal ? "core-element-separator--horizontal" : "core-element-separator--vertical", props.className, isHoverNeeded ? "core-element-separator-group-hovered" : unhoverClass); const orientation = props.orientation; const separatorSize = props.separatorSize; const style = useMemo(() => getStyle(orientation, separatorSize), [orientation, separatorSize]); const styles = { ...style, ...props.style, }; return (React.createElement("button", { style: styles, className: classNames, onPointerDown: onPointerDown, onPointerOver: onPointerOver, onPointerOut: onPointerOut, "aria-label": translate("elementSeparator.label"), tabIndex: -1 })); }; //# sourceMappingURL=ElementSeparator.js.map