mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-14 08:30:50 +00:00
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>
This commit is contained in:
1 parent
0d08b0f148
commit
6faaefebee
9 files changed
+336
-33
No files matched your search
@@ -252,8 +252,15 @@
|
||||
left: 0; right: 0; bottom: 0; width: auto;
|
||||
max-height: 78dvh; padding: 10px 18px calc(18px + env(safe-area-inset-bottom));
|
||||
border-radius: 26px 26px 0 0; border-bottom: 0;
|
||||
animation: pc-sheet 400ms var(--pz-ease-drawer) both;
|
||||
/* Pas de fill-mode « both » : une fois l'entrée finie, le glisser (style inline) reprend la main sur transform */
|
||||
animation: pc-sheet 400ms var(--pz-ease-drawer) backwards;
|
||||
transition: transform 240ms var(--pz-ease-drawer);
|
||||
}
|
||||
/* Pendant le glisser, la feuille suit le doigt sans transition */
|
||||
.person-card[data-dragging] { transition: none; }
|
||||
/* La poignée et l'en-tête portent le geste : le navigateur ne doit pas le prendre pour un défilement (les champs gardent le leur) */
|
||||
.person-card .pc-grab, .person-card .pc-close, .person-card .pc-head { touch-action: none; }
|
||||
.person-card .pc-head :is(input, textarea, select, label, [contenteditable]) { touch-action: auto; }
|
||||
.pc-grab { display: block; width: 38px; height: 4px; margin: 0 auto 2px; border-radius: 4px; background: var(--pz-line); flex: none; }
|
||||
.pc-close { top: 10px; right: 10px; }
|
||||
.pc-photo { width: 68px; height: 68px; }
|
||||
@@ -263,6 +270,7 @@
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.person-card { animation: pz-fade 200ms ease both; }
|
||||
.person-card { transition-duration: 1ms; }
|
||||
}
|
||||
|
||||
/* Prénom inconnu (issue Anaïs, 18/10) : libellé en italique, case à cocher sous les champs de nom */
|
||||
|
||||
@@ -10,6 +10,7 @@ import GeoMediaThumbnail from './GeoMediaThumbnail'
|
||||
import ImmichFacePanel from './ImmichFacePanel.jsx'
|
||||
import './PersonCard.css'
|
||||
import PzBusy from './PzBusy.jsx'
|
||||
import { useSheetDrag, useSheetMode } from '../hooks/useSheetDrag'
|
||||
import PortraitCropModal from './PortraitCropModal.jsx'
|
||||
import { imagePlacement } from '../utils/portraitCrop.js'
|
||||
import { t, dateLocale } from '../i18n/index.js'
|
||||
@@ -143,6 +144,9 @@ const PersonCard = ({
|
||||
const [openedFields, setOpenedFields] = useState([])
|
||||
const [focusField, setFocusField] = useState(null)
|
||||
const bodyRef = useRef(null)
|
||||
// Feuille du bas (téléphone) : on la ferme en la glissant par la poignée ou l'en-tête, comme la croix
|
||||
const sheetMode = useSheetMode()
|
||||
const drag = useSheetDrag({ enabled: sheetMode, zone: '.pc-grab, .pc-head, .pc-close', onClose })
|
||||
|
||||
// Reset edits quand la personne change
|
||||
useEffect(() => {
|
||||
@@ -634,7 +638,14 @@ const PersonCard = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`person-card ${editMode ? 'person-card--edit-mode' : ''}`} role="dialog" aria-label={t('Fiche de {name}', { name: fullNameLabel(person) })}>
|
||||
<div
|
||||
className={`person-card ${editMode ? 'person-card--edit-mode' : ''}`}
|
||||
role="dialog"
|
||||
aria-label={t('Fiche de {name}', { name: fullNameLabel(person) })}
|
||||
style={drag.style}
|
||||
data-dragging={drag.dragging || undefined}
|
||||
{...drag.bind}
|
||||
>
|
||||
<span className="pc-grab" aria-hidden="true" />
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pc-close" onClick={onClose} aria-label={t('Fermer la fiche')}>
|
||||
<X size={18} strokeWidth={2} />
|
||||
|
||||
@@ -9,6 +9,7 @@ import TextOptions from '../annotations/TextOptions'
|
||||
import StickerPicker from '../annotations/StickerPicker'
|
||||
import { toJourneyRole, toOpaqueTreeId, trackAppEvent } from '../../utils/analytics'
|
||||
import { t } from '../../i18n/index.js'
|
||||
import { PHONE_QUERY } from '../../utils/sheetDrag'
|
||||
import './ContextualNavbar.css'
|
||||
|
||||
const ANNOT_TOOLS = [
|
||||
@@ -27,8 +28,7 @@ const ANNOT_MENU = [
|
||||
{ id: 'photo', label: t('Poser une photo'), Icon: Image },
|
||||
]
|
||||
|
||||
// Téléphone (ou pointeur grossier) : le « + » et le compte s'ouvrent en feuille, sinon en popover
|
||||
const PHONE_QUERY = '(max-width: 767px), (pointer: coarse)'
|
||||
// Téléphone (ou pointeur grossier) : le « + » et le compte s'ouvrent en feuille, sinon en popover (PHONE_QUERY, partagé avec la fiche)
|
||||
const ICON = { size: 18, strokeWidth: 2, 'aria-hidden': true }
|
||||
|
||||
function ContextualNavbar({
|
||||
|
||||
@@ -44,8 +44,10 @@
|
||||
box-shadow: 0 -12px 40px rgba(42, 38, 34, 0.16);
|
||||
/* Pas de fill-mode : une fois l'entrée finie, le glisser (style inline) reprend la main */
|
||||
animation: navmSheetIn 320ms var(--pz-ease-drawer);
|
||||
transition: transform 240ms var(--pz-ease-drawer);
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.navm-sheet[data-dragging] { transition: none; }
|
||||
@keyframes navmSheetIn {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
@@ -155,4 +157,5 @@
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.navm-pop, .navm-sheet, .navm-scrim { animation-duration: 1ms; }
|
||||
.navm-sheet { transition-duration: 1ms; }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useSheetDrag } from '../../hooks/useSheetDrag'
|
||||
import './NavMenu.css'
|
||||
|
||||
/**
|
||||
@@ -9,8 +10,8 @@ import './NavMenu.css'
|
||||
*/
|
||||
export function NavMenu({ phone = false, label, onClose, triggerRef, children }) {
|
||||
const ref = useRef(null)
|
||||
const dragStart = useRef(null)
|
||||
const [dragY, setDragY] = useState(0)
|
||||
// Le geste part de la poignée seulement (le contenu de la feuille défile et reste intact)
|
||||
const drag = useSheetDrag({ enabled: phone, zone: '.navm-handle', onClose, closes: ({ offset }) => offset > 70 })
|
||||
|
||||
useEffect(() => {
|
||||
const node = ref.current
|
||||
@@ -21,35 +22,20 @@ export function NavMenu({ phone = false, label, onClose, triggerRef, children })
|
||||
}
|
||||
}, [triggerRef])
|
||||
|
||||
const onHandleDown = (event) => {
|
||||
dragStart.current = event.clientY
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId)
|
||||
}
|
||||
const onHandleMove = (event) => {
|
||||
if (dragStart.current === null) return
|
||||
setDragY(Math.max(0, event.clientY - dragStart.current))
|
||||
}
|
||||
const onHandleUp = (event) => {
|
||||
if (dragStart.current === null) return
|
||||
const distance = event.clientY - dragStart.current
|
||||
dragStart.current = null
|
||||
if (distance > 70) onClose()
|
||||
else setDragY(0)
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
return createPortal(
|
||||
<>
|
||||
<div className="navm-scrim" onClick={onClose} aria-hidden="true" />
|
||||
<div ref={ref} className="navm-sheet" role="dialog" aria-label={label} style={dragY ? { transform: `translateY(${dragY}px)` } : undefined}>
|
||||
<div
|
||||
className="navm-handle"
|
||||
onPointerDown={onHandleDown}
|
||||
onPointerMove={onHandleMove}
|
||||
onPointerUp={onHandleUp}
|
||||
onPointerCancel={() => { dragStart.current = null; setDragY(0) }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className="navm-sheet"
|
||||
role="dialog"
|
||||
aria-label={label}
|
||||
style={drag.style}
|
||||
data-dragging={drag.dragging || undefined}
|
||||
{...drag.bind}
|
||||
>
|
||||
<div className="navm-handle" aria-hidden="true">
|
||||
<span />
|
||||
</div>
|
||||
{children}
|
||||
|
||||
Reference in new issue
Block a user