@i-novus/ui-core
Version:
Базовые UI компоненты
256 lines (255 loc) • 10.6 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { forwardRef, useEffect, useRef, useState } from 'react';
import { IS_DEV } from '../../utils/isDev';
import { Tooltip } from './Tooltip';
import { useTooltip } from './TooltipProvider';
import { TooltipContent } from './TooltipContent';
// eslint-disable-next-line sonarjs/cognitive-complexity
export const TooltipController = forwardRef((props, ref) => {
const { id, anchorId, anchorSelect, content, html, render, className, classNameArrow, variant = 'light', place = 'top', offset = 10, wrapper = 'div', children = null, events = ['hover'], openOnClick = false, positionStrategy = 'absolute', middlewares, delayShow = 0, delayHide = 0, float = false, hidden = false, noArrow = false, clickable = false, closeOnEsc = false, closeOnScroll = false, closeOnResize = false, style, position, isOpen, defaultIsOpen, disableStyleInjection = false, border, opacity, arrowColor, setIsOpen, afterShow, afterHide, } = props;
const [tooltipContent, setTooltipContent] = useState(content);
const [tooltipHtml, setTooltipHtml] = useState(html);
const [tooltipPlace, setTooltipPlace] = useState(place);
const [tooltipVariant, setTooltipVariant] = useState(variant);
const [tooltipOffset, setTooltipOffset] = useState(offset);
const [tooltipDelayShow, setTooltipDelayShow] = useState(delayShow);
const [tooltipDelayHide, setTooltipDelayHide] = useState(delayHide);
const [tooltipFloat, setTooltipFloat] = useState(float);
const [tooltipHidden, setTooltipHidden] = useState(hidden);
const [tooltipWrapper, setTooltipWrapper] = useState(wrapper);
const [tooltipEvents, setTooltipEvents] = useState(events);
const [tooltipPositionStrategy, setTooltipPositionStrategy] = useState(positionStrategy);
const [activeAnchor, setActiveAnchor] = useState(null);
const styleInjectionRef = useRef(disableStyleInjection);
const { anchorRefs, activeAnchor: providerActiveAnchor } = useTooltip(id);
const getDataAttributesFromAnchorElement = (elementReference) => {
return elementReference?.getAttributeNames().reduce((out, name) => {
if (name.startsWith('data-tooltip-')) {
const parsedAttribute = name.replace(/^data-tooltip-/, '');
out[parsedAttribute] = elementReference?.getAttribute(name) ?? null;
}
return out;
}, {});
};
const applyAllDataAttributesFromAnchorElement = (dataAttributes) => {
const handleDataAttributes = {
place: (value) => {
setTooltipPlace(value ?? place);
},
content: (value) => {
setTooltipContent(value ?? content);
},
html: (value) => {
setTooltipHtml(value ?? html);
},
variant: (value) => {
setTooltipVariant(value ?? variant);
},
offset: (value) => {
setTooltipOffset(value === null ? offset : Number(value));
},
wrapper: (value) => {
setTooltipWrapper(value ?? wrapper);
},
events: (value) => {
const parsed = value?.split(' ');
setTooltipEvents(parsed ?? events);
},
'position-strategy': (value) => {
setTooltipPositionStrategy(value ?? positionStrategy);
},
'delay-show': (value) => {
setTooltipDelayShow(value === null ? delayShow : Number(value));
},
'delay-hide': (value) => {
setTooltipDelayHide(value === null ? delayHide : Number(value));
},
float: (value) => {
setTooltipFloat(value === null ? float : value === 'true');
},
hidden: (value) => {
setTooltipHidden(value === null ? hidden : value === 'true');
},
};
// reset unset data attributes to default values
// without this, data attributes from the last active anchor will still be used
Object.values(handleDataAttributes).forEach(handler => handler(null));
Object.entries(dataAttributes).forEach(([key, value]) => {
handleDataAttributes[key]?.(value);
});
};
useEffect(() => {
setTooltipContent(content);
}, [content]);
useEffect(() => {
setTooltipHtml(html);
}, [html]);
useEffect(() => {
setTooltipPlace(place);
}, [place]);
useEffect(() => {
setTooltipVariant(variant);
}, [variant]);
useEffect(() => {
setTooltipOffset(offset);
}, [offset]);
useEffect(() => {
setTooltipDelayShow(delayShow);
}, [delayShow]);
useEffect(() => {
setTooltipDelayHide(delayHide);
}, [delayHide]);
useEffect(() => {
setTooltipFloat(float);
}, [float]);
useEffect(() => {
setTooltipHidden(hidden);
}, [hidden]);
useEffect(() => {
setTooltipPositionStrategy(positionStrategy);
}, [positionStrategy]);
useEffect(() => {
if (styleInjectionRef.current === disableStyleInjection) {
return;
}
if (IS_DEV) {
console.warn('[react-tooltip] Do not change `disableStyleInjection` dynamically.');
}
}, [disableStyleInjection]);
useEffect(() => {
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('react-tooltip-inject-styles', {
detail: {
disableCore: disableStyleInjection === 'core',
disableBase: disableStyleInjection,
},
}));
}
}, []);
useEffect(() => {
const elementRefs = new Set(anchorRefs);
let selector = anchorSelect;
if (!selector && id) {
selector = `[data-tooltip-id='${id}']`;
}
if (selector) {
try {
const anchorsBySelect = document.querySelectorAll(selector);
anchorsBySelect.forEach((anchor) => {
elementRefs.add({ current: anchor });
});
}
catch {
if (IS_DEV) {
console.warn(`[react-tooltip] "${selector}" is not a valid CSS selector`);
}
}
}
const anchorById = document.querySelector(`[id='${anchorId}']`);
if (anchorById) {
elementRefs.add({ current: anchorById });
}
if (!elementRefs.size) {
return () => null;
}
const anchorElement = activeAnchor ?? anchorById ?? providerActiveAnchor.current;
const observerCallback = (mutationList) => {
mutationList.forEach((mutation) => {
if (!anchorElement ||
mutation.type !== 'attributes' ||
!mutation.attributeName?.startsWith('data-tooltip-')) {
return;
}
// make sure to get all set attributes, since all unset attributes are reset
const dataAttributes = getDataAttributesFromAnchorElement(anchorElement);
applyAllDataAttributesFromAnchorElement(dataAttributes);
});
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(observerCallback);
// do not check for subtree and childrens, we only want to know attribute changes
// to stay watching `data-attributes-*` from anchor element
const observerConfig = { attributes: true, childList: false, subtree: false };
if (anchorElement) {
const dataAttributes = getDataAttributesFromAnchorElement(anchorElement);
applyAllDataAttributesFromAnchorElement(dataAttributes);
// Start observing the target node for configured mutations
observer.observe(anchorElement, observerConfig);
}
return () => {
// Remove the observer when the tooltip is destroyed
observer.disconnect();
};
}, [anchorRefs, providerActiveAnchor, activeAnchor, anchorId, anchorSelect]);
useEffect(() => {
if (IS_DEV) {
if (style?.border) {
console.warn('[react-tooltip] Do not set `style.border`. Use `border` prop instead.');
}
if (border && !CSS.supports('border', `${border}`)) {
console.warn(`[react-tooltip] "${border}" is not a valid \`border\`.`);
}
if (style?.opacity) {
console.warn('[react-tooltip] Do not set `style.opacity`. Use `opacity` prop instead.');
}
if (opacity && !CSS.supports('opacity', `${opacity}`)) {
console.warn(`[react-tooltip] "${opacity}" is not a valid \`opacity\`.`);
}
}
}, []);
/**
* content priority: children < render or content < html
* children should be lower priority so that it can be used as the "default" content
*/
let renderedContent = children;
const contentWrapperRef = useRef(null);
if (render) {
const rendered = render({ content: tooltipContent ?? null, activeAnchor });
renderedContent = rendered ? (_jsx("div", { ref: contentWrapperRef, className: "react-tooltip-content-wrapper", children: rendered })) : null;
}
else if (tooltipContent) {
renderedContent = tooltipContent;
}
if (tooltipHtml) {
renderedContent = _jsx(TooltipContent, { content: tooltipHtml });
}
const tooltipProps = {
id,
anchorId,
anchorSelect,
className,
classNameArrow,
content: renderedContent,
contentWrapperRef,
place: tooltipPlace,
variant: tooltipVariant,
offset: tooltipOffset,
wrapper: tooltipWrapper,
events: tooltipEvents,
openOnClick,
positionStrategy: tooltipPositionStrategy,
middlewares,
delayShow: tooltipDelayShow,
delayHide: tooltipDelayHide,
float: tooltipFloat,
hidden: tooltipHidden,
noArrow,
clickable,
closeOnEsc,
closeOnScroll,
closeOnResize,
style,
position,
isOpen,
defaultIsOpen,
border,
opacity,
arrowColor,
setIsOpen,
afterShow,
afterHide,
activeAnchor,
setActiveAnchor: (anchor) => setActiveAnchor(anchor),
};
return _jsx(Tooltip, { ...tooltipProps, ref: ref });
});