From 99bd0a9d3548e603a414115cba91a4654173f8ac Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sun, 1 Feb 2026 14:48:47 +0000 Subject: [PATCH] feat(Tooltip): add floating animations for tooltip appearance based on placement --- .../Elements/MainModal/backend/Tabs.jsx | 39 ++++++----- src/components/Elements/Tooltip/Tooltip.jsx | 10 +-- src/components/Elements/Tooltip/tooltip.scss | 65 ++++++++++++++++++- 3 files changed, 89 insertions(+), 25 deletions(-) diff --git a/src/components/Elements/MainModal/backend/Tabs.jsx b/src/components/Elements/MainModal/backend/Tabs.jsx index 4f1d2333..1edede01 100644 --- a/src/components/Elements/MainModal/backend/Tabs.jsx +++ b/src/components/Elements/MainModal/backend/Tabs.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useLayoutEffect, useRef } from 'react'; import { useT } from 'contexts/TranslationContext'; import variables from 'config/variables'; import Tab from './Tab'; @@ -38,7 +38,6 @@ const Tabs = ({ }; const initial = getInitialSection(); - const [currentTab, setCurrentTab] = useState(initial.label); const [currentName, setCurrentName] = useState(initial.name); const [showReminder, setShowReminder] = useState(localStorage.getItem('showReminder') === 'true'); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState( @@ -47,12 +46,24 @@ const Tabs = ({ const [searchQuery, setSearchQuery] = useState(''); const contentRef = useRef(null); + // Derive currentTab label from currentName - avoids setState in effects + const currentTab = (() => { + if (sections && currentName) { + const section = sections.find((s) => s.name === currentName); + if (section) { + return t(section.label); + } + } + // Fallback: find label from children + const child = children.find((c) => c.props.name === currentName); + return child?.props.label || children[0]?.props.label; + })(); + const handleTabClick = (tab, name) => { if (name !== currentName) { variables.stats.postEvent('tab', `Opened ${name}`); } - setCurrentTab(tab); setCurrentName(name); // Scroll content to top when changing tabs @@ -73,24 +84,12 @@ const Tabs = ({ } }, []); - // Update labels when language changes - useEffect(() => { - if (sections && currentName) { - const section = sections.find((s) => s.name === currentName); - if (section) { - const newLabel = t(section.label); - setCurrentTab(newLabel); - } - } - }, [t, sections, currentName]); - // Handle navigation trigger for settings sections (popstate) - useEffect(() => { + // useLayoutEffect is appropriate here for synchronous state updates before paint + useLayoutEffect(() => { if (navigationTrigger?.type === 'settings-section' && sections) { const section = sections.find((s) => s.name === navigationTrigger.data); if (section) { - const label = t(section.label); - setCurrentTab(label); setCurrentName(section.name); // Scroll content to top when navigating via browser history if (contentRef.current) { @@ -98,12 +97,12 @@ const Tabs = ({ } } } - }, [navigationTrigger, sections, t]); + }, [navigationTrigger, sections]); // Reset to first tab when requested - useEffect(() => { + // useLayoutEffect is appropriate here for synchronous state updates before paint + useLayoutEffect(() => { if (resetToFirst) { - setCurrentTab(children[0]?.props.label); setCurrentName(children[0]?.props.name); // Scroll content to top when resetting to first tab if (contentRef.current) { diff --git a/src/components/Elements/Tooltip/Tooltip.jsx b/src/components/Elements/Tooltip/Tooltip.jsx index 5df2df0c..fbb4206e 100644 --- a/src/components/Elements/Tooltip/Tooltip.jsx +++ b/src/components/Elements/Tooltip/Tooltip.jsx @@ -1,4 +1,4 @@ -import { useState, memo, useRef } from 'react'; +import { useState, memo, useRef, useId } from 'react'; import { useFloating, flip, offset, shift } from '@floating-ui/react-dom'; import './tooltip.scss'; @@ -6,7 +6,7 @@ function Tooltip({ children, title, style, placement, subtitle }) { const [showTooltip, setShowTooltip] = useState(false); const [isClosing, setIsClosing] = useState(false); const [reference, setReference] = useState(null); - const tooltipId = useRef(`tooltip-${Math.random()}`); + const tooltipId = useId(); const closeTimeout = useRef(null); const { @@ -23,6 +23,8 @@ function Tooltip({ children, title, style, placement, subtitle }) { }, }); + const { setFloating } = refs; + const handleMouseEnter = () => { // Clear any pending close timeout if mouse re-enters during exit if (closeTimeout.current) { @@ -76,13 +78,13 @@ function Tooltip({ children, title, style, placement, subtitle }) { onFocus={handleFocus} onBlur={handleBlur} ref={setReference} - aria-describedby={tooltipId.current} + aria-describedby={tooltipId} > {children} {(showTooltip || isClosing) && (