react-floater
Version:
Floaters everywhere!
995 lines (994 loc) • 28.9 kB
JavaScript
import is from "is-lite";
import { Children, Fragment, cloneElement, isValidElement, useCallback, useEffect, useMemo, useReducer, useRef } from "react";
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
import { createPortal } from "react-dom";
import { deepmerge } from "deepmerge-ts";
import { createPopper } from "@popperjs/core";
//#region src/literals.ts
const POSITIONING_PROPS = [
"position",
"top",
"right",
"bottom",
"left"
];
const STATUS = {
INIT: "init",
IDLE: "idle",
RENDER: "render",
OPENING: "opening",
OPEN: "open",
CLOSING: "closing",
ERROR: "error"
};
//#endregion
//#region src/components/Floater/Arrow.tsx
function FloaterArrow(props) {
const { arrow, arrowRef, placement, styles } = props;
const { arrow: { base, color, display, position, size } } = styles;
const arrowStyles = {
display,
position
};
let points;
let x = base;
let y = size;
if (placement.startsWith("top")) points = `0,0 ${x / 2},${y} ${x},0`;
else if (placement.startsWith("bottom")) points = `${x},${y} ${x / 2},0 0,${y}`;
else if (placement.startsWith("left")) {
y = base;
x = size;
points = `0,0 ${x},${y / 2} 0,${y}`;
} else if (placement.startsWith("right")) {
y = base;
x = size;
points = `${x},${y} ${x},0 0,${y / 2}`;
}
if (arrow) return /* @__PURE__ */ jsx("span", {
ref: arrowRef,
style: {
color,
height: placement.startsWith("left") || placement.startsWith("right") ? base : size,
width: placement.startsWith("left") || placement.startsWith("right") ? size : base
},
children: arrow
});
return /* @__PURE__ */ jsx("span", {
ref: arrowRef,
className: "__floater__arrow",
style: arrowStyles,
children: /* @__PURE__ */ jsx("svg", {
height: y,
version: "1.1",
width: x,
xmlns: "http://www.w3.org/2000/svg",
children: /* @__PURE__ */ jsx("polygon", {
fill: color,
points
})
})
});
}
//#endregion
//#region src/components/Floater/CloseButton.tsx
function FloaterCloseButton({ onClick, styles }) {
const { color, height, width, ...style } = styles;
return /* @__PURE__ */ jsx("button", {
"aria-label": "close",
onClick,
style,
type: "button",
children: /* @__PURE__ */ jsx("svg", {
height: `${height}px`,
preserveAspectRatio: "xMidYMid",
version: "1.1",
viewBox: "0 0 18 18",
width: `${width}px`,
xmlns: "http://www.w3.org/2000/svg",
children: /* @__PURE__ */ jsx("g", { children: /* @__PURE__ */ jsx("path", {
d: "M8.13911129,9.00268191 L0.171521827,17.0258467 C-0.0498027049,17.248715 -0.0498027049,17.6098394 0.171521827,17.8327545 C0.28204354,17.9443526 0.427188206,17.9998706 0.572051765,17.9998706 C0.71714958,17.9998706 0.862013139,17.9443526 0.972581703,17.8327545 L9.0000937,9.74924618 L17.0276057,17.8327545 C17.1384085,17.9443526 17.2832721,17.9998706 17.4281356,17.9998706 C17.5729992,17.9998706 17.718097,17.9443526 17.8286656,17.8327545 C18.0499901,17.6098862 18.0499901,17.2487618 17.8286656,17.0258467 L9.86135722,9.00268191 L17.8340066,0.973848225 C18.0553311,0.750979934 18.0553311,0.389855532 17.8340066,0.16694039 C17.6126821,-0.0556467968 17.254037,-0.0556467968 17.0329467,0.16694039 L9.00042166,8.25611765 L0.967006424,0.167268345 C0.745681892,-0.0553188426 0.387317931,-0.0553188426 0.165993399,0.167268345 C-0.0553311331,0.390136635 -0.0553311331,0.751261038 0.165993399,0.974176179 L8.13920499,9.00268191 L8.13911129,9.00268191 Z",
fill: color
}) })
})
});
}
//#endregion
//#region src/components/Floater/Container.tsx
function FloaterContainer(props) {
const { content, footer, onClick, open, positionWrapper, showCloseButton, styles, title } = props;
const output = { content: isValidElement(content) ? content : /* @__PURE__ */ jsx("div", {
className: "__floater__content",
style: styles.content,
children: content
}) };
if (title) output.title = isValidElement(title) ? title : /* @__PURE__ */ jsx("div", {
className: "__floater__title",
style: styles.title,
children: title
});
if (footer) output.footer = isValidElement(footer) ? footer : /* @__PURE__ */ jsx("div", {
className: "__floater__footer",
style: styles.footer,
children: footer
});
if ((showCloseButton || positionWrapper) && !is.boolean(open)) output.close = /* @__PURE__ */ jsx(FloaterCloseButton, {
onClick,
styles: styles.close
});
return /* @__PURE__ */ jsxs("div", {
className: "__floater__container",
style: styles.container,
children: [
output.close,
output.title,
output.content,
output.footer
]
});
}
//#endregion
//#region src/components/Floater/index.tsx
function Floater(props) {
const { component, content, floaterRef, hideArrow, id, onClick: closeFn, placement, status, styles } = props;
const style = useMemo(() => {
const { arrow: { size }, floater, floaterCentered, floaterClosing, floaterOpening, floaterWithComponent } = styles;
let element = { ...floater };
if (!hideArrow) {
if (placement.startsWith("top")) element.padding = `0 0 ${size}px`;
else if (placement.startsWith("bottom")) element.padding = `${size}px 0 0`;
else if (placement.startsWith("left")) element.padding = `0 ${size}px 0 0`;
else if (placement.startsWith("right")) element.padding = `0 0 0 ${size}px`;
}
if (status === STATUS.CLOSING) element = {
...element,
...floaterClosing
};
if (status === STATUS.OPENING || status === STATUS.OPEN) element = {
...element,
...floaterOpening
};
if (placement === "center") element = {
...element,
...floaterCentered
};
if (component) element = {
...element,
...floaterWithComponent
};
return element;
}, [
component,
hideArrow,
placement,
status,
styles
]);
const shouldRender = [
"closing",
"open",
"opening",
"render"
].includes(status);
const output = {};
const classes = ["__floater"];
const baseProps = { role: "tooltip" };
if (component) {
const componentProps = {
closeFn,
id,
...baseProps
};
output.content = isValidElement(component) ? cloneElement(component, componentProps) : component(componentProps);
} else output.content = /* @__PURE__ */ jsx(FloaterContainer, {
...props,
content
});
if (status === STATUS.OPEN) classes.push("__floater__open");
if (!hideArrow) output.arrow = /* @__PURE__ */ jsx(FloaterArrow, { ...props });
if (!shouldRender) return null;
return /* @__PURE__ */ jsx("div", {
ref: floaterRef,
id,
style: { zIndex: styles.options.zIndex },
children: /* @__PURE__ */ jsx("div", {
className: classes.join(" "),
style,
...baseProps,
children: /* @__PURE__ */ jsxs("div", {
className: "__floater__body",
children: [output.content, output.arrow]
})
})
});
}
//#endregion
//#region src/modules/helpers.ts
const portalId = "react-floater-portal";
function canUseDOM() {
return !!(typeof window !== "undefined" && window.document?.createElement);
}
function enhanceProps(props) {
return {
autoOpen: false,
debug: false,
disableFlip: false,
disableHoverToClick: false,
event: "click",
eventDelay: .4,
hideArrow: false,
offset: 15,
placement: "bottom",
showCloseButton: false,
styles: {},
target: null,
wrapperOptions: { position: false },
...props
};
}
function getFallbackPlacements(placement) {
if (placement.startsWith("left")) return ["top", "bottom"];
if (placement.startsWith("right")) return ["bottom", "top"];
}
function getModifiers(modifiers) {
return deepmerge({
flip: {
name: "flip",
enabled: true,
options: { padding: 20 }
},
preventOverflow: {
name: "preventOverflow",
enabled: true,
options: { padding: 10 }
}
}, modifiers ?? {});
}
function isFixed(el) {
if (!el) return false;
const { nodeName } = el;
if (nodeName === "BODY" || nodeName === "HTML") return false;
if (getComputedStyle(el).position === "fixed") return true;
return el.parentNode instanceof HTMLElement ? isFixed(el.parentNode) : false;
}
function isMobile() {
return "ontouchstart" in window && /Mobi/.test(navigator.userAgent);
}
/**
* Log method calls if debug is enabled
*/
function log({ data, debug = false, title }) {
const logFn = console.log;
if (debug && title && data) {
console.groupCollapsed(`%creact-floater: ${title}`, "color: #9b00ff; font-weight: bold; font-size: 12px;");
if (Array.isArray(data)) data.forEach((d) => {
if (is.plainObject(d) && d.key) logFn.apply(console, [d.key, d.value]);
else logFn.apply(console, [d]);
});
else logFn.apply(console, [data]);
console.groupEnd();
}
}
function mergeModifier(modifier, customModifier) {
return deepmerge(modifier, customModifier ?? {});
}
function off(element, eventType, handler, options = false) {
element.removeEventListener(eventType, handler, options);
}
function on(element, eventType, handler, options = false) {
element.addEventListener(eventType, handler, options);
}
function once(element, eventType, handler, options = false) {
let nextCB = (event) => {
handler(event);
off(element, eventType, nextCB);
};
on(element, eventType, nextCB, options);
return () => off(element, eventType, nextCB);
}
function randomId() {
return `rf-${Math.round(Math.random() * 1e5)}`;
}
//#endregion
//#region src/modules/hooks.ts
function useEffectOnce(effect) {
useEffect(effect, []);
}
function useMount(effect) {
useEffectOnce(effect);
}
function useUnmount(fn) {
const fnRef = useRef(fn);
fnRef.current = fn;
useEffectOnce(() => () => fnRef.current());
}
function useUpdateEffect(effect, deps) {
const isFirst = useRef(true);
useEffect(() => {
if (isFirst.current) {
isFirst.current = false;
return;
}
return effect();
}, deps);
}
//#endregion
//#region src/components/Portal.tsx
function ReactFloaterPortal(props) {
const { children, hasChildren, internalId, placement, portalElement, target, zIndex } = props;
const node = useRef(null);
const initialize = useCallback(() => {
if (portalElement) node.current = is.string(portalElement) ? document.querySelector(portalElement) : portalElement;
if (!portalElement || !node.current) {
const portal = document.getElementById(portalId);
if (portal) {
const ids = portal.dataset.ids?.split(",").filter(Boolean) ?? [];
if (!ids.includes(internalId) && internalId) ids.push(internalId);
portal.dataset.ids = ids.join(",");
node.current = portal;
} else {
node.current = document.createElement("div");
node.current.id = portalId;
node.current.dataset.ids = internalId;
node.current.style.zIndex = `${zIndex}`;
document.body.appendChild(node.current);
}
}
}, [
internalId,
portalElement,
zIndex
]);
useMount(() => {
initialize();
});
useEffect(() => {
initialize();
}, [initialize]);
useUnmount(() => {
if (!node.current) return;
try {
if (node.current.id === "react-floater-portal") {
const remainingIds = (node.current.dataset.ids?.split(",") ?? []).filter((id) => id !== internalId);
node.current.dataset.ids = remainingIds.join(",");
if (remainingIds.length === 0) {
document.body.removeChild(node.current);
node.current = null;
}
}
} catch {
node.current = null;
}
});
if (node.current) {
if (!hasChildren && !target && placement !== "center") return null;
return createPortal(children, node.current);
}
return null;
}
//#endregion
//#region src/components/Wrapper.tsx
function FloaterWrapper(props) {
const { childRef, children, id, isControlled, onClick, onMount, onMouseEnter, onMouseLeave, status, style, styles, wrapperRef } = props;
useMount(() => {
onMount();
});
let element = null;
const mergedStyles = {
...styles,
...style,
...isValidElement(children) ? children.props.style : void 0
};
const wrapperId = `${id}-wrapper`;
let wrapperProps = {
"aria-describedby": [
STATUS.OPENING,
STATUS.OPEN,
STATUS.CLOSING
].includes(status) ? id : void 0,
style: mergedStyles
};
if (!isControlled) wrapperProps = {
...wrapperProps,
onClick,
onMouseEnter,
onMouseLeave
};
if (children) if (Children.count(children) === 1 && isValidElement(children) && children.type !== Fragment) element = is.function(children.type) ? /* @__PURE__ */ jsx("span", {
ref: wrapperRef,
id: wrapperId,
...wrapperProps,
children: cloneElement(Children.only(children), { innerRef: childRef })
}) : cloneElement(Children.only(children), {
id: wrapperId,
ref: wrapperRef,
...wrapperProps
});
else element = /* @__PURE__ */ jsx("span", {
ref: wrapperRef,
id: wrapperId,
...wrapperProps,
children
});
return element;
}
//#endregion
//#region src/modules/styles.ts
const defaultOptions = { zIndex: 100 };
function getStyles(styles) {
const { options = {}, ...rest } = styles ?? {};
return deepmerge({
arrow: {
base: 32,
color: "#fff",
display: "inline-flex",
position: "absolute",
size: 16
},
close: {
backgroundColor: "transparent",
border: 0,
borderRadius: 0,
color: "#555",
fontSize: 0,
height: 15,
outline: "none",
padding: 10,
position: "absolute",
right: 0,
top: 0,
width: 15,
WebkitAppearance: "none"
},
container: {
backgroundColor: "#fff",
color: "#666",
minHeight: 60,
minWidth: 200,
padding: 20,
position: "relative",
zIndex: 10
},
content: { fontSize: 15 },
footer: {
borderTop: "1px solid #ccc",
fontSize: 13,
marginTop: 10,
paddingTop: 5
},
floater: {
display: "inline-block",
filter: "drop-shadow(0 0 3px rgba(0, 0, 0, 0.3))",
maxWidth: 300,
opacity: 0,
position: "relative",
transition: "opacity 0.3s",
visibility: "hidden",
zIndex: options.zIndex
},
floaterCentered: {
left: "50%",
position: "fixed",
top: "50%",
transform: "translate(-50%, -50%)"
},
floaterClosing: {
opacity: 0,
visibility: "visible"
},
floaterOpening: {
opacity: 1,
visibility: "visible"
},
floaterWithComponent: { maxWidth: "100%" },
title: {
borderBottom: "1px solid #555",
color: "#555",
fontSize: 18,
marginBottom: 5,
paddingBottom: 6,
paddingRight: 18
},
wrapper: {
cursor: "help",
zIndex: options.zIndex
},
wrapperPosition: {
left: -1e3,
position: "absolute",
top: -1e3,
visibility: "hidden"
},
options: deepmerge(defaultOptions, options)
}, rest);
}
//#endregion
//#region src/modules/useFloater.ts
function useFloater(props) {
const { arrow: arrowElement, autoOpen = false, callback, children, component, content, debug = false, disableFlip = false, disableHoverToClick = false, event = "click", eventDelay = .4, footer, getPopper, hideArrow = false, id, modifiers, offset = 15, open, placement = "bottom", portalElement, showCloseButton = false, style, styles, target, title, wrapperOptions } = enhanceProps(props);
const [state, setState] = useReducer((previousState, nextState) => ({
...previousState,
...nextState
}), {
currentPlacement: placement,
positionWrapper: !!wrapperOptions?.position && !!target,
status: STATUS.INIT,
statusWrapper: STATUS.INIT
});
const { currentPlacement, positionWrapper, status, statusWrapper } = state;
const arrowRef = useRef(null);
const childRef = useRef(null);
const floaterRef = useRef(null);
const wrapperRef = useRef(null);
const eventDelayTimer = useRef(void 0);
const internalId = useRef(randomId());
const isMounted = useRef(false);
const popperGeneration = useRef(0);
const popperRef = useRef(void 0);
const previousStatus = useRef(STATUS.INIT);
const stateRef = useRef(state);
const removeTransitionListener = useRef(void 0);
const wrapperPopperRef = useRef(void 0);
const wrapperStyles = useRef(void 0);
const callbackRef = useRef(callback);
const getPopperRef = useRef(getPopper);
const initPopperRef = useRef(void 0);
const propsRef = useRef(props);
const targetElement = useRef(void 0);
callbackRef.current = callback;
getPopperRef.current = getPopper;
propsRef.current = props;
targetElement.current = () => {
if (target) {
if (is.domElement(target)) return target;
return document.querySelector(target);
}
return childRef.current ?? wrapperRef.current;
};
const currentDebug = debug || !!window.ReactFloaterDebug;
const isIdle = status === STATUS.IDLE;
const currentEvent = useMemo(() => {
if (event === "hover" && isMobile() && !disableHoverToClick) return "click";
return event;
}, [disableHoverToClick, event]);
const currentStyles = useMemo(() => {
const nextStyles = getStyles(styles);
const element = targetElement.current();
if (positionWrapper) {
let wrapperCurrentStyles;
if (!isIdle) wrapperCurrentStyles = nextStyles.wrapperPosition;
else if (statusWrapper === STATUS.RENDER) wrapperCurrentStyles = wrapperPopperRef.current?.state.styles;
nextStyles.wrapper = {
...nextStyles.wrapper,
...wrapperCurrentStyles
};
}
if (element) {
const targetStyles = window.getComputedStyle(element);
if (wrapperStyles.current) nextStyles.wrapper = {
...nextStyles.wrapper,
...wrapperStyles.current
};
else if (!["relative", "static"].includes(targetStyles.position)) {
wrapperStyles.current = {};
if (!positionWrapper) {
POSITIONING_PROPS.forEach((d) => {
if (d === "position") wrapperStyles.current[d] = targetStyles[d];
else wrapperStyles.current[d] = targetStyles[d];
});
nextStyles.wrapper = {
...nextStyles.wrapper,
...wrapperStyles.current
};
}
}
}
return nextStyles;
}, [
positionWrapper,
isIdle,
statusWrapper,
styles
]);
const updateState = useCallback((nextState, callback_) => {
if (isMounted.current) {
setState(nextState);
stateRef.current = {
...stateRef.current,
...nextState
};
if (callback_) callback_();
}
}, []);
const toggle = useCallback((forceStatus) => {
let nextStatus = stateRef.current.status === STATUS.OPEN ? STATUS.CLOSING : STATUS.RENDER;
if (!is.undefined(forceStatus)) nextStatus = forceStatus;
updateState({
status: nextStatus,
statusWrapper: nextStatus === STATUS.CLOSING ? STATUS.RENDER : STATUS.IDLE
});
}, [updateState]);
const initPopper = useCallback(() => {
const nextStatus = stateRef.current.status === STATUS.RENDER ? STATUS.OPENING : STATUS.IDLE;
popperGeneration.current++;
const generation = popperGeneration.current;
const element = targetElement.current();
if (placement === "center") requestAnimationFrame(() => {
if (popperGeneration.current !== generation) return;
updateState({ status: nextStatus });
});
else if (element) {
if (floaterRef.current) {
const { arrow, flip, offset: offsetModifier, ...rest } = getModifiers(modifiers);
popperRef.current = createPopper(element, floaterRef.current, {
placement,
strategy: isFixed(targetElement.current()) ? "fixed" : "absolute",
modifiers: [
mergeModifier({
name: "arrow",
enabled: !hideArrow,
options: {
element: arrowRef.current,
padding: 8
}
}, arrow),
mergeModifier({
name: "flip",
enabled: !disableFlip,
options: {
altAxis: false,
fallbackPlacements: getFallbackPlacements(placement || "bottom")
}
}, flip),
mergeModifier({
name: "offset",
enabled: true,
options: { offset: [0, offset] }
}, offsetModifier),
{
name: "updatePlacement",
enabled: true,
phase: "afterWrite",
fn: ({ instance, state: popperState }) => {
if (popperState.placement !== stateRef.current.currentPlacement) {
popperRef.current = instance;
updateState({ currentPlacement: popperState.placement });
}
}
},
{
name: "applyArrowStyle",
enabled: true,
phase: "write",
fn: ({ state: popperState }) => {
const { elements: { arrow: stateArrow }, placement: statePlacement } = popperState;
if (stateArrow) {
if (statePlacement.startsWith("top")) {
stateArrow.style.bottom = "0px";
stateArrow.style.right = "";
} else if (statePlacement.startsWith("bottom")) {
stateArrow.style.top = "0px";
stateArrow.style.right = "";
} else if (statePlacement.startsWith("left")) {
stateArrow.style.right = "0px";
stateArrow.style.bottom = "";
} else if (statePlacement.startsWith("right")) {
stateArrow.style.left = "0px";
stateArrow.style.bottom = "";
}
}
}
},
...Object.values(rest)
],
onFirstUpdate: (popperState) => {
if (popperGeneration.current !== generation) return;
updateState({
currentPlacement: popperState.placement,
status: nextStatus
});
if (placement !== popperState.placement) setTimeout(() => {
popperRef.current?.forceUpdate();
});
}
});
if (getPopperRef.current && popperRef.current) getPopperRef.current(popperRef.current, "floater");
} else if (stateRef.current.status === STATUS.RENDER)
/* v8 ignore start -- @preserve */
requestAnimationFrame(() => {
if (isMounted.current && stateRef.current.status === STATUS.RENDER) initPopper();
});
else
/* v8 ignore next 3 -- @preserve */
updateState({ status: STATUS.IDLE });
/* v8 ignore start -- @preserve */
if (wrapperRef.current && !wrapperPopperRef.current && stateRef.current.positionWrapper) {
const wrapperOffset = wrapperOptions?.offset ?? 0;
wrapperPopperRef.current = createPopper(element, wrapperRef.current, {
placement: wrapperOptions?.placement ?? placement,
strategy: isFixed(targetElement.current()) ? "fixed" : "absolute",
modifiers: [
{
name: "arrow",
enabled: false
},
{
name: "offset",
options: { offset: [0, wrapperOffset] }
},
{
name: "flip",
enabled: false
}
],
onFirstUpdate: (popperState) => {
updateState({ statusWrapper: STATUS.RENDER });
if (placement !== popperState.placement) setTimeout(() => {
wrapperPopperRef.current?.forceUpdate();
});
}
});
if (getPopperRef.current) getPopperRef.current(wrapperPopperRef.current, "wrapper");
}
}
}, [
disableFlip,
hideArrow,
modifiers,
offset,
placement,
updateState,
wrapperOptions?.offset,
wrapperOptions?.placement
]);
const cleanUp = useCallback(() => {
if (popperRef.current) {
popperRef.current.destroy();
popperRef.current = void 0;
}
if (wrapperPopperRef.current) {
wrapperPopperRef.current.destroy();
wrapperPopperRef.current = void 0;
}
}, []);
initPopperRef.current = initPopper;
const handleTransitionEnd = useCallback(() => {
/* v8 ignore next 3 -- @preserve */
if (wrapperPopperRef.current) wrapperPopperRef.current.forceUpdate();
updateState({ status: stateRef.current.status === STATUS.OPENING ? STATUS.OPEN : STATUS.IDLE }, () => {
if (callbackRef.current) callbackRef.current(stateRef.current.status === STATUS.OPEN ? "open" : "close", enhanceProps(propsRef.current));
});
}, [updateState]);
const handleClick = useCallback(() => {
if (is.boolean(open)) return;
if (currentEvent === "click" || currentEvent === "hover" && positionWrapper) {
log({
title: "click",
data: [{
event,
status: status === STATUS.OPEN ? "closing" : "opening"
}],
debug: currentDebug
});
toggle(status === STATUS.IDLE ? STATUS.RENDER : void 0);
}
}, [
currentDebug,
currentEvent,
event,
open,
positionWrapper,
status,
toggle
]);
const handleMouseEnter = useCallback(() => {
if (is.boolean(open) || isMobile() || currentEvent !== "hover") return;
log({
title: "mouseEnter",
data: [{
key: "originalEvent",
value: event
}],
debug: currentDebug
});
if (status === STATUS.IDLE) {
clearTimeout(eventDelayTimer.current);
eventDelayTimer.current = void 0;
toggle(STATUS.RENDER);
}
}, [
currentDebug,
currentEvent,
event,
open,
status,
toggle
]);
const handleMouseLeave = useCallback(() => {
if (is.boolean(open) || isMobile()) return;
if (currentEvent === "hover") {
log({
title: "mouseLeave",
data: [{
key: "originalEvent",
value: event
}],
debug: currentDebug
});
const hasOpenStatus = [STATUS.OPENING, STATUS.OPEN].includes(status);
if (!eventDelay) toggle(status === STATUS.CLOSING ? STATUS.IDLE : STATUS.CLOSING);
else if (!positionWrapper) {
if (hasOpenStatus) {
clearTimeout(eventDelayTimer.current);
eventDelayTimer.current = window.setTimeout(() => {
toggle();
eventDelayTimer.current = void 0;
}, eventDelay * 1e3);
}
}
}
}, [
currentDebug,
currentEvent,
event,
eventDelay,
open,
positionWrapper,
status,
toggle
]);
const handleWrapperMount = useCallback(() => {
if (positionWrapper) initPopper();
}, [initPopper, positionWrapper]);
useEffect(() => {
isMounted.current = true;
initPopperRef.current();
}, []);
useEffect(() => {
log({
title: "init",
data: {
hasChildren: !!children,
hasTarget: !!target,
isControlled: is.boolean(open),
positionWrapper,
target: targetElement.current(),
floater: floaterRef.current
},
debug: currentDebug
});
}, [
children,
target,
open,
positionWrapper,
currentDebug
]);
useEffect(() => {
return () => {
isMounted.current = false;
clearTimeout(eventDelayTimer.current);
removeTransitionListener.current?.();
cleanUp();
};
}, [cleanUp]);
useUpdateEffect(() => {
if (is.boolean(open)) toggle(open ? STATUS.RENDER : STATUS.CLOSING);
}, [open, toggle]);
useEffect(() => {
const next = !!wrapperOptions?.position && !!target;
if (stateRef.current.positionWrapper !== next) updateState({ positionWrapper: next });
}, [
wrapperOptions?.position,
target,
updateState
]);
useEffect(() => {
if (status === previousStatus.current) return;
if (status === STATUS.IDLE && previousStatus.current !== STATUS.IDLE) {
if (open || previousStatus.current === STATUS.INIT && autoOpen) toggle(STATUS.RENDER);
}
if (status === STATUS.RENDER && previousStatus.current !== STATUS.RENDER) {
if (popperRef.current) popperRef.current.destroy();
initPopper();
}
if (floaterRef.current && (status === STATUS.RENDER || status === STATUS.CLOSING) && previousStatus.current !== status) {
removeTransitionListener.current?.();
removeTransitionListener.current = once(floaterRef.current, "transitionend", handleTransitionEnd);
}
if (status === STATUS.IDLE && previousStatus.current === STATUS.CLOSING) {
if (popperRef.current) {
popperRef.current.destroy();
popperRef.current = void 0;
}
/* v8 ignore next 3 -- @preserve */
if (wrapperPopperRef.current) wrapperPopperRef.current.forceUpdate();
}
previousStatus.current = status;
}, [
autoOpen,
handleTransitionEnd,
initPopper,
open,
status,
toggle
]);
return {
arrowRef,
childRef,
floaterRef,
wrapperRef,
currentPlacement,
positionWrapper,
status,
currentStyles,
internalId: internalId.current,
arrowElement,
children,
component,
content,
footer,
hideArrow,
id,
open,
portalElement,
showCloseButton,
style,
target,
title,
handleClick,
handleMouseEnter,
handleMouseLeave,
handleWrapperMount
};
}
//#endregion
//#region src/index.tsx
function FloaterComponent(props) {
const { arrowElement, arrowRef, childRef, children, component, content, currentPlacement, currentStyles, floaterRef, footer, handleClick, handleMouseEnter, handleMouseLeave, handleWrapperMount, hideArrow, id, internalId, open, portalElement, positionWrapper, showCloseButton, status, style, target, title, wrapperRef } = useFloater(props);
const wrapper = /* @__PURE__ */ jsx(FloaterWrapper, {
childRef,
id: id ?? internalId,
isControlled: is.boolean(open),
onClick: handleClick,
onMount: handleWrapperMount,
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
status,
style,
styles: currentStyles.wrapper,
wrapperRef,
children
});
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(ReactFloaterPortal, {
hasChildren: !!children,
internalId,
placement: currentPlacement,
portalElement,
target,
zIndex: currentStyles.options.zIndex,
children: [/* @__PURE__ */ jsx(Floater, {
arrow: arrowElement,
arrowRef,
component,
content,
floaterRef,
footer,
hideArrow: hideArrow || currentPlacement === "center",
id: id ?? internalId,
onClick: handleClick,
placement: currentPlacement,
positionWrapper,
showCloseButton,
status,
styles: currentStyles,
title
}), positionWrapper && wrapper]
}), !positionWrapper && wrapper] });
}
function ReactFloater(props) {
if (!canUseDOM()) return null;
return /* @__PURE__ */ jsx(FloaterComponent, { ...props });
}
//#endregion
export { ReactFloater as default };
//# sourceMappingURL=index.mjs.map