feat: navigation swipe mobile horizontale
All checks were successful
Deploy / Deploy to Production (push) Successful in 21s
All checks were successful
Deploy / Deploy to Production (push) Successful in 21s
- App.svelte : swipe gauche/droite → même comportement que les touches clavier (navigation entre slides) - Play.svelte : stopImmediatePropagation sur touchend, blocage du scroll vertical (touchmove) - Portfolio.svelte : migration du touch vertical (composable) vers horizontal — navigation entre projets, slide voisine aux bords, blocage scroll vertical, debounce 650ms anti-spam Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
58c31ea391
commit
e41a730b4d
3 changed files with 113 additions and 2 deletions
|
|
@ -79,9 +79,45 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener('keydown', handleKeydown)
|
window.addEventListener('keydown', handleKeydown)
|
||||||
|
|
||||||
|
// Swipe gauche/droite → même comportement que les touches clavier
|
||||||
|
const TOUCH_THRESHOLD = 50
|
||||||
|
let touchStartX = 0
|
||||||
|
let touchStartY = 0
|
||||||
|
|
||||||
|
const handleTouchStart = (e) => {
|
||||||
|
touchStartX = e.touches[0].clientX
|
||||||
|
touchStartY = e.touches[0].clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTouchEnd = (e) => {
|
||||||
|
const deltaX = touchStartX - e.changedTouches[0].clientX
|
||||||
|
const deltaY = touchStartY - e.changedTouches[0].clientY
|
||||||
|
if (Math.abs(deltaX) < TOUCH_THRESHOLD) return
|
||||||
|
if (Math.abs(deltaY) > Math.abs(deltaX)) return
|
||||||
|
|
||||||
|
const activePath = slides.active?.path ?? ''
|
||||||
|
const currentPath = window.location.pathname.replace(/^\/en/, '') || '/'
|
||||||
|
const isSubPage = activePath && currentPath.startsWith(activePath + '/')
|
||||||
|
if (isSubPage) return
|
||||||
|
|
||||||
|
if (deltaX > 0) {
|
||||||
|
const next = slides.all[slides.activeIndex + 1]
|
||||||
|
if (next) slideTo(next.path)
|
||||||
|
} else {
|
||||||
|
const prev = slides.all[slides.activeIndex - 1]
|
||||||
|
if (prev) slideTo(prev.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('touchstart', handleTouchStart, { passive: true })
|
||||||
|
window.addEventListener('touchend', handleTouchEnd, { passive: true })
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
window.removeEventListener('keydown', handleKeydown)
|
window.removeEventListener('keydown', handleKeydown)
|
||||||
|
window.removeEventListener('touchstart', handleTouchStart)
|
||||||
|
window.removeEventListener('touchend', handleTouchEnd)
|
||||||
clearTimeout(resizeTimer)
|
clearTimeout(resizeTimer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,8 @@
|
||||||
|
|
||||||
function onTouchEnd(e) {
|
function onTouchEnd(e) {
|
||||||
if (!isActive) return
|
if (!isActive) return
|
||||||
|
// Toujours intercepter : Play gère lui-même toute la navigation touch
|
||||||
|
e.stopImmediatePropagation()
|
||||||
const deltaX = touchStartX - e.changedTouches[0].clientX
|
const deltaX = touchStartX - e.changedTouches[0].clientX
|
||||||
if (Math.abs(deltaX) < TOUCH_THRESHOLD) return
|
if (Math.abs(deltaX) < TOUCH_THRESHOLD) return
|
||||||
|
|
||||||
|
|
@ -137,14 +139,22 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Bloque le scroll vertical natif (pull-to-refresh, bounce) quand la slide est active
|
||||||
|
function onTouchMove(e) {
|
||||||
|
if (!isActive) return
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', onKeyDown, { capture: true })
|
window.addEventListener('keydown', onKeyDown, { capture: true })
|
||||||
window.addEventListener('touchstart', onTouchStart, { capture: true, passive: true })
|
window.addEventListener('touchstart', onTouchStart, { capture: true, passive: true })
|
||||||
window.addEventListener('touchend', onTouchEnd, { capture: true, passive: true })
|
window.addEventListener('touchend', onTouchEnd, { capture: true, passive: true })
|
||||||
|
window.addEventListener('touchmove', onTouchMove, { passive: false })
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', onKeyDown, { capture: true })
|
window.removeEventListener('keydown', onKeyDown, { capture: true })
|
||||||
window.removeEventListener('touchstart', onTouchStart, { capture: true })
|
window.removeEventListener('touchstart', onTouchStart, { capture: true })
|
||||||
window.removeEventListener('touchend', onTouchEnd, { capture: true })
|
window.removeEventListener('touchend', onTouchEnd, { capture: true })
|
||||||
|
window.removeEventListener('touchmove', onTouchMove)
|
||||||
clearTimeout(t1)
|
clearTimeout(t1)
|
||||||
clearTimeout(t2)
|
clearTimeout(t2)
|
||||||
clearTimeout(t3)
|
clearTimeout(t3)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { slides } from '@state/slides.svelte'
|
import { slides } from '@state/slides.svelte'
|
||||||
|
import { slideTo } from '@router'
|
||||||
import { createScrollNav } from '@composables/useScrollNav.svelte.js'
|
import { createScrollNav } from '@composables/useScrollNav.svelte.js'
|
||||||
import GalleryAnimation from '@components/ui/GalleryAnimation.svelte'
|
import GalleryAnimation from '@components/ui/GalleryAnimation.svelte'
|
||||||
import ResponsivePicture from '@components/ui/ResponsivePicture.svelte'
|
import ResponsivePicture from '@components/ui/ResponsivePicture.svelte'
|
||||||
|
|
@ -51,15 +52,79 @@
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Touch horizontal (mobile) ---
|
||||||
|
// Même pattern que Play : navigation entre projets, puis slide voisine aux bords.
|
||||||
|
// Le scroll vertical ne fait rien (touchmove bloqué).
|
||||||
|
const TOUCH_THRESHOLD = 50
|
||||||
|
const TOUCH_LOCK_MS = 650
|
||||||
|
let touchStartX = 0
|
||||||
|
let touchStartY = 0
|
||||||
|
let touchLocked = false
|
||||||
|
let touchLockTimer = null
|
||||||
|
|
||||||
|
function onTouchStart(e) {
|
||||||
|
if (!isActive) return
|
||||||
|
touchStartX = e.touches[0].clientX
|
||||||
|
touchStartY = e.touches[0].clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchEnd(e) {
|
||||||
|
if (!isActive) return
|
||||||
|
// Toujours intercepter : Portfolio gère lui-même toute la navigation touch
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
if (touchLocked) return
|
||||||
|
const deltaX = touchStartX - e.changedTouches[0].clientX
|
||||||
|
const deltaY = touchStartY - e.changedTouches[0].clientY
|
||||||
|
if (Math.abs(deltaX) < TOUCH_THRESHOLD || Math.abs(deltaY) > Math.abs(deltaX)) return
|
||||||
|
|
||||||
|
// Verrouillage anti-spam (même durée que le composable useScrollNav)
|
||||||
|
touchLocked = true
|
||||||
|
clearTimeout(touchLockTimer)
|
||||||
|
touchLockTimer = setTimeout(() => { touchLocked = false }, TOUCH_LOCK_MS)
|
||||||
|
|
||||||
|
if (deltaX > 0) {
|
||||||
|
// Swipe gauche → projet suivant, ou slide suivante si fin de liste
|
||||||
|
if (currentIndex < projects.length - 1) {
|
||||||
|
currentIndex++
|
||||||
|
setAnchor(currentIndex)
|
||||||
|
} else {
|
||||||
|
const next = slides.all[slides.activeIndex + 1]
|
||||||
|
if (next) slideTo(next.path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Swipe droite → projet précédent, ou slide précédente si début de liste
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
currentIndex--
|
||||||
|
setAnchor(currentIndex)
|
||||||
|
} else {
|
||||||
|
const prev = slides.all[slides.activeIndex - 1]
|
||||||
|
if (prev) slideTo(prev.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bloque le scroll vertical natif quand la slide est active
|
||||||
|
function onTouchMove(e) {
|
||||||
|
if (!isActive) return
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
// --- onMount ---
|
// --- onMount ---
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
sectionEl?.addEventListener('wheel', nav.onWheel, { passive: false })
|
sectionEl?.addEventListener('wheel', nav.onWheel, { passive: false })
|
||||||
window.addEventListener('keydown', nav.onKeyDown)
|
window.addEventListener('keydown', nav.onKeyDown)
|
||||||
|
window.addEventListener('touchstart', onTouchStart, { capture: true, passive: true })
|
||||||
|
window.addEventListener('touchend', onTouchEnd, { capture: true, passive: true })
|
||||||
|
window.addEventListener('touchmove', onTouchMove, { passive: false })
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sectionEl?.removeEventListener('wheel', nav.onWheel)
|
sectionEl?.removeEventListener('wheel', nav.onWheel)
|
||||||
window.removeEventListener('keydown', nav.onKeyDown)
|
window.removeEventListener('keydown', nav.onKeyDown)
|
||||||
|
window.removeEventListener('touchstart', onTouchStart, { capture: true })
|
||||||
|
window.removeEventListener('touchend', onTouchEnd, { capture: true })
|
||||||
|
window.removeEventListener('touchmove', onTouchMove)
|
||||||
nav.destroy()
|
nav.destroy()
|
||||||
|
clearTimeout(touchLockTimer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -72,6 +137,8 @@
|
||||||
wasActive = true
|
wasActive = true
|
||||||
} else if (wasActive) {
|
} else if (wasActive) {
|
||||||
nav.reset()
|
nav.reset()
|
||||||
|
touchLocked = false
|
||||||
|
clearTimeout(touchLockTimer)
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
clearAnchor()
|
clearAnchor()
|
||||||
wasActive = false
|
wasActive = false
|
||||||
|
|
@ -87,8 +154,6 @@
|
||||||
class="portfolio golden-grid"
|
class="portfolio golden-grid"
|
||||||
style={backgroundImage ? `--background-image: url('${backgroundImage}')` : ''}
|
style={backgroundImage ? `--background-image: url('${backgroundImage}')` : ''}
|
||||||
bind:this={sectionEl}
|
bind:this={sectionEl}
|
||||||
ontouchstart={nav.onTouchStart}
|
|
||||||
ontouchend={nav.onTouchEnd}
|
|
||||||
aria-label="Portfolio"
|
aria-label="Portfolio"
|
||||||
>
|
>
|
||||||
<!-- Decorative vertical lines -->
|
<!-- Decorative vertical lines -->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue