optimize swipeable tabs

This commit is contained in:
yohlo
2025-08-27 09:58:55 -05:00
parent 1eb621dd34
commit e5f3bbe095

View File

@@ -1,6 +1,6 @@
import { FloatingIndicator, UnstyledButton, Box, Text } from "@mantine/core"; import { FloatingIndicator, UnstyledButton, Box, Text } from "@mantine/core";
import { Carousel } from "@mantine/carousel"; import { Carousel } from "@mantine/carousel";
import { useState, useEffect, ReactNode, useRef } from "react"; import { useState, useEffect, ReactNode, useRef, useCallback, useMemo } from "react";
import { useRouter } from "@tanstack/react-router"; import { useRouter } from "@tanstack/react-router";
interface TabItem { interface TabItem {
@@ -19,22 +19,22 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps
const search = router.state.location.search as any; const search = router.state.location.search as any;
const [embla, setEmbla] = useState<any>(null); const [embla, setEmbla] = useState<any>(null);
const getActiveTabFromUrl = () => { const getActiveTabFromUrl = useCallback(() => {
const urlTab = search?.tab; const urlTab = search?.tab;
if (typeof urlTab === 'string') { if (typeof urlTab === 'string') {
const tabIndex = tabs.findIndex(tab => tab.label.toLowerCase() === urlTab.toLowerCase()); const tabIndex = tabs.findIndex(tab => tab.label.toLowerCase() === urlTab.toLowerCase());
return tabIndex !== -1 ? tabIndex : defaultTab; return tabIndex !== -1 ? tabIndex : defaultTab;
} }
return defaultTab; return defaultTab;
}; }, [search?.tab, tabs, defaultTab]);
const [activeTab, setActiveTab] = useState(getActiveTabFromUrl); const [activeTab, setActiveTab] = useState(getActiveTabFromUrl);
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null); const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
const [controlsRefs, setControlsRefs] = useState<Record<number, HTMLSpanElement | null>>({}); const controlsRefs = useRef<Record<number, HTMLSpanElement | null>>({});
const slideRefs = useRef<Record<number, HTMLDivElement | null>>({}); const slideRefs = useRef<Record<number, HTMLDivElement | null>>({});
const [carouselHeight, setCarouselHeight] = useState<number | 'auto'>('auto'); const [carouselHeight, setCarouselHeight] = useState<number | 'auto'>('auto');
const changeTab = (index: number) => { const changeTab = useCallback((index: number) => {
if (index === activeTab || index < 0 || index >= tabs.length) return; if (index === activeTab || index < 0 || index >= tabs.length) return;
setActiveTab(index); setActiveTab(index);
@@ -47,19 +47,20 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps
url.searchParams.set('tab', tabLabel); url.searchParams.set('tab', tabLabel);
window.history.replaceState(null, '', url.toString()); window.history.replaceState(null, '', url.toString());
} }
}; }, [activeTab, tabs, embla, onTabChange]);
const handleEmblaSelect = useCallback(() => {
if (!embla) return;
const newIndex = embla.selectedScrollSnap();
changeTab(newIndex);
}, [embla, changeTab]);
useEffect(() => { useEffect(() => {
if (!embla) return; if (!embla) return;
const onSelect = () => { embla.on("select", handleEmblaSelect);
const newIndex = embla.selectedScrollSnap(); return () => embla.off("select", handleEmblaSelect);
changeTab(newIndex); }, [embla, handleEmblaSelect]);
};
embla.on("select", onSelect);
return () => embla.off("select", onSelect);
}, [embla, activeTab, tabs]);
useEffect(() => { useEffect(() => {
const newActiveTab = getActiveTabFromUrl(); const newActiveTab = getActiveTabFromUrl();
@@ -77,14 +78,13 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps
} }
}, [activeTab]); }, [activeTab]);
const setControlRef = (index: number) => (node: HTMLSpanElement | null) => { const setControlRef = useCallback((index: number) => (node: HTMLSpanElement | null) => {
controlsRefs[index] = node; controlsRefs.current[index] = node;
setControlsRefs(controlsRefs); }, []);
};
const setSlideRef = (index: number) => (node: HTMLDivElement | null) => { const setSlideRef = useCallback((index: number) => (node: HTMLDivElement | null) => {
slideRefs.current[index] = node; slideRefs.current[index] = node;
}; }, []);
return ( return (
<Box> <Box>
@@ -100,7 +100,7 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps
}} }}
> >
<FloatingIndicator <FloatingIndicator
target={controlsRefs[activeTab]} target={controlsRefs.current[activeTab]}
parent={rootRef} parent={rootRef}
styles={{ styles={{
root: { root: {