'use client'; import * as React from 'react'; import { makeStyles, mergeClasses } from '@griffel/react'; import { useTabListContext_unstable } from '../TabList/TabListContext'; import { tokens } from '@fluentui/react-theme'; import { useAnimationFrame } from '@fluentui/react-utilities'; // eslint-disable-next-line @typescript-eslint/naming-convention const tabIndicatorCssVars_unstable = { offsetVar: '--fui-Tab__indicator--offset', scaleVar: '--fui-Tab__indicator--scale' }; const useActiveIndicatorStyles = makeStyles({ base: { // overflow is required to allow the selection indicator to animate outside the tab area. overflow: 'visible' }, animated: { '::after': { transitionProperty: 'transform', transitionDuration: `${tokens.durationSlow}`, transitionTimingFunction: `${tokens.curveDecelerateMax}` }, '@media (prefers-reduced-motion: reduce)': { '::after': { transitionProperty: 'none', transitionDuration: '0.01ms' } } }, horizontal: { '::after': { transformOrigin: 'left', transform: `translateX(var(${tabIndicatorCssVars_unstable.offsetVar})) scaleX(var(${tabIndicatorCssVars_unstable.scaleVar}))` } }, vertical: { '::after': { transformOrigin: 'top', transform: `translateY(var(${tabIndicatorCssVars_unstable.offsetVar})) scaleY(var(${tabIndicatorCssVars_unstable.scaleVar}))` } } }); const calculateTabRect = (element)=>{ if (element) { var _element_parentElement; const parentRect = ((_element_parentElement = element.parentElement) === null || _element_parentElement === void 0 ? void 0 : _element_parentElement.getBoundingClientRect()) || { x: 0, y: 0, width: 0, height: 0 }; const tabRect = element.getBoundingClientRect(); return { x: tabRect.x - parentRect.x, y: tabRect.y - parentRect.y, width: tabRect.width, height: tabRect.height }; } return undefined; }; const getRegisteredTabRect = (registeredTabs, value)=>{ var _registeredTabs_JSON_stringify; const element = isValueDefined(value) ? (_registeredTabs_JSON_stringify = registeredTabs[JSON.stringify(value)]) === null || _registeredTabs_JSON_stringify === void 0 ? void 0 : _registeredTabs_JSON_stringify.ref.current : undefined; return element ? calculateTabRect(element) : undefined; }; // eslint-disable-next-line eqeqeq const isValueDefined = (value)=>value != null; /** * Adds additional styling to the active tab selection indicator to create a sliding animation. */ export const useTabAnimatedIndicatorStyles_unstable = (state)=>{ const { disabled, selected, vertical } = state; const activeIndicatorStyles = useActiveIndicatorStyles(); const [lastAnimatedFrom, setLastAnimatedFrom] = React.useState(); const [animationValues, setAnimationValues] = React.useState({ offset: 0, scale: 1 }); const getRegisteredTabs = useTabListContext_unstable((ctx)=>ctx.getRegisteredTabs); const [requestAnimationFrame] = useAnimationFrame(); if (selected) { const { previousSelectedValue, selectedValue, registeredTabs } = getRegisteredTabs(); if (isValueDefined(previousSelectedValue) && lastAnimatedFrom !== previousSelectedValue) { const previousSelectedTabRect = getRegisteredTabRect(registeredTabs, previousSelectedValue); const selectedTabRect = getRegisteredTabRect(registeredTabs, selectedValue); if (selectedTabRect && previousSelectedTabRect) { const offset = vertical ? previousSelectedTabRect.y - selectedTabRect.y : previousSelectedTabRect.x - selectedTabRect.x; const scale = vertical ? previousSelectedTabRect.height / selectedTabRect.height : previousSelectedTabRect.width / selectedTabRect.width; setAnimationValues({ offset, scale }); setLastAnimatedFrom(previousSelectedValue); // Reset the animation values after the animation is complete requestAnimationFrame(()=>setAnimationValues({ offset: 0, scale: 1 })); } } } else if (isValueDefined(lastAnimatedFrom)) { // need to clear the last animated from so that if this tab is selected again // from the same previous tab as last time, that animation still happens. setLastAnimatedFrom(undefined); } // do not apply any animation if the tab is disabled if (disabled) { return state; } // the animation should only happen as the selection indicator returns to its // original position and not when set at the previous tabs position. const animating = animationValues.offset === 0 && animationValues.scale === 1; state.root.className = mergeClasses(state.root.className, selected && activeIndicatorStyles.base, selected && animating && activeIndicatorStyles.animated, selected && (vertical ? activeIndicatorStyles.vertical : activeIndicatorStyles.horizontal)); const rootCssVars = { [tabIndicatorCssVars_unstable.offsetVar]: `${animationValues.offset}px`, [tabIndicatorCssVars_unstable.scaleVar]: `${animationValues.scale}` }; state.root.style = { ...rootCssVars, ...state.root.style }; return state; };