From ebf19408ac186fb4458ea8ff45ab33c48ec53ca9 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 16 Oct 2023 22:01:23 +0200 Subject: [PATCH 1/4] feat: implement tooltips --- src/App/App.js | 18 +++-- src/App/styles.less | 12 +++ src/common/Tooltip/Tooltip.js | 63 +++++++++++++++ src/common/Tooltip/TooltipContext.js | 8 ++ src/common/Tooltip/TooltipProvider.js | 106 ++++++++++++++++++++++++++ src/common/Tooltip/index.js | 11 +++ src/common/Tooltip/styles.less | 20 +++++ src/common/Tooltip/useTooltip.js | 10 +++ src/common/index.js | 4 + 9 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 src/common/Tooltip/Tooltip.js create mode 100644 src/common/Tooltip/TooltipContext.js create mode 100644 src/common/Tooltip/TooltipProvider.js create mode 100644 src/common/Tooltip/index.js create mode 100644 src/common/Tooltip/styles.less create mode 100644 src/common/Tooltip/useTooltip.js diff --git a/src/App/App.js b/src/App/App.js index be59767b7..de443d9d4 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next'); const { Router } = require('stremio-router'); const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); -const { ToastProvider, CONSTANTS, withCoreSuspender } = require('stremio/common'); +const { ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); const DeepLinkHandler = require('./DeepLinkHandler'); const ErrorDialog = require('./ErrorDialog'); @@ -159,13 +159,15 @@ const App = () => { : - - - + + + + + :
diff --git a/src/App/styles.less b/src/App/styles.less index f4454424c..e5ee4a835 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -116,6 +116,18 @@ html { } } + .tooltip-container { + height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + padding: 0 1.5rem; + font-size: 1rem; + color: var(--primary-foreground-color); + border-radius: var(--border-radius); + background-color: var(--modal-background-color); + } + .router { width: 100%; height: 100%; diff --git a/src/common/Tooltip/Tooltip.js b/src/common/Tooltip/Tooltip.js new file mode 100644 index 000000000..025a4ab21 --- /dev/null +++ b/src/common/Tooltip/Tooltip.js @@ -0,0 +1,63 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +const React = require('react'); +const PropTypes = require('prop-types'); +const useTooltip = require('./useTooltip'); +const styles = require('./styles'); + +const createId = () => (Math.random() + 1).toString(36).substring(7); + +const Tooltip = ({ label, position, margin }) => { + const tooltip = useTooltip(); + + const id = React.useRef(createId()); + const element = React.useRef(null); + + const show = () => { + tooltip.toggle(id.current, true); + }; + + const hide = () => { + tooltip.toggle(id.current, false); + }; + + React.useEffect(() => { + if (element.current && element.current.parentElement) { + const parentElement = element.current.parentElement; + tooltip.add({ + id: id.current, + label, + position, + margin, + parent: parentElement, + }); + + parentElement.addEventListener('mouseenter', show); + parentElement.addEventListener('mouseleave', hide); + } + + return () => { + if (element.current && element.current.parentElement) { + const parentElement = element.current.parentElement; + parentElement.removeEventListener('mouseenter', show); + parentElement.removeEventListener('mouseleave', hide); + + tooltip.remove(id.current); + } + }; + }, []); + + return ( +
+ ); +}; + +Tooltip.displayName = 'Tooltip'; + +Tooltip.propTypes = { + label: PropTypes.string.isRequired, + position: PropTypes.string.isRequired, + margin: PropTypes.number, +}; + +module.exports = Tooltip; diff --git a/src/common/Tooltip/TooltipContext.js b/src/common/Tooltip/TooltipContext.js new file mode 100644 index 000000000..f4da8a082 --- /dev/null +++ b/src/common/Tooltip/TooltipContext.js @@ -0,0 +1,8 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +const { createContext } = require('react'); + +const TooltipContext = createContext(null); + +module.exports = TooltipContext; + diff --git a/src/common/Tooltip/TooltipProvider.js b/src/common/Tooltip/TooltipProvider.js new file mode 100644 index 000000000..c19780248 --- /dev/null +++ b/src/common/Tooltip/TooltipProvider.js @@ -0,0 +1,106 @@ +// 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 ( + + { children } +
+ { + tooltips.map(({ ref, id, label, position, margin, active, parent }) => ( +
+ { label } +
+ )) + } +
+
+ ); +}; + +TooltipProvider.propTypes = { + children: PropTypes.node, + className: PropTypes.string, +}; + +module.exports = TooltipProvider; diff --git a/src/common/Tooltip/index.js b/src/common/Tooltip/index.js new file mode 100644 index 000000000..a909daba2 --- /dev/null +++ b/src/common/Tooltip/index.js @@ -0,0 +1,11 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +const TooltipProvider = require('./TooltipProvider'); +const Tooltip = require('./Tooltip'); +const useTooltip = require('./useTooltip'); + +module.exports = { + TooltipProvider, + Tooltip, + useTooltip, +}; diff --git a/src/common/Tooltip/styles.less b/src/common/Tooltip/styles.less new file mode 100644 index 000000000..22bcb40ef --- /dev/null +++ b/src/common/Tooltip/styles.less @@ -0,0 +1,20 @@ +// 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 { + position: fixed; + visibility: hidden; + + &:global(.active) { + visibility: visible; + } +} \ No newline at end of file diff --git a/src/common/Tooltip/useTooltip.js b/src/common/Tooltip/useTooltip.js new file mode 100644 index 000000000..d787dca39 --- /dev/null +++ b/src/common/Tooltip/useTooltip.js @@ -0,0 +1,10 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +const React = require('react'); +const TooltipContext = require('./TooltipContext'); + +const useTooltip = () => { + return React.useContext(TooltipContext); +}; + +module.exports = useTooltip; diff --git a/src/common/index.js b/src/common/index.js index f267be575..56a41f8b5 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -23,6 +23,7 @@ const SharePrompt = require('./SharePrompt'); const Slider = require('./Slider'); const TextInput = require('./TextInput'); const { ToastProvider, useToast } = require('./Toast'); +const { TooltipProvider, Tooltip, useTooltip } = require('./Tooltip'); const comparatorWithPriorities = require('./comparatorWithPriorities'); const CONSTANTS = require('./CONSTANTS'); const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender'); @@ -70,6 +71,9 @@ module.exports = { TextInput, ToastProvider, useToast, + TooltipProvider, + Tooltip, + useTooltip, comparatorWithPriorities, CONSTANTS, withCoreSuspender, From 6430c6205534cb72a740f3bfc1852ed4aec2d30b Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 17 Oct 2023 10:35:55 +0200 Subject: [PATCH 2/4] refactor(Tooltip): rename mouse event handlers --- src/common/Tooltip/Tooltip.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/Tooltip/Tooltip.js b/src/common/Tooltip/Tooltip.js index 025a4ab21..91a11ef5c 100644 --- a/src/common/Tooltip/Tooltip.js +++ b/src/common/Tooltip/Tooltip.js @@ -13,11 +13,11 @@ const Tooltip = ({ label, position, margin }) => { const id = React.useRef(createId()); const element = React.useRef(null); - const show = () => { + const onMouseEnter = () => { tooltip.toggle(id.current, true); }; - const hide = () => { + const onMouseLeave = () => { tooltip.toggle(id.current, false); }; @@ -32,15 +32,15 @@ const Tooltip = ({ label, position, margin }) => { parent: parentElement, }); - parentElement.addEventListener('mouseenter', show); - parentElement.addEventListener('mouseleave', hide); + parentElement.addEventListener('mouseenter', onMouseEnter); + parentElement.addEventListener('mouseleave', onMouseLeave); } return () => { if (element.current && element.current.parentElement) { const parentElement = element.current.parentElement; - parentElement.removeEventListener('mouseenter', show); - parentElement.removeEventListener('mouseleave', hide); + parentElement.removeEventListener('mouseenter', onMouseEnter); + parentElement.removeEventListener('mouseleave', onMouseLeave); tooltip.remove(id.current); } From c0f3e04036a00f32eb72c030d41e0229c6d17c5b Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 17 Oct 2023 10:37:53 +0200 Subject: [PATCH 3/4] refactor: remove unused export --- src/common/Tooltip/index.js | 2 -- src/common/index.js | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/common/Tooltip/index.js b/src/common/Tooltip/index.js index a909daba2..82356a28f 100644 --- a/src/common/Tooltip/index.js +++ b/src/common/Tooltip/index.js @@ -2,10 +2,8 @@ const TooltipProvider = require('./TooltipProvider'); const Tooltip = require('./Tooltip'); -const useTooltip = require('./useTooltip'); module.exports = { TooltipProvider, Tooltip, - useTooltip, }; diff --git a/src/common/index.js b/src/common/index.js index 56a41f8b5..700fd432b 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -23,7 +23,7 @@ const SharePrompt = require('./SharePrompt'); const Slider = require('./Slider'); const TextInput = require('./TextInput'); const { ToastProvider, useToast } = require('./Toast'); -const { TooltipProvider, Tooltip, useTooltip } = require('./Tooltip'); +const { TooltipProvider, Tooltip } = require('./Tooltip'); const comparatorWithPriorities = require('./comparatorWithPriorities'); const CONSTANTS = require('./CONSTANTS'); const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender'); @@ -73,7 +73,6 @@ module.exports = { useToast, TooltipProvider, Tooltip, - useTooltip, comparatorWithPriorities, CONSTANTS, withCoreSuspender, From 922ef054e4b7065c952293408b7bb269d014d73d Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 17 Oct 2023 10:42:47 +0200 Subject: [PATCH 4/4] refactor: add transition to tooltips --- src/App/styles.less | 1 + src/common/Tooltip/styles.less | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/App/styles.less b/src/App/styles.less index e5ee4a835..d2ac62b59 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -126,6 +126,7 @@ html { color: var(--primary-foreground-color); border-radius: var(--border-radius); background-color: var(--modal-background-color); + transition: opacity 0.25s ease-out; } .router { diff --git a/src/common/Tooltip/styles.less b/src/common/Tooltip/styles.less index 22bcb40ef..d69825b71 100644 --- a/src/common/Tooltip/styles.less +++ b/src/common/Tooltip/styles.less @@ -13,8 +13,10 @@ .tooltip-container { position: fixed; visibility: hidden; + opacity: 0; &:global(.active) { visibility: visible; + opacity: 1; } } \ No newline at end of file