93 lines
2.6 KiB
JavaScript
93 lines
2.6 KiB
JavaScript
import { useState, useRef } from 'react';
|
|
import { useSwipeable } from 'react-swipeable';
|
|
import { useIsMobile } from '../hooks/useIsMobile';
|
|
import './SwipeableView.css';
|
|
|
|
export default function SwipeableView({
|
|
tabs = [],
|
|
activeIndex: controlledIndex,
|
|
onTabChange,
|
|
showTabs = true,
|
|
}) {
|
|
const isMobile = useIsMobile();
|
|
const [internalIndex, setInternalIndex] = useState(0);
|
|
const [swipeOffset, setSwipeOffset] = useState(0);
|
|
const [isSwiping, setIsSwiping] = useState(false);
|
|
const trackRef = useRef(null);
|
|
|
|
const activeIndex = controlledIndex !== undefined ? controlledIndex : internalIndex;
|
|
|
|
const setIndex = (idx) => {
|
|
const clamped = Math.max(0, Math.min(tabs.length - 1, idx));
|
|
if (controlledIndex === undefined) setInternalIndex(clamped);
|
|
onTabChange?.(clamped);
|
|
};
|
|
|
|
const handlers = useSwipeable({
|
|
onSwiping: ({ deltaX }) => {
|
|
if (!isMobile) return;
|
|
setIsSwiping(true);
|
|
setSwipeOffset(deltaX);
|
|
},
|
|
onSwipedLeft: ({ deltaX }) => {
|
|
if (!isMobile) return;
|
|
setIsSwiping(false);
|
|
setSwipeOffset(0);
|
|
if (Math.abs(deltaX) > 30) setIndex(activeIndex + 1);
|
|
},
|
|
onSwipedRight: ({ deltaX }) => {
|
|
if (!isMobile) return;
|
|
setIsSwiping(false);
|
|
setSwipeOffset(0);
|
|
if (Math.abs(deltaX) > 30) setIndex(activeIndex - 1);
|
|
},
|
|
onTouchEndOrOnMouseUp: () => {
|
|
setIsSwiping(false);
|
|
setSwipeOffset(0);
|
|
},
|
|
trackMouse: false,
|
|
trackTouch: true,
|
|
delta: 30,
|
|
preventScrollOnSwipe: false,
|
|
});
|
|
|
|
const trackTranslate = -activeIndex * 100 + (isSwiping ? (swipeOffset / (trackRef.current?.offsetWidth || 1)) * 100 : 0);
|
|
|
|
return (
|
|
<div className="swipeable-view">
|
|
{showTabs && (
|
|
<div className="swipeable-view__tabs" role="tablist">
|
|
{tabs.map((tab, i) => (
|
|
<button
|
|
key={tab.key}
|
|
role="tab"
|
|
aria-selected={i === activeIndex}
|
|
className={`swipeable-view__tab ${i === activeIndex ? 'is-active' : ''}`}
|
|
onClick={() => setIndex(i)}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
<div
|
|
{...(isMobile ? handlers : {})}
|
|
ref={trackRef}
|
|
className={`swipeable-view__track ${isSwiping ? 'is-swiping' : ''}`}
|
|
style={{ transform: `translateX(${trackTranslate}%)` }}
|
|
>
|
|
{tabs.map((tab, i) => (
|
|
<div
|
|
key={tab.key}
|
|
role="tabpanel"
|
|
aria-hidden={i !== activeIndex}
|
|
className="swipeable-view__panel"
|
|
>
|
|
{tab.content}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|