Phones: a panel announces it closed; the legend's resize drag owns its gesture
- MapControlMenu dispatches 'dxmaps:panel-close' (composed, bubbling), the twin of 'dxmaps:panel-open', when a panel closes by ✕, an outside tap or Esc, or is unmounted while open. A host can then hide its own chrome while a panel is up (DX Life's top scroll strip sat over the Share panel's ✕). - The mobile legend's resize drag scrolled the page. Only the strip and the title rows carried touch-action:none, so a drag that began elsewhere in the 44px grab zone let the browser start panning before touchmove could cancel it, and its later touchmoves were not cancelable. touchstart in the grab zone now claims the gesture (preventDefault), and a touch that never becomes a drag is clicked on release (inputs keep their native tap). The height is written once per frame (rAF), and the last frame lands before the snap.
This commit is contained in:
parent
a3225e072f
commit
c535c0f031
2 changed files with 44 additions and 9 deletions
|
|
@ -602,7 +602,12 @@ const SNAP_MIN_PX = 24
|
|||
// the .dragging class flag).
|
||||
const sheetRef = ref(null)
|
||||
const isDragging = ref(false)
|
||||
const drag = { pending: null, active: false, startY: 0, startH: 0, startState: '', containerH: 0, maxH: 0, lastH: 0 }
|
||||
const drag = { pending: null, active: false, startY: 0, startH: 0, startState: '', containerH: 0, maxH: 0, lastH: 0, tapTarget: null }
|
||||
// One height write per frame while dragging (the latest finger position wins), never one per touchmove.
|
||||
let _dragRaf = 0
|
||||
function flushDragHeight() { _dragRaf = 0; uiStore.setLegendHeight(`${drag.lastH}px`) }
|
||||
// A control the reader may need the browser's own handling for (focus, a keyboard): never taken over.
|
||||
const NATIVE_TAP = /^(INPUT|SELECT|TEXTAREA)$/
|
||||
let lastDragEndAt = 0
|
||||
// A drag that starts on a button (caret / close X / strip) can still be followed
|
||||
// by a synthesized click on release in some browsers — ignore it.
|
||||
|
|
@ -724,6 +729,21 @@ function onSheetTouchStart(event) {
|
|||
if (!t || !sheet) return
|
||||
const inGrabZone = (t.clientY - sheet.getBoundingClientRect().top) <= GRAB_ZONE_PX
|
||||
if (!inGrabZone) return
|
||||
// ★ THE HANDLE OWNS THE GESTURE FROM THE FIRST TOUCH (2026-10-02, the Driver's phone: a resize drag scrolled the
|
||||
// page). Only the strip and the title rows carry touch-action:none, so a drag that began anywhere else in the
|
||||
// 44px grab zone let the browser start panning before our touchmove could cancel it, and from then on its
|
||||
// touchmoves were not cancelable: the page scrolled while the sheet resized. preventDefault on touchstart
|
||||
// claims the gesture for the whole grab zone, whatever touch-action says. It also swallows the browser's own
|
||||
// click, so a touch that never becomes a drag is turned back into one on release (tapTarget.click()), and
|
||||
// inputs keep their native tap.
|
||||
// The element to click on release: the nearest HTML element (a tap on a button's SVG icon lands on a <path>,
|
||||
// which has no click()), so the button's own @click runs.
|
||||
let target = (event.composedPath && event.composedPath()[0]) || event.target
|
||||
while (target && typeof target.click !== 'function') target = target.parentNode
|
||||
if (!(target && NATIVE_TAP.test(target.tagName || '')) && event.cancelable) {
|
||||
event.preventDefault()
|
||||
drag.tapTarget = target
|
||||
} else drag.tapTarget = null
|
||||
drag.pending = 'grab'
|
||||
drag.active = false
|
||||
drag.startY = t.clientY
|
||||
|
|
@ -748,15 +768,25 @@ function onSheetTouchMove(event) {
|
|||
drag.active = true
|
||||
isDragging.value = true // .dragging → transition:none, same flush as the height
|
||||
}
|
||||
event.preventDefault() // no page/content scroll while resizing
|
||||
const h = Math.round(Math.min(drag.maxH, Math.max(COLLAPSED_PX, drag.startH + dy)))
|
||||
drag.lastH = h
|
||||
uiStore.setLegendHeight(`${h}px`)
|
||||
if (event.cancelable) event.preventDefault() // no page/content scroll while resizing
|
||||
drag.lastH = Math.round(Math.min(drag.maxH, Math.max(COLLAPSED_PX, drag.startH + dy)))
|
||||
if (!_dragRaf) {
|
||||
if (typeof requestAnimationFrame === 'function') _dragRaf = requestAnimationFrame(flushDragHeight)
|
||||
else flushDragHeight()
|
||||
}
|
||||
}
|
||||
|
||||
function onSheetTouchEnd() {
|
||||
function onSheetTouchEnd(event) {
|
||||
drag.pending = null
|
||||
if (!drag.active) return // a tap — the @click handlers own it
|
||||
const tapTarget = drag.tapTarget
|
||||
drag.tapTarget = null
|
||||
if (!drag.active) {
|
||||
// A tap. The @click handlers own it; if touchstart took the browser's click, hand it back (not on a cancel).
|
||||
if (tapTarget && !(event && event.type === 'touchcancel') && typeof tapTarget.click === 'function') tapTarget.click()
|
||||
return
|
||||
}
|
||||
// The last frame's height lands before the snap reads it.
|
||||
if (_dragRaf) { if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(_dragRaf); flushDragHeight() }
|
||||
drag.active = false
|
||||
isDragging.value = false // transition back on BEFORE the snap so it animates
|
||||
lastDragEndAt = Date.now()
|
||||
|
|
|
|||
|
|
@ -132,9 +132,13 @@ function onOtherOpened(e) { if (props.open && e.detail !== el.value) emit('close
|
|||
// it sat over the Share panel's ✕ (measured 2026-10-02, cities-with-internet). So every panel says it opened, as
|
||||
// a composed event that leaves the shadow root, and MapInfo puts its pointer away (library/components/info/MapInfo.vue).
|
||||
const PANEL_OPEN_EVENT = 'dxmaps:panel-open'
|
||||
// ...and its twin when it closes (✕, an outside tap, Esc, or unmounted while open), so a HOST can hide its own chrome
|
||||
// while a panel is up: DX Life's top scroll strip sat over the Share panel's ✕.
|
||||
const PANEL_CLOSE_EVENT = 'dxmaps:panel-close'
|
||||
const announceClose = () => { try { host.value?.dispatchEvent(new CustomEvent(PANEL_CLOSE_EVENT, { bubbles: true, composed: true })) } catch (_) {} }
|
||||
|
||||
watch(() => props.open, async (open) => {
|
||||
if (!open) return
|
||||
watch(() => props.open, async (open, was) => {
|
||||
if (!open) { if (was) announceClose(); return }
|
||||
await nextTick()
|
||||
place()
|
||||
try { host.value?.dispatchEvent(new CustomEvent(OPEN_EVENT, { detail: el.value })) } catch (_) {}
|
||||
|
|
@ -166,6 +170,7 @@ onMounted(() => {
|
|||
document.addEventListener('keydown', onKeyDown)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (props.open) announceClose()
|
||||
host.value?.removeEventListener(OPEN_EVENT, onOtherOpened)
|
||||
window.removeEventListener('resize', onResize)
|
||||
document.removeEventListener('click', onClickOutside, true)
|
||||
|
|
|
|||
Loading…
Reference in a new issue