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

184 lines • 10.1 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { useCallback, useRef, useState } from 'react'; import { nodeContains } from '@awsui/component-toolkit/dom'; import { getLogicalBoundingClientRect } from '@awsui/component-toolkit/internal'; import { findUpUntilMultiple, isContainingBlock } from '../internal/utils/dom'; import { calculateScroll, getFirstScrollableParent, scrollRectangleIntoView, } from '../internal/utils/scrollable-containers'; import { calculatePosition, getDimensions, getOffsetDimensions, isCenterOutside } from './utils/positions'; export default function usePopoverPosition({ popoverRef, bodyRef, arrowRef, trackRef, contentRef, allowScrollToFit, allowVerticalOverflow, preferredPosition, renderWithPortal, keepPosition, hideOnOverscroll, }) { const previousInternalPositionRef = useRef(null); const [popoverStyle, setPopoverStyle] = useState({}); const [internalPosition, setInternalPosition] = useState(null); const [isOverscrolling, setIsOverscrolling] = useState(false); // Store the handler in a ref so that it can still be replaced from outside of the listener closure. const positionHandlerRef = useRef(() => { }); const scrollableContainerRectRef = useRef(null); const updatePositionHandler = useCallback((onContentResize = false) => { var _a; if (!trackRef.current || !popoverRef.current || !bodyRef.current || !contentRef.current || !arrowRef.current) { return; } // Get important elements const popover = popoverRef.current; const body = bodyRef.current; const arrow = arrowRef.current; const document = popover.ownerDocument; const track = trackRef.current; // If the popover body isn't being rendered for whatever reason (e.g. "display: none" or JSDOM), // or track does not belong to the document - bail on calculating dimensions. const { offsetWidth, offsetHeight } = getOffsetDimensions(popover); if (offsetWidth === 0 || offsetHeight === 0 || !nodeContains(document.body, track)) { return; } // Imperatively move body off-screen to give it room to expand. // Not doing this in React because this recalculation should happen // in the span of a single frame without rerendering anything. const prevInsetBlockStart = popover.style.insetBlockStart; const prevInsetInlineStart = popover.style.insetInlineStart; popover.style.insetBlockStart = '0'; popover.style.insetInlineStart = '0'; // Imperatively remove body styles that can remain from the previous computation. body.style.maxBlockSize = ''; body.style.overflowX = ''; body.style.overflowY = ''; // Get rects representing key elements // Use getComputedStyle for arrowRect to avoid modifications made by transform const viewportRect = getViewportRect(document.defaultView); const trackRect = getLogicalBoundingClientRect(track); const arrowRect = getDimensions(arrow); const { containingBlock, boundary } = findUpUntilMultiple({ startElement: popover, tests: { containingBlock: isContainingBlock, boundary: (element) => isContainingBlock(element) || isBoundary(element), }, }); // Rectangle for the containing block, which provides the reference frame for the popover coordinates. const containingBlockRect = containingBlock ? getLogicalBoundingClientRect(containingBlock) : viewportRect; // Rectangle outside of which the popover should not be positioned, because it would be clipped. const boundaryRect = boundary ? getLogicalBoundingClientRect(boundary) : getDocumentRect(document); const bodyBorderWidth = getBorderWidth(body); const contentRect = getLogicalBoundingClientRect(contentRef.current); const contentBoundingBox = { inlineSize: contentRect.inlineSize + 2 * bodyBorderWidth, blockSize: contentRect.blockSize + 2 * bodyBorderWidth, }; // When keepPosition is true and the recalculation was triggered by a resize of the popover content, // we maintain the previously defined internal position, // but we still call calculatePosition to know if the popover should be scrollable. const shouldKeepPosition = keepPosition && onContentResize && !!previousInternalPositionRef.current; const fixedInternalPosition = (_a = (shouldKeepPosition && previousInternalPositionRef.current)) !== null && _a !== void 0 ? _a : undefined; // Calculate the arrow direction and viewport-relative position of the popover. const { scrollable, internalPosition: newInternalPosition, rect, } = calculatePosition({ preferredPosition, fixedInternalPosition, trigger: trackRect, arrow: arrowRect, body: contentBoundingBox, container: boundaryRect, viewport: viewportRect, renderWithPortal, allowVerticalOverflow, }); // Get the position of the popover relative to the containing block. const popoverOffset = toRelativePosition(rect, containingBlockRect); // Cache the distance between the trigger and the popover (which stays the same as you scroll), // and use that to recalculate the new popover position. const trackRelativeOffset = toRelativePosition(popoverOffset, toRelativePosition(trackRect, containingBlockRect)); // Bring back the container to its original position to prevent any flashing. popover.style.insetBlockStart = prevInsetBlockStart; popover.style.insetInlineStart = prevInsetInlineStart; // Allow popover body to scroll if can't fit the popover into the container/viewport otherwise. if (scrollable) { body.style.maxBlockSize = rect.blockSize + 'px'; body.style.overflowX = 'hidden'; body.style.overflowY = 'auto'; } // Remember the internal position in case we want to keep it later. previousInternalPositionRef.current = newInternalPosition; setInternalPosition(newInternalPosition); const shouldScroll = allowScrollToFit && !shouldKeepPosition; // Position the popover const insetBlockStart = shouldScroll ? popoverOffset.insetBlockStart + calculateScroll(rect) : popoverOffset.insetBlockStart; setPopoverStyle({ insetBlockStart, insetInlineStart: popoverOffset.insetInlineStart }); // Scroll if necessary if (shouldScroll) { const scrollableParent = getFirstScrollableParent(popover); scrollRectangleIntoView(rect, scrollableParent); } if (hideOnOverscroll && trackRef.current instanceof HTMLElement) { const scrollableContainer = getFirstScrollableParent(trackRef.current); if (scrollableContainer) { scrollableContainerRectRef.current = getLogicalBoundingClientRect(scrollableContainer); } } positionHandlerRef.current = () => { const trackRect = getLogicalBoundingClientRect(track); const newTrackOffset = toRelativePosition(trackRect, containingBlock ? getLogicalBoundingClientRect(containingBlock) : viewportRect); setPopoverStyle({ insetBlockStart: newTrackOffset.insetBlockStart + trackRelativeOffset.insetBlockStart, insetInlineStart: newTrackOffset.insetInlineStart + trackRelativeOffset.insetInlineStart, }); if (hideOnOverscroll && scrollableContainerRectRef.current) { // Assuming the arrow tip is at the vertical center of the popover trigger. // This is good enough for disabled reason tooltip in select and multiselect. // Can be further refined to take the exact arrow position into account if hideOnOverscroll is to be used in other cases. setIsOverscrolling(isCenterOutside(trackRect, scrollableContainerRectRef.current)); } }; }, [ trackRef, popoverRef, bodyRef, contentRef, arrowRef, keepPosition, preferredPosition, renderWithPortal, allowVerticalOverflow, allowScrollToFit, hideOnOverscroll, ]); return { updatePositionHandler, popoverStyle, internalPosition, positionHandlerRef, isOverscrolling }; } function getBorderWidth(element) { return parseInt(getComputedStyle(element).borderWidth) || 0; } /** * Convert a viewport-relative offset to an element-relative offset. */ function toRelativePosition(element, parent) { return { insetBlockStart: element.insetBlockStart - parent.insetBlockStart, insetInlineStart: element.insetInlineStart - parent.insetInlineStart, }; } /** * Get a BoundingBox that represents the visible viewport. */ function getViewportRect(window) { var _a, _b, _c, _d; return { insetBlockStart: 0, insetInlineStart: 0, inlineSize: (_b = (_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : window.innerWidth, blockSize: (_d = (_c = window.visualViewport) === null || _c === void 0 ? void 0 : _c.height) !== null && _d !== void 0 ? _d : window.innerHeight, }; } function getDocumentRect(document) { const { insetBlockStart, insetInlineStart } = getLogicalBoundingClientRect(document.documentElement); return { insetBlockStart, insetInlineStart, inlineSize: document.documentElement.scrollWidth, blockSize: document.documentElement.scrollHeight, }; } function isBoundary(element) { const computedStyle = getComputedStyle(element); return !!computedStyle.clipPath && computedStyle.clipPath !== 'none'; } //# sourceMappingURL=use-popover-position.js.map