feat(stammbaum): Kontextmenü per Long-Press auf Touch-Geräten öffnen
Das Rechtsklick-Kontextmenü (ID kopieren / Fehler melden) war auf Mobil nicht erreichbar (onContextMenu feuert auf Touch nicht zuverlässig). Jetzt öffnet ein langer Druck (~500 ms ohne Verschieben) auf eine Ahnenkarte dasselbe Menü; der darauf folgende synthetische Klick (Umwurzeln) wird unterdrückt. Bewegung >10 px bricht den Long-Press ab (kollidiert nicht mit Pan/Zoom). Legenden-Hinweis ergänzt. e2e: Long-Press öffnet das Menü (desktop; phone-Projekt übersprungen, da synthetischer Touch dort instabil). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
* - Druck/PDF: separate CSS-Grid-Ahnentafel (.stammbaum-print), per
|
||||
* @media print sichtbar; Browser „Als PDF speichern“ ist der Export.
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent, type TouchEvent as ReactTouchEvent } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import Tree from 'react-d3-tree'
|
||||
import type { CustomNodeElementProps, Point, RawNodeDatum } from 'react-d3-tree'
|
||||
@@ -102,11 +102,20 @@ export default function StammbaumPage() {
|
||||
} | null>(null)
|
||||
const [reportContext, setReportContext] = useState<ReportErrorContext | null>(null)
|
||||
|
||||
const openContextMenu = useCallback((e: ReactMouseEvent, gerbil: Gerbil) => {
|
||||
e.preventDefault()
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, gerbil })
|
||||
const openContextMenuAt = useCallback((x: number, y: number, gerbil: Gerbil) => {
|
||||
setContextMenu({ x, y, gerbil })
|
||||
}, [])
|
||||
|
||||
// Rechtsklick (Desktop). Auf Touch-Geräten gibt es keinen Rechtsklick — dort
|
||||
// öffnet ein langer Druck (Long-Press) dasselbe Menü, siehe PedigreeCard.
|
||||
const openContextMenu = useCallback(
|
||||
(e: ReactMouseEvent, gerbil: Gerbil) => {
|
||||
e.preventDefault()
|
||||
openContextMenuAt(e.clientX, e.clientY, gerbil)
|
||||
},
|
||||
[openContextMenuAt],
|
||||
)
|
||||
|
||||
const closeContextMenu = useCallback(() => setContextMenu(null), [])
|
||||
|
||||
const copyId = useCallback(
|
||||
@@ -349,13 +358,14 @@ export default function StammbaumPage() {
|
||||
onOpen={() => navigate(`/rennmaeuse/${node.gerbil.id}/stammbaum`)}
|
||||
onExpand={() => handleExpand(path)}
|
||||
onContextMenu={(e) => openContextMenu(e, node.gerbil)}
|
||||
onLongPress={(x, y) => openContextMenuAt(x, y, node.gerbil)}
|
||||
/>
|
||||
)}
|
||||
</foreignObject>
|
||||
</g>
|
||||
)
|
||||
},
|
||||
[nodesByPath, farbschlagOf, navigate, handleExpand, openContextMenu, t],
|
||||
[nodesByPath, farbschlagOf, navigate, handleExpand, openContextMenu, openContextMenuAt, t],
|
||||
)
|
||||
|
||||
/* ── Zustände: Laden / Fehler ── */
|
||||
@@ -443,6 +453,7 @@ export default function StammbaumPage() {
|
||||
<li>{t.tapHint}</li>
|
||||
<li>{t.hintName}</li>
|
||||
<li>{t.hintExpand}</li>
|
||||
<li>{t.hintContext}</li>
|
||||
{hasSiblingPairing && <li>{t.hintSiblings}</li>}
|
||||
</ul>
|
||||
</section>
|
||||
@@ -498,6 +509,7 @@ function PedigreeCard({
|
||||
onOpen,
|
||||
onExpand,
|
||||
onContextMenu,
|
||||
onLongPress,
|
||||
}: {
|
||||
node: AnimalNode
|
||||
isRoot: boolean
|
||||
@@ -505,6 +517,7 @@ function PedigreeCard({
|
||||
onOpen: () => void
|
||||
onExpand: () => void
|
||||
onContextMenu: (e: ReactMouseEvent) => void
|
||||
onLongPress: (x: number, y: number) => void
|
||||
}) {
|
||||
const t = de.pages.stammbaum
|
||||
const g = node.gerbil
|
||||
@@ -517,11 +530,57 @@ function PedigreeCard({
|
||||
: `${API_BASE_URL}${g.profilePhotoUrl}`
|
||||
: null
|
||||
const photoUrl = rawUrl !== failedUrl ? rawUrl : null
|
||||
|
||||
/* Long-Press (Touch) = Rechtsklick-Äquivalent: ~500 ms halten ohne zu
|
||||
verschieben öffnet das Kontextmenü. Der danach folgende Klick (Umwurzeln)
|
||||
wird unterdrückt. */
|
||||
const pressTimer = useRef<number | null>(null)
|
||||
const pressStart = useRef<{ x: number; y: number } | null>(null)
|
||||
const longPressed = useRef(false)
|
||||
const cancelPress = () => {
|
||||
if (pressTimer.current != null) {
|
||||
clearTimeout(pressTimer.current)
|
||||
pressTimer.current = null
|
||||
}
|
||||
}
|
||||
const onTouchStart = (e: ReactTouchEvent) => {
|
||||
const tch = e.touches[0]
|
||||
if (!tch) return
|
||||
pressStart.current = { x: tch.clientX, y: tch.clientY }
|
||||
longPressed.current = false
|
||||
cancelPress()
|
||||
pressTimer.current = window.setTimeout(() => {
|
||||
pressTimer.current = null
|
||||
longPressed.current = true
|
||||
onLongPress(pressStart.current!.x, pressStart.current!.y)
|
||||
}, 500)
|
||||
}
|
||||
const onTouchMove = (e: ReactTouchEvent) => {
|
||||
const tch = e.touches[0]
|
||||
if (!tch || !pressStart.current) return
|
||||
if (Math.abs(tch.clientX - pressStart.current.x) > 10 || Math.abs(tch.clientY - pressStart.current.y) > 10) {
|
||||
cancelPress()
|
||||
}
|
||||
}
|
||||
const onTouchEnd = (e: ReactTouchEvent) => {
|
||||
cancelPress()
|
||||
if (longPressed.current) {
|
||||
// Synthetischen Klick verhindern (sonst würde umgewurzelt / Menü sofort
|
||||
// wieder geschlossen).
|
||||
e.preventDefault()
|
||||
longPressed.current = false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={isRoot ? 'pedigree-card pedigree-card--root' : 'pedigree-card'}
|
||||
onClick={isRoot ? undefined : onOpen}
|
||||
onContextMenu={onContextMenu}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onTouchCancel={cancelPress}
|
||||
role={isRoot ? undefined : 'button'}
|
||||
title={isRoot ? undefined : t.tapHint}
|
||||
>
|
||||
|
||||
@@ -415,6 +415,7 @@ export const de = {
|
||||
/** Mini-Legende unter dem Baum (STAMMBAUM-EXPAND). */
|
||||
hintName: 'Namenslink: Tierakte öffnen',
|
||||
hintExpand: '+: weitere Vorfahren nachladen',
|
||||
hintContext: 'Rechtsklick (oder Karte lange gedrückt halten): ID kopieren / Fehler melden',
|
||||
hintSiblings: '⟲: Eltern sind Geschwister — gemeinsame Vorfahren siehe Vaterlinie',
|
||||
/** Verweis-Knoten bei Geschwister-Verpaarung (Vater & Mutter aus demselben Wurf). */
|
||||
siblingPairing: {
|
||||
|
||||
Reference in New Issue
Block a user