mirror of
https://github.com/mue/mue.git
synced 2026-07-13 12:07:45 +02:00
feat(Tooltip): add floating animations for tooltip appearance based on placement
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
</div>
|
||||
{(showTooltip || isClosing) && (
|
||||
<span
|
||||
ref={refs.setFloating}
|
||||
ref={setFloating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? '',
|
||||
|
||||
@@ -5,6 +5,54 @@
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@keyframes floatingFromTop {
|
||||
0% {
|
||||
transform: translateY(5px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatingFromBottom {
|
||||
0% {
|
||||
transform: translateY(-5px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatingFromLeft {
|
||||
0% {
|
||||
transform: translateX(5px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatingFromRight {
|
||||
0% {
|
||||
transform: translateX(-5px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floating {
|
||||
0% {
|
||||
transform: translate(0, -5px);
|
||||
@@ -46,9 +94,24 @@
|
||||
&[data-status='open'] {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
animation-name: floating;
|
||||
animation-duration: 0.3s;
|
||||
animation-timing-function: ease-in;
|
||||
|
||||
&[data-placement^='top'] {
|
||||
animation-name: floatingFromTop;
|
||||
}
|
||||
|
||||
&[data-placement^='bottom'] {
|
||||
animation-name: floatingFromBottom;
|
||||
}
|
||||
|
||||
&[data-placement^='left'] {
|
||||
animation-name: floatingFromLeft;
|
||||
}
|
||||
|
||||
&[data-placement^='right'] {
|
||||
animation-name: floatingFromRight;
|
||||
}
|
||||
}
|
||||
|
||||
// Closing state - exit animation
|
||||
|
||||
Reference in New Issue
Block a user