diff --git a/resources/js/components/gallery/CategoryPillCarousel.css b/resources/js/components/gallery/CategoryPillCarousel.css index f1454bef..40f57111 100644 --- a/resources/js/components/gallery/CategoryPillCarousel.css +++ b/resources/js/components/gallery/CategoryPillCarousel.css @@ -156,3 +156,24 @@ background: linear-gradient(135deg, #f08830 0%, #d9720f 100%); transform: none; } + +.nb-react-drag-zone { + position: absolute; + left: 48px; + right: 48px; + bottom: 0; + height: 12px; + z-index: 1; + cursor: grab; + -webkit-tap-highlight-color: transparent; +} + +.nb-react-drag-zone:active { + cursor: grabbing; +} + +@media (hover: none) and (pointer: coarse) { + .nb-react-drag-zone { + display: none; + } +} diff --git a/resources/js/components/gallery/CategoryPillCarousel.jsx b/resources/js/components/gallery/CategoryPillCarousel.jsx index 12c08ae4..d731cd8b 100644 --- a/resources/js/components/gallery/CategoryPillCarousel.jsx +++ b/resources/js/components/gallery/CategoryPillCarousel.jsx @@ -12,11 +12,14 @@ export default function CategoryPillCarousel({ }) { const viewportRef = useRef(null); const stripRef = useRef(null); + const dragZoneRef = useRef(null); const animationRef = useRef(0); + const suppressClickRef = useRef(false); const dragStateRef = useRef({ active: false, - moved: false, + started: false, pointerId: null, + pointerType: 'mouse', startX: 0, startOffset: 0, }); @@ -154,10 +157,13 @@ export default function CategoryPillCarousel({ useEffect(() => { const strip = stripRef.current; - if (!strip) return; + const dragZone = dragZoneRef.current; + if (!strip || !dragZone) return; const onPointerDown = (event) => { - if (event.pointerType === 'mouse' && event.button !== 0) return; + const isMouse = event.pointerType === 'mouse'; + const fromDragZone = event.currentTarget === dragZone; + if (isMouse && !fromDragZone) return; if (animationRef.current) { cancelAnimationFrame(animationRef.current); @@ -165,18 +171,17 @@ export default function CategoryPillCarousel({ } dragStateRef.current.active = true; - dragStateRef.current.moved = false; + dragStateRef.current.started = false; dragStateRef.current.pointerId = event.pointerId; + dragStateRef.current.pointerType = event.pointerType || 'mouse'; dragStateRef.current.startX = event.clientX; dragStateRef.current.startOffset = offset; - setDragging(true); + setDragging(false); if (strip.setPointerCapture) { try { strip.setPointerCapture(event.pointerId); } catch (_) { /* no-op */ } } - - event.preventDefault(); }; const onPointerMove = (event) => { @@ -184,7 +189,20 @@ export default function CategoryPillCarousel({ if (!state.active || state.pointerId !== event.pointerId) return; const dx = event.clientX - state.startX; - if (Math.abs(dx) > 3) state.moved = true; + const threshold = state.pointerType === 'touch' ? 12 : 8; + if (!state.started) { + if (Math.abs(dx) <= threshold) { + return; + } + + state.started = true; + setDragging(true); + } + + if (state.started) { + event.preventDefault(); + } + moveTo(state.startOffset + dx); }; @@ -192,7 +210,9 @@ export default function CategoryPillCarousel({ const state = dragStateRef.current; if (!state.active || state.pointerId !== event.pointerId) return; + suppressClickRef.current = state.started; state.active = false; + state.started = false; state.pointerId = null; setDragging(false); @@ -202,10 +222,10 @@ export default function CategoryPillCarousel({ }; const onClickCapture = (event) => { - if (!dragStateRef.current.moved) return; + if (!suppressClickRef.current) return; event.preventDefault(); event.stopPropagation(); - dragStateRef.current.moved = false; + suppressClickRef.current = false; }; strip.addEventListener('pointerdown', onPointerDown); @@ -214,12 +234,22 @@ export default function CategoryPillCarousel({ strip.addEventListener('pointercancel', onPointerUpOrCancel); strip.addEventListener('click', onClickCapture, true); + dragZone.addEventListener('pointerdown', onPointerDown); + dragZone.addEventListener('pointermove', onPointerMove); + dragZone.addEventListener('pointerup', onPointerUpOrCancel); + dragZone.addEventListener('pointercancel', onPointerUpOrCancel); + return () => { strip.removeEventListener('pointerdown', onPointerDown); strip.removeEventListener('pointermove', onPointerMove); strip.removeEventListener('pointerup', onPointerUpOrCancel); strip.removeEventListener('pointercancel', onPointerUpOrCancel); strip.removeEventListener('click', onClickCapture, true); + + dragZone.removeEventListener('pointerdown', onPointerDown); + dragZone.removeEventListener('pointermove', onPointerMove); + dragZone.removeEventListener('pointerup', onPointerUpOrCancel); + dragZone.removeEventListener('pointercancel', onPointerUpOrCancel); }; }, [moveTo, offset]); @@ -277,6 +307,12 @@ export default function CategoryPillCarousel({ > + + ); }