Skip to content

Commit 6761d95

Browse files
improvement(tab-strip): reveal the active tab in a fixed 200ms
1 parent da48640 commit 6761d95

1 file changed

Lines changed: 37 additions & 6 deletions

File tree

‎packages/emcn/src/components/tab-strip/tab-strip.tsx‎

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ import {
2222
scrollFadeAttributes,
2323
scrollFadeXClass,
2424
} from '@sim/emcn'
25-
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
25+
import {
26+
AnimatePresence,
27+
type AnimationPlaybackControls,
28+
animate,
29+
motion,
30+
useReducedMotion,
31+
} from 'framer-motion'
2632
import { Plus, X } from '../../icons'
2733
import { cn } from '../../lib/cn'
2834
import { Button } from '../button/button'
@@ -32,6 +38,12 @@ import { TabStripAction } from './tab-strip-action'
3238
const DRAG_EDGE_ZONE = 40
3339
const DRAG_SCROLL_SPEED = 8
3440
const TAB_TRANSITION = { duration: 0.1, ease: [0.2, 0, 0, 1] as const }
41+
/**
42+
* Fixed duration for bringing the active tab into view. Native smooth scrolling
43+
* takes longer the further it travels, so a crowded strip crawled across dozens
44+
* of tabs.
45+
*/
46+
const REVEAL_SCROLL_TRANSITION = { duration: 0.2, ease: TAB_TRANSITION.ease }
3547

3648
/**
3749
* Width, not flex-basis: `flex-1` compiles to `flex: 1 1 0%`, and Tailwind emits
@@ -508,6 +520,7 @@ export function TabStrip({
508520
const dropTargetIndexRef = useRef<number | null>(null)
509521
const autoScrollRafRef = useRef<number | null>(null)
510522
const autoScrollDirectionRef = useRef(0)
523+
const revealScrollRef = useRef<AnimationPlaybackControls | null>(null)
511524
const focusedTabRef = useRef<{
512525
id: string
513526
element: HTMLButtonElement
@@ -537,6 +550,11 @@ export function TabStrip({
537550
setCanScrollRight(node.scrollLeft < maxScrollLeft - 1)
538551
}, [])
539552

553+
const stopRevealScroll = useCallback(() => {
554+
revealScrollRef.current?.stop()
555+
revealScrollRef.current = null
556+
}, [])
557+
540558
const stopAutoScroll = useCallback(() => {
541559
if (autoScrollRafRef.current !== null) cancelAnimationFrame(autoScrollRafRef.current)
542560
autoScrollRafRef.current = null
@@ -579,9 +597,20 @@ export function TabStrip({
579597
// gradient at a scroll extreme, so no margin is needed to clear one.
580598
const nextLeft = Math.max(0, Math.min(maxScrollLeft, target))
581599
if (Math.abs(nextLeft - node.scrollLeft) < 1) return
582-
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false
583-
node.scrollTo({ left: nextLeft, behavior: reduceMotion ? 'auto' : 'smooth' })
584-
}, [activeRegularId, regularTabOrder])
600+
stopRevealScroll()
601+
if (reduceMotion) {
602+
node.scrollLeft = nextLeft
603+
return
604+
}
605+
revealScrollRef.current = animate(node.scrollLeft, nextLeft, {
606+
...REVEAL_SCROLL_TRANSITION,
607+
onUpdate: (left) => {
608+
node.scrollLeft = left
609+
},
610+
})
611+
}, [activeRegularId, regularTabOrder, reduceMotion, stopRevealScroll])
612+
613+
useEffect(() => stopRevealScroll, [stopRevealScroll])
585614

586615
useLayoutEffect(() => {
587616
revealActiveTab()
@@ -621,13 +650,14 @@ export function TabStrip({
621650
event.deltaY
622651
)
623652
if (next === null) return
653+
stopRevealScroll()
624654
node.scrollLeft = next
625655
updateOverflow()
626656
event.preventDefault()
627657
}
628658
strip.addEventListener('wheel', handleWheel, { passive: false })
629659
return () => strip.removeEventListener('wheel', handleWheel)
630-
}, [updateOverflow])
660+
}, [stopRevealScroll, updateOverflow])
631661

632662
const handleDragStart = useCallback(
633663
(event: ReactDragEvent<HTMLDivElement>, id: string) => {
@@ -677,6 +707,7 @@ export function TabStrip({
677707
stopAutoScroll()
678708
if (direction === 0) return
679709
autoScrollDirectionRef.current = direction
710+
stopRevealScroll()
680711
const tick = () => {
681712
const before = node.scrollLeft
682713
node.scrollLeft += direction * DRAG_SCROLL_SPEED
@@ -690,7 +721,7 @@ export function TabStrip({
690721
}
691722
autoScrollRafRef.current = requestAnimationFrame(tick)
692723
},
693-
[stopAutoScroll, tabs, updateOverflow]
724+
[stopAutoScroll, stopRevealScroll, tabs, updateOverflow]
694725
)
695726

696727
const handleDragOver = useCallback(

0 commit comments

Comments
 (0)