@chayns-components/core
Version:
A set of beautiful React components for developing your own applications with chayns.
155 lines (151 loc) • 6.38 kB
JavaScript
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