Files
parenthese/frontend/src/utils/sheetDrag.js
T
IliesAllaliandClaude Haiku 5.5 6faaefebee Fiche : fermer en glissant vers le bas sur téléphone
La feuille de la fiche (PersonCard) se ferme en glissant sa poignée ou son en-tête vers le bas, comme le menu de la barre. Le geste est factorisé : logique pure dans utils/sheetDrag.js, hook dans hooks/useSheetDrag.js, utilisé aussi par NavMenu (poignée, seuil de 70 px inchangé).

- Le geste part seulement de la poignée et de l'en-tête. Jamais d'un champ de saisie, d'un libellé ni pendant une sélection de texte. Le contenu des onglets défile comme avant.
- La feuille suit le doigt (transform), ne monte jamais au-delà de sa position de repos.
- Fermeture au-delà d'un tiers de la hauteur, ou sur un lâcher rapide vers le bas. Sinon retour court.
- La fermeture passe par le même chemin que la croix (enregistrement automatique compris). Si l'enregistrement est refusé, la fiche revient et affiche son erreur.
- Mode feuille seulement (PHONE_QUERY et largeur de la feuille CSS). Sur ordinateur, rien ne change.
- prefers-reduced-motion respecté.

Tests : logique du geste (zones, direction, seuil, vitesse) en vitest. Recette Playwright iPhone 13 émulé, gestes tactiles CDP : fermeture par la poignée, retour après un petit glisser, lâcher rapide, défilement du contenu sans fermer, pas de geste depuis un champ, enregistrement puis fermeture, ordinateur inchangé, croix.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-19 11:06:35 +02:00

52 lines
2.5 KiB
JavaScript

// Geste « feuille » : une feuille du bas (fiche de personne, menu de la barre) se ferme en glissant vers le bas.
// Logique pure, sans React ni DOM, testée dans sheetDrag.test.js. Le hook qui l'utilise est hooks/useSheetDrag.js.
// Téléphone ou pointeur grossier : même règle pour la barre et pour la fiche
export const PHONE_QUERY = '(max-width: 767px), (pointer: coarse)'
// Même seuil que la feuille CSS de la fiche (PersonCard.css) : au-delà, la fiche est un panneau flottant
export const SHEET_WIDTH_QUERY = '(max-width: 768px)'
// Distance avant de trancher entre glisser vertical, horizontal et tap
export const DIRECTION_LOCK_PX = 8
// Lâcher rapide vers le bas : vitesse en px/ms, et course minimale pour qu'il compte
export const FLICK_VELOCITY = 0.6
export const FLICK_MIN_OFFSET = 24
// Deux points plus espacés que ça ne donnent pas une vitesse fiable
const VELOCITY_WINDOW_MS = 120
// Champs de saisie et libellés : jamais le point de départ d'un glisser
export const NO_DRAG_SELECTOR = 'input, textarea, select, label, [contenteditable]:not([contenteditable="false"]), [data-sheet-no-drag]'
/**
* Le glisser peut-il partir de cette cible ?
* `zoneSelector` : les seuls endroits de la feuille qui déclenchent le geste (poignée, en-tête).
* `selectionText` : texte sélectionné au moment de l'appui, auquel cas rien ne bouge.
*/
export function canStartSheetDrag(target, zoneSelector, selectionText = '') {
if (!target || typeof target.closest !== 'function') return false
if (selectionText) return false
if (!target.closest(zoneSelector)) return false
return !target.closest(NO_DRAG_SELECTOR)
}
/** Intention du mouvement depuis l'appui : 'pending' (encore trop petit), 'down' (glisser vers le bas), 'other' (haut ou horizontal) */
export function sheetDragIntent(dx, dy) {
if (Math.abs(dx) < DIRECTION_LOCK_PX && Math.abs(dy) < DIRECTION_LOCK_PX) return 'pending'
if (dy > 0 && dy >= Math.abs(dx)) return 'down'
return 'other'
}
/** Vitesse (px/ms, positive vers le bas) entre deux points {y, t}. Zéro si les points sont trop éloignés dans le temps. */
export function velocityBetween(from, to) {
if (!from || !to) return 0
const dt = to.t - from.t
if (dt <= 0 || dt > VELOCITY_WINDOW_MS) return 0
return (to.y - from.y) / dt
}
/** Au lâcher : fermer si la course dépasse un tiers de la hauteur, ou si le lâcher est rapide vers le bas */
export function shouldCloseSheet({ offset, height, velocity = 0 }) {
if (height > 0 && offset >= height / 3) return true
return velocity >= FLICK_VELOCITY && offset >= FLICK_MIN_OFFSET
}