@yandex/ui
Version:
Yandex UI components
126 lines (125 loc) • 6.03 kB
JavaScript
import { __read, __spread } from "tslib";
import { useMemo, useRef, useState } from 'react';
import { isEqual } from '../lib/isEqual';
import { useIsomorphicLayoutEffect as useLayoutEffect } from '../useIsomorphicLayoutEffect';
import { createPopper } from './createPopper';
import { getElementsFromRefs } from './utils';
/**
* Реакт-хук, реализующий позиционирование попапа при помощи popper.
*/
export function usePopper(props) {
var anchorRef = props.anchorRef, _a = props.arrowMarginThreshold, arrowMarginThreshold = _a === void 0 ? 4 : _a, _b = props.placement, placement = _b === void 0 ? 'bottom' : _b, _c = props.enabled, enabled = _c === void 0 ? true : _c, _d = props.marginThreshold, marginThreshold = _d === void 0 ? 16 : _d, _e = props.modifiers, modifiers = _e === void 0 ? [] : _e, motionless = props.motionless, offset = props.offset, unsafe_tailOffset = props.unsafe_tailOffset, children = props.children, boundary = props.boundary;
var placements = Array.isArray(placement) ? placement : [placement];
var popperRef = useRef(null);
var prevPopperOptions = useRef(null);
// Используем useState вместо useRef для установки ссылок, т.к. нам
// важно выполнить обновление в момент установки, а не на следующем тике.
var _f = __read(useState(), 2), popupNode = _f[0], setPopupNode = _f[1];
var _g = __read(useState(), 2), arrowNode = _g[0], setArrowNode = _g[1];
var popperOptions = useMemo(function () {
var _a = __read(placements), placement = _a[0], fallbackPlacements = _a.slice(1);
var popperBoundary = getElementsFromRefs(boundary);
var options = {
// Добавляем children в опции popper для того,
// чтобы обновить координаты при изменении контента.
children: children,
// При инициализации указываем единственное направление,
// все остальные направления применяются в модификаторе flip.
placement: placement,
modifiers: __spread([
{
name: 'eventListeners',
enabled: !motionless,
},
{
name: 'offset',
options: {
offset: offset,
tailOffset: unsafe_tailOffset,
},
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
},
},
{
name: 'preventOverflow',
options: {
// Свойство позволяет учитывать границы в overflow контейнере.
altBoundary: true,
boundary: popperBoundary,
},
},
{
name: 'arrow',
enabled: Boolean(arrowNode),
options: {
element: arrowNode,
padding: arrowMarginThreshold,
},
},
{
name: 'flip',
options: {
padding: marginThreshold,
fallbackPlacements: fallbackPlacements,
// Свойство позволяет учитывать границы в overflow контейнере.
altBoundary: true,
boundary: popperBoundary,
},
},
{
name: 'hide',
options: {
boundary: popperBoundary,
},
}
], modifiers),
};
// Отдаем объект из кэша если значения в опциях не изменились,
// это позволяет более эффективно производить обновления и не требовать
// от пользователей кэшировать устанавливаемые свойства.
if (isEqual(prevPopperOptions.current, options)) {
return prevPopperOptions.current || options;
}
return (prevPopperOptions.current = options);
}, [
placements,
offset,
arrowNode,
arrowMarginThreshold,
motionless,
marginThreshold,
unsafe_tailOffset,
modifiers,
children,
boundary,
]);
useLayoutEffect(function () {
if (popperRef.current !== null) {
popperRef.current.setOptions(popperOptions);
}
}, [popperOptions]);
useLayoutEffect(function () {
// NOTE: В данный момент не реализован cleanup для случая, когда якорь был удален из документа,
// т.к. мы используем ref-объект, а не прямую ссылку на DOM-элемент.
if (anchorRef.current && popupNode && enabled) {
popperRef.current = createPopper(anchorRef.current, popupNode, popperOptions);
popperRef.current.forceUpdate();
}
return function () {
if (popperRef.current !== null) {
popperRef.current.destroy();
popperRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [anchorRef, popupNode, enabled]);
return {
popper: popperRef.current,
setArrowRef: setArrowNode,
setPopupRef: setPopupNode,
};
}