fix: separate pill clicks from carousel drag with bottom drag zone
This commit is contained in:
@@ -156,3 +156,24 @@
|
|||||||
background: linear-gradient(135deg, #f08830 0%, #d9720f 100%);
|
background: linear-gradient(135deg, #f08830 0%, #d9720f 100%);
|
||||||
transform: none;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ export default function CategoryPillCarousel({
|
|||||||
}) {
|
}) {
|
||||||
const viewportRef = useRef(null);
|
const viewportRef = useRef(null);
|
||||||
const stripRef = useRef(null);
|
const stripRef = useRef(null);
|
||||||
|
const dragZoneRef = useRef(null);
|
||||||
const animationRef = useRef(0);
|
const animationRef = useRef(0);
|
||||||
|
const suppressClickRef = useRef(false);
|
||||||
const dragStateRef = useRef({
|
const dragStateRef = useRef({
|
||||||
active: false,
|
active: false,
|
||||||
moved: false,
|
started: false,
|
||||||
pointerId: null,
|
pointerId: null,
|
||||||
|
pointerType: 'mouse',
|
||||||
startX: 0,
|
startX: 0,
|
||||||
startOffset: 0,
|
startOffset: 0,
|
||||||
});
|
});
|
||||||
@@ -154,10 +157,13 @@ export default function CategoryPillCarousel({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const strip = stripRef.current;
|
const strip = stripRef.current;
|
||||||
if (!strip) return;
|
const dragZone = dragZoneRef.current;
|
||||||
|
if (!strip || !dragZone) return;
|
||||||
|
|
||||||
const onPointerDown = (event) => {
|
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) {
|
if (animationRef.current) {
|
||||||
cancelAnimationFrame(animationRef.current);
|
cancelAnimationFrame(animationRef.current);
|
||||||
@@ -165,18 +171,17 @@ export default function CategoryPillCarousel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
dragStateRef.current.active = true;
|
dragStateRef.current.active = true;
|
||||||
dragStateRef.current.moved = false;
|
dragStateRef.current.started = false;
|
||||||
dragStateRef.current.pointerId = event.pointerId;
|
dragStateRef.current.pointerId = event.pointerId;
|
||||||
|
dragStateRef.current.pointerType = event.pointerType || 'mouse';
|
||||||
dragStateRef.current.startX = event.clientX;
|
dragStateRef.current.startX = event.clientX;
|
||||||
dragStateRef.current.startOffset = offset;
|
dragStateRef.current.startOffset = offset;
|
||||||
|
|
||||||
setDragging(true);
|
setDragging(false);
|
||||||
|
|
||||||
if (strip.setPointerCapture) {
|
if (strip.setPointerCapture) {
|
||||||
try { strip.setPointerCapture(event.pointerId); } catch (_) { /* no-op */ }
|
try { strip.setPointerCapture(event.pointerId); } catch (_) { /* no-op */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerMove = (event) => {
|
const onPointerMove = (event) => {
|
||||||
@@ -184,7 +189,20 @@ export default function CategoryPillCarousel({
|
|||||||
if (!state.active || state.pointerId !== event.pointerId) return;
|
if (!state.active || state.pointerId !== event.pointerId) return;
|
||||||
|
|
||||||
const dx = event.clientX - state.startX;
|
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);
|
moveTo(state.startOffset + dx);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,7 +210,9 @@ export default function CategoryPillCarousel({
|
|||||||
const state = dragStateRef.current;
|
const state = dragStateRef.current;
|
||||||
if (!state.active || state.pointerId !== event.pointerId) return;
|
if (!state.active || state.pointerId !== event.pointerId) return;
|
||||||
|
|
||||||
|
suppressClickRef.current = state.started;
|
||||||
state.active = false;
|
state.active = false;
|
||||||
|
state.started = false;
|
||||||
state.pointerId = null;
|
state.pointerId = null;
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
|
|
||||||
@@ -202,10 +222,10 @@ export default function CategoryPillCarousel({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onClickCapture = (event) => {
|
const onClickCapture = (event) => {
|
||||||
if (!dragStateRef.current.moved) return;
|
if (!suppressClickRef.current) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
dragStateRef.current.moved = false;
|
suppressClickRef.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
strip.addEventListener('pointerdown', onPointerDown);
|
strip.addEventListener('pointerdown', onPointerDown);
|
||||||
@@ -214,12 +234,22 @@ export default function CategoryPillCarousel({
|
|||||||
strip.addEventListener('pointercancel', onPointerUpOrCancel);
|
strip.addEventListener('pointercancel', onPointerUpOrCancel);
|
||||||
strip.addEventListener('click', onClickCapture, true);
|
strip.addEventListener('click', onClickCapture, true);
|
||||||
|
|
||||||
|
dragZone.addEventListener('pointerdown', onPointerDown);
|
||||||
|
dragZone.addEventListener('pointermove', onPointerMove);
|
||||||
|
dragZone.addEventListener('pointerup', onPointerUpOrCancel);
|
||||||
|
dragZone.addEventListener('pointercancel', onPointerUpOrCancel);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
strip.removeEventListener('pointerdown', onPointerDown);
|
strip.removeEventListener('pointerdown', onPointerDown);
|
||||||
strip.removeEventListener('pointermove', onPointerMove);
|
strip.removeEventListener('pointermove', onPointerMove);
|
||||||
strip.removeEventListener('pointerup', onPointerUpOrCancel);
|
strip.removeEventListener('pointerup', onPointerUpOrCancel);
|
||||||
strip.removeEventListener('pointercancel', onPointerUpOrCancel);
|
strip.removeEventListener('pointercancel', onPointerUpOrCancel);
|
||||||
strip.removeEventListener('click', onClickCapture, true);
|
strip.removeEventListener('click', onClickCapture, true);
|
||||||
|
|
||||||
|
dragZone.removeEventListener('pointerdown', onPointerDown);
|
||||||
|
dragZone.removeEventListener('pointermove', onPointerMove);
|
||||||
|
dragZone.removeEventListener('pointerup', onPointerUpOrCancel);
|
||||||
|
dragZone.removeEventListener('pointercancel', onPointerUpOrCancel);
|
||||||
};
|
};
|
||||||
}, [moveTo, offset]);
|
}, [moveTo, offset]);
|
||||||
|
|
||||||
@@ -277,6 +307,12 @@ export default function CategoryPillCarousel({
|
|||||||
>
|
>
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor" className="w-[18px] h-[18px]" aria-hidden="true"><path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd"/></svg>
|
<svg viewBox="0 0 20 20" fill="currentColor" className="w-[18px] h-[18px]" aria-hidden="true"><path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={dragZoneRef}
|
||||||
|
className="nb-react-drag-zone"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user