UNPKG

@chayns-components/core

Version:

A set of beautiful React components for developing your own applications with chayns.

155 lines (151 loc) 6.38 kB
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { StyledDropdownBodyWrapper, StyledDropdownBodyWrapperContent } from './DropdownBodyWrapper.styles'; import { createPortal } from 'react-dom'; import { DropdownDirection } from '../../types/dropdown'; import DelayedDropdownContent from './delayed-dropdown-content/DelayedDropdownContent'; import { useDropdown, useDropdownListener } from '../../hooks/dropdown'; import { ContainerAnchor, useContainer } from '../../hooks/container'; const DropdownBodyWrapper = /*#__PURE__*/forwardRef(({ anchorElement, bodyWidth, children, container: containerProp, contentHeight = 0, direction = DropdownDirection.BOTTOM_RIGHT, maxHeight, minBodyWidth = 0, onAvailableMaxHeightChange, onClose, onOutsideClick, onMeasure, shouldCaptureEvents = true, shouldShowDropdown }, ref) => { const isInChaynsWalletRef = useRef(false); const [measuredContentHeight, setMeasuredContentHeight] = useState(0); const [measuredContentWidth, setMeasuredContentWidth] = useState(0); const [portal, setPortal] = useState(); const contentRef = useRef(null); const shouldPreventClickRef = useRef(false); const touchTimeoutRef = useRef(undefined); const container = useContainer({ anchorElement, container: containerProp }); const { transform, width, coordinates, availableMaxHeight } = useDropdown({ anchorElement, container, contentHeight, contentWidth: bodyWidth ?? measuredContentWidth, direction, shouldShowDropdown }); useEffect(() => { if (typeof onAvailableMaxHeightChange === 'function') { onAvailableMaxHeightChange(availableMaxHeight); } }, [availableMaxHeight, onAvailableMaxHeightChange]); const handleClose = useCallback(() => { if (typeof onClose === 'function') { onClose(); } }, [onClose]); /** * This function closes the body */ const handleClick = useCallback(event => { if (contentRef.current && shouldShowDropdown && !anchorElement.contains(event.target) && !contentRef.current.contains(event.target)) { event.preventDefault(); event.stopPropagation(); const shouldPreventCloseOnClick = onOutsideClick?.() ?? false; if (!shouldPreventClickRef.current && !shouldPreventCloseOnClick) { handleClose(); } } shouldPreventClickRef.current = false; }, [anchorElement, handleClose, onOutsideClick, shouldShowDropdown]); const handleContentMeasure = useCallback(measurements => { // Measurements are only needed if the content is shown in the chayns wallet. To prevent // unnecessary renders, we only set the height if the content is shown in the wallet. if (isInChaynsWalletRef.current) { setMeasuredContentHeight(measurements.height); } setMeasuredContentWidth(measurements.width); if (typeof onMeasure === 'function') { onMeasure(measurements); } }, [onMeasure]); const handleTouchEnd = useCallback(() => { clearTimeout(touchTimeoutRef.current); }, []); const handleTouchStart = useCallback(() => { touchTimeoutRef.current = window.setTimeout(() => { shouldPreventClickRef.current = true; }, 500); }, []); /** * This hook listens for clicks */ useDropdownListener({ onClick: handleClick, onClose: handleClose, onTouchEnd: handleTouchEnd, onTouchStart: handleTouchStart, shouldCaptureEvents }); useEffect(() => { const isBottomDirection = [DropdownDirection.BOTTOM, DropdownDirection.BOTTOM_LEFT, DropdownDirection.BOTTOM_RIGHT].includes(direction); const reservationWrapperElement = anchorElement.closest(ContainerAnchor.RESERVATION_WRAPPER); isInChaynsWalletRef.current = !!(reservationWrapperElement && reservationWrapperElement.contains(anchorElement)) || true; // This effect checks if additional space is needed to show dropdown content in chayns cards. if (isBottomDirection && isInChaynsWalletRef.current && measuredContentHeight > 0 && reservationWrapperElement && shouldShowDropdown) { const availableHeight = window.innerHeight - anchorElement.getBoundingClientRect().bottom; // If the content height is greater than the available height, we need to add additional space. // This is to ensure that the dropdown content is fully visible. The 16 pixels are a buffer for shadows. const additionalNeededSpace = measuredContentHeight + 16 - availableHeight; if (additionalNeededSpace > 0) { // Add margin bottom to the reservation wrapper to ensure the dropdown content is fully visible. reservationWrapperElement.style.marginBottom = `${additionalNeededSpace}px`; } else { // Reset the margin bottom if no additional space is needed. reservationWrapperElement.style.marginBottom = '0px'; } } if (isInChaynsWalletRef.current && reservationWrapperElement && !shouldShowDropdown) { // Reset the margin bottom when the dropdown is closed. reservationWrapperElement.style.marginBottom = '0px'; } return () => { if (reservationWrapperElement) { reservationWrapperElement.style.marginBottom = '0px'; } }; }, [anchorElement, direction, measuredContentHeight, shouldShowDropdown]); useEffect(() => { if (!container) return; setPortal(() => /*#__PURE__*/createPortal(/*#__PURE__*/React.createElement(DelayedDropdownContent, { coordinates: coordinates, onMeasure: handleContentMeasure, shouldShowContent: shouldShowDropdown, transform: transform }, /*#__PURE__*/React.createElement(StyledDropdownBodyWrapperContent, { $width: width, $minWidth: minBodyWidth, $maxHeight: maxHeight, $direction: direction, ref: contentRef, className: typeof maxHeight === 'number' ? 'chayns-scrollbar' : undefined, tabIndex: 0 }, children)), container)); }, [children, container, coordinates, direction, handleContentMeasure, maxHeight, minBodyWidth, shouldShowDropdown, transform, width]); useImperativeHandle(ref, () => contentRef.current, []); return /*#__PURE__*/React.createElement(StyledDropdownBodyWrapper, null, portal); }); DropdownBodyWrapper.displayName = 'DropdownBodyWrapper'; export default DropdownBodyWrapper; //# sourceMappingURL=DropdownBodyWrapper.js.map