refactor(Tooltip): improve logic

This commit is contained in:
Tim 2023-10-24 00:27:15 +02:00
parent ea23012884
commit aa4dcb7410
13 changed files with 180 additions and 127 deletions

View file

@ -6,7 +6,7 @@ const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button');
const styles = require('./styles');
const { Tooltip } = require('stremio/common/Tooltip');
const { Tooltip } = require('stremio/common/Tooltips');
const ActionButton = ({ className, icon, label, tooltip, ...props }) => {
return (

View file

@ -1,106 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useState, createRef } = require('react');
const PropTypes = require('prop-types');
const classNames = require('classnames');
const TooltipContext = require('./TooltipContext');
const styles = require('./styles');
const TooltipProvider = ({ children, className }) => {
const [tooltips, setTooltips] = useState([]);
const add = ({ id, label, position, margin = 15, parent }) => {
const ref = createRef(null);
const tooltip = {
ref,
id,
label,
position,
margin: margin,
active: false,
parent,
};
setTooltips((tooltips) => ([
...tooltips,
tooltip,
]));
};
const remove = (id) => {
setTooltips((tooltips) => (
tooltips.filter((tooltip) => tooltip.id !== id)
));
};
const toggle = (id, state) => {
setTooltips((tooltips) => (
tooltips.map((tooltip) => {
if (tooltip.id === id) {
tooltip.active = state;
}
return tooltip;
})
));
};
const style = (ref, position, margin, active, parent) => {
if (!active) return {};
const tooltipHeight = ref.current?.offsetHeight ?? 0;
const tooltipWidth = ref.current?.offsetWidth ?? 0;
const parentBounds = parent.getBoundingClientRect();
switch (position) {
case 'top':
return {
top: `${parentBounds.top - tooltipHeight - margin}px`,
left: `${(parentBounds.left + (parentBounds.width / 2)) - (tooltipWidth / 2)}px`,
};
case 'bottom':
return {
top: `${parentBounds.top + parentBounds.height + margin}px`,
left: `${(parentBounds.left + (parentBounds.width / 2)) - (tooltipWidth / 2)}px`,
};
case 'left':
return {
top: `${parentBounds.top + (parentBounds.height / 2) - (tooltipHeight / 2)}px`,
left: `${(parentBounds.left - tooltipWidth - margin)}px`,
};
case 'right':
return {
top: `${parentBounds.top + (parentBounds.height / 2) - (tooltipHeight / 2)}px`,
left: `${(parentBounds.left + parentBounds.width + margin)}px`,
};
}
};
return (
<TooltipContext.Provider value={{ add, remove, toggle }}>
{ children }
<div className={'tooltips-container'}>
{
tooltips.map(({ ref, id, label, position, margin, active, parent }) => (
<div
key={id}
ref={ref}
className={classNames(className, styles['tooltip-container'], { 'active': active })}
style={style(ref, position, margin, active, parent)}
>
{ label }
</div>
))
}
</div>
</TooltipContext.Provider>
);
};
TooltipProvider.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};
module.exports = TooltipProvider;

View file

@ -2,25 +2,35 @@
const React = require('react');
const PropTypes = require('prop-types');
const useTooltip = require('./useTooltip');
const useTooltip = require('../useTooltip');
const styles = require('./styles');
const createId = () => (Math.random() + 1).toString(36).substring(7);
const Tooltip = ({ label, position, margin }) => {
const Tooltip = ({ label, position, margin = 15 }) => {
const tooltip = useTooltip();
const id = React.useRef(createId());
const element = React.useRef(null);
const onMouseEnter = () => {
tooltip.toggle(id.current, true);
tooltip.update(id.current, {
active: true,
});
};
const onMouseLeave = () => {
tooltip.toggle(id.current, false);
tooltip.update(id.current, {
active: false,
});
};
React.useEffect(() => {
tooltip.update(id.current, {
label,
});
}, [label]);
React.useLayoutEffect(() => {
if (element.current && element.current.parentElement) {
const parentElement = element.current.parentElement;
@ -45,15 +55,13 @@ const Tooltip = ({ label, position, margin }) => {
tooltip.remove(id.current);
}
};
}, [label]);
}, []);
return (
<div ref={element} className={styles['tooltip']} />
<div ref={element} className={styles['tooltip-placeholder']} />
);
};
Tooltip.displayName = 'Tooltip';
Tooltip.propTypes = {
label: PropTypes.string.isRequired,
position: PropTypes.string.isRequired,

View file

@ -0,0 +1,6 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Tooltip = require('./Tooltip');
module.exports = Tooltip;

View file

@ -0,0 +1,11 @@
// Copyright (C) 2017-2023 Smart code 203358507
.tooltip-placeholder {
z-index: -1;
visibility: hidden;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}

View file

@ -0,0 +1,72 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classNames = require('classnames');
const styles = require('./styles');
const TooltipItem = React.memo(({ className, active, label, position, margin, parent }) => {
const ref = React.useRef(null);
const timeout = React.useRef(null);
const [style, setStyle] = React.useState({});
React.useEffect(() => {
clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
if (active && ref.current) {
const tooltipBounds = ref.current.getBoundingClientRect();
const parentBounds = parent.getBoundingClientRect();
switch (position) {
case 'top':
return setStyle({
top: `${parentBounds.top - tooltipBounds.height - margin}px`,
left: `${(parentBounds.left + (parentBounds.width / 2)) - (tooltipBounds.width / 2)}px`,
});
case 'bottom':
return setStyle({
top: `${parentBounds.top + parentBounds.height + margin}px`,
left: `${(parentBounds.left + (parentBounds.width / 2)) - (tooltipBounds.width / 2)}px`,
});
case 'left':
return setStyle({
top: `${parentBounds.top + (parentBounds.height / 2) - (tooltipBounds.height / 2)}px`,
left: `${(parentBounds.left - tooltipBounds.width - margin)}px`,
});
case 'right':
return setStyle({
top: `${parentBounds.top + (parentBounds.height / 2) - (tooltipBounds.height / 2)}px`,
left: `${(parentBounds.left + parentBounds.width + margin)}px`,
});
}
}
});
return () => clearTimeout(timeout.current);
}, [active, position, margin, parent, label]);
return (
<div
ref={ref}
className={classNames(className, styles['tooltip-item'], { 'active': active })}
style={style}
>
{ label }
</div>
);
});
TooltipItem.displayName = 'TooltipItem';
TooltipItem.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
label: PropTypes.string,
position: PropTypes.string,
margin: PropTypes.number,
parent: PropTypes.instanceOf(HTMLElement),
};
module.exports = TooltipItem;

View file

@ -0,0 +1,6 @@
// Copyright (C) 2017-2023 Smart code 203358507
const TooltipItem = require('./TooltipItem');
module.exports = TooltipItem;

View file

@ -1,16 +1,6 @@
// Copyright (C) 2017-2023 Smart code 203358507
.tooltip {
z-index: -1;
visibility: hidden;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.tooltip-container {
.tooltip-item {
position: fixed;
visibility: hidden;
opacity: 0;

View file

@ -0,0 +1,66 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const TooltipContext = require('./TooltipContext');
const TooltipItem = require('./TooltipItem');
const TooltipProvider = ({ children, className }) => {
const [tooltips, setTooltips] = React.useState([]);
const add = (options) => {
const tooltip = {
...options,
active: false,
};
setTooltips((tooltips) => ([
...tooltips,
tooltip,
]));
};
const remove = (id) => {
setTooltips((tooltips) => (
tooltips.filter((tooltip) => tooltip.id !== id)
));
};
const update = (id, state) => {
setTooltips((tooltips) => (
tooltips.map((tooltip) => {
if (tooltip.id === id) {
tooltip = {
...tooltip,
...state,
};
}
return tooltip;
})
));
};
return (
<TooltipContext.Provider value={{ add, remove, update }}>
{ children }
<div className={'tooltips-items-container'}>
{
tooltips.map(({ id, ...tooltip }) => (
<TooltipItem
key={id}
className={className}
{...tooltip}
/>
))
}
</div>
</TooltipContext.Provider>
);
};
TooltipProvider.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};
module.exports = TooltipProvider;

View file

@ -23,7 +23,7 @@ const SharePrompt = require('./SharePrompt');
const Slider = require('./Slider');
const TextInput = require('./TextInput');
const { ToastProvider, useToast } = require('./Toast');
const { TooltipProvider, Tooltip } = require('./Tooltip');
const { TooltipProvider, Tooltip } = require('./Tooltips');
const comparatorWithPriorities = require('./comparatorWithPriorities');
const CONSTANTS = require('./CONSTANTS');
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');