diff --git a/frontend/app/components/quiz/questions/Ordering.tsx b/frontend/app/components/quiz/questions/Ordering.tsx index 43c2350..60bdbbe 100644 --- a/frontend/app/components/quiz/questions/Ordering.tsx +++ b/frontend/app/components/quiz/questions/Ordering.tsx @@ -1,7 +1,7 @@ 'use client'; import { GripVertical } from 'lucide-react'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import SubmitButton from '@/app/components/common/SubmitButton'; import { cn } from '@/lib/cn'; @@ -32,12 +32,65 @@ export default function Ordering({ items.map((text, origIdx) => ({ text, origIdx })), ); const [dragging, setDragging] = useState(null); + const listRef = useRef(null); + const draggingRef = useRef(null); + const [announcement, setAnnouncement] = useState(''); const moveItem = (from: number, to: number) => { - const next = [...ordered]; - const [item] = next.splice(from, 1); - next.splice(to, 0, item); - setOrdered(next); + setOrdered((prev) => { + const next = [...prev]; + const [item] = next.splice(from, 1); + next.splice(to, 0, item); + return next; + }); + }; + + const moveByKeyboard = (i: number, dir: -1 | 1) => { + const to = i + dir; + if (to < 0 || to >= ordered.length) return; + moveItem(i, to); + setAnnouncement( + `${ordered[i].text}: pozycja ${to + 1} z ${ordered.length}`, + ); + }; + + const rowUnderPointer = (clientY: number): number | null => { + const rows = listRef.current?.children; + if (!rows) return null; + for (let k = 0; k < rows.length; k++) { + const r = rows[k].getBoundingClientRect(); + if (clientY >= r.top && clientY <= r.bottom) return k; + } + return null; + }; + + const startDrag = (i: number) => (e: React.PointerEvent) => { + if (draggingRef.current !== null) return; + if (e.button !== 0) return; + e.preventDefault(); + draggingRef.current = i; + setDragging(i); + e.currentTarget.setPointerCapture(e.pointerId); + }; + + const onDragMove = (e: React.PointerEvent) => { + const from = draggingRef.current; + if (from === null) return; + const to = rowUnderPointer(e.clientY); + if (to !== null && to !== from) { + moveItem(from, to); + draggingRef.current = to; + setDragging(to); + } + }; + + const endDrag = (e: React.PointerEvent) => { + if (draggingRef.current === null) return; + draggingRef.current = null; + setDragging(null); + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId); + } }; return ( @@ -48,37 +101,48 @@ export default function Ordering({ > -
+
{ordered.map(({ text, origIdx }, i) => { const isDragging = dragging === i; return (
setDragging(i)} - onDragOver={(e) => { - e.preventDefault(); - if (dragging !== null && dragging !== i) { - moveItem(dragging, i); - setDragging(i); + role="button" + tabIndex={0} + aria-label={`Pozycja ${i + 1} z ${ordered.length}: ${text}. Użyj strzałek w górę i w dół, aby zmienić kolejność.`} + onKeyDown={(e) => { + if (e.key === 'ArrowUp') { + e.preventDefault(); + moveByKeyboard(i, -1); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + moveByKeyboard(i, 1); } }} - onDragEnd={() => setDragging(null)} className={cn( - 'flex w-full cursor-grab items-center gap-3 rounded-2xl px-4 py-3.5 transition-all', + 'flex w-full items-center gap-3 rounded-2xl px-4 py-3.5 transition-all outline-none select-none focus-visible:ring-2 focus-visible:ring-[var(--orange)]', isDragging ? 'border-2 border-[var(--selected-border)] bg-[var(--selected-bg)] shadow-[0_4px_12px_rgba(246,162,0,0.25)]' : 'border-[1.5px] border-[var(--border)] bg-[var(--card-bg)]', )} > - + {i + 1} @@ -95,6 +159,10 @@ export default function Ordering({ })}
+
+ {announcement} +
+ onSubmit?.(ordered.map((x) => x.origIdx))}