From 06455f24014ad0200e67ba227fc5d9aabe036815 Mon Sep 17 00:00:00 2001 From: Max Ronzhin Date: Mon, 3 Aug 2026 23:59:26 +0300 Subject: [PATCH] =?UTF-8?q?fix(room):=20=D1=88=D1=82=D0=BE=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D1=8C=20=D1=81=D0=B2=D0=B0=D0=B9=D0=BF=D0=BE=D0=BC?= =?UTF-8?q?=20=D0=B2=D0=BD=D0=B8=D0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Обработчики висели только на ручке `.room-sheet-handle` (40×4px) — попасть в неё пальцем практически невозможно, и палец почти всегда приземлялся на панель, где обработчиков не было вовсе. Свайп теперь закрывает шторку при жесте по любому месту панели, но только когда содержимое проскроллено в самый верх — иначе палец должен листать список устройств, как в любом стандартном bottom sheet. --- .../components/room/DeviceSettingsDialog.tsx | 39 ++++++++++++------- frontend/src/styles/room.css | 15 +++++-- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/room/DeviceSettingsDialog.tsx b/frontend/src/components/room/DeviceSettingsDialog.tsx index 9760cd4..d9e7c8e 100644 --- a/frontend/src/components/room/DeviceSettingsDialog.tsx +++ b/frontend/src/components/room/DeviceSettingsDialog.tsx @@ -11,7 +11,7 @@ interface DeviceSettingsDialogProps extends StageViewProps { onClose: () => void } -/** Свайп ручки шторки вниз дальше этого порога (px) закрывает панель, меньше — она возвращается на место. */ +/** Свайп панели вниз дальше этого порога (px) закрывает шторку, меньше — она возвращается на место. */ const SHEET_DISMISS_THRESHOLD_PX = 80 /** Человекочитаемая подпись пункта списка устройств — `label` пуст, пока нет разрешения на медиа. */ @@ -30,7 +30,13 @@ function deviceLabel(device: MediaDeviceInfo, index: number, fallback: string): * `deviceId` при следующем подключении. * * На мобильной ширине (`useIsCompactViewport`) рендерится шторкой снизу вместо - * модалки — по клику вне, Escape (`useModalDismiss`) и свайпу вниз за ручку. + * модалки — по клику вне, Escape (`useModalDismiss`) и свайпу вниз по ЛЮБОМУ + * месту панели (не только за ручку-волосок `.room-sheet-handle` — та её не + * пережила бы: 40×4px, попасть пальцем почти нереально). Свайп срабатывает, + * только если содержимое панели проскроллено в самый верх (`scrollTop === 0` + * на touchstart, см. `dragEligibleRef`) — иначе палец должен листать список + * устройств, а не закрывать окно; решение фиксируется на весь жест, а не + * пересчитывается на каждый touchmove, как в любом стандартном bottom sheet. * Десктоп не меняется. * * Там же, и только там, первой секцией идёт «Вид» (режим показа участников и @@ -61,6 +67,12 @@ export function DeviceSettingsDialog({ useModalDismiss(onClose) const dragStartYRef = useRef(null) + // Жест начат, когда контент панели был проскроллен в самый верх — свайп по + // панели, у которой ещё есть что скроллить, должен листать содержимое, а не + // закрывать шторку. Решается ОДИН раз в touchstart и держится весь жест + // (даже если внутри него направление сменится) — так же, как в любом + // стандартном bottom sheet. + const dragEligibleRef = useRef(false) const [dragOffset, setDragOffset] = useState(0) const [isDragging, setIsDragging] = useState(false) @@ -92,18 +104,21 @@ export function DeviceSettingsDialog({ } } - function handleHandleTouchStart(e: React.TouchEvent) { + function handlePanelTouchStart(e: React.TouchEvent) { + dragEligibleRef.current = e.currentTarget.scrollTop === 0 + if (!dragEligibleRef.current) return dragStartYRef.current = e.touches[0].clientY setIsDragging(true) } - function handleHandleTouchMove(e: React.TouchEvent) { - if (dragStartYRef.current === null) return + function handlePanelTouchMove(e: React.TouchEvent) { + if (!dragEligibleRef.current || dragStartYRef.current === null) return const delta = e.touches[0].clientY - dragStartYRef.current if (delta > 0) setDragOffset(delta) } - function handleHandleTouchEnd() { + function handlePanelTouchEnd() { + if (!dragEligibleRef.current) return if (dragOffset > SHEET_DISMISS_THRESHOLD_PX) { onClose() return @@ -124,20 +139,16 @@ export function DeviceSettingsDialog({
e.stopPropagation()} + onTouchStart={isCompact ? handlePanelTouchStart : undefined} + onTouchMove={isCompact ? handlePanelTouchMove : undefined} + onTouchEnd={isCompact ? handlePanelTouchEnd : undefined} style={ isCompact && dragOffset ? { transform: `translateY(${dragOffset}px)`, transition: isDragging ? 'none' : undefined } : undefined } > - {isCompact && ( -
- )} + {isCompact &&
}

{isCompact ? 'Настройки' : 'Настройки устройств'}

diff --git a/frontend/src/styles/room.css b/frontend/src/styles/room.css index dc1543c..dc18916 100644 --- a/frontend/src/styles/room.css +++ b/frontend/src/styles/room.css @@ -803,10 +803,16 @@ video[data-lk-source='screen_share'] { object-fit: contain; background: #000; } * Тот же диалог (`DeviceSettingsDialog`), что модалка выше, — только контейнер * снизу вместо центра экрана: `.room-sheet-overlay`/`.room-sheet-panel` вместо * `.room-modal-overlay`/`.room-modal-panel`, разметка полей (`.room-field` и - * ниже) общая. Ручка `.room-sheet-handle` — свайп вниз для закрытия (JS считает - * смещение пальца и сам решает, закрывать или вернуть панель на место; - * `transition` тут — только пружина возврата, во время самого драга - * компонент подставляет инлайновый `transition: none`). + * ниже) общая. Свайп вниз для закрытия — JS на самой панели (не на ручке — + * см. докстринг `DeviceSettingsDialog`) считает смещение пальца и сам решает, + * закрывать или вернуть панель на место; `transition` тут — только пружина + * возврата, во время самого драга компонент подставляет инлайновый + * `transition: none`. `overscroll-behavior-y: contain` — чтобы при свайпе + * вниз от самого верха списка устройств iOS/Chrome не показывали заодно + * ещё и нативный эффект растяжения скролла поверх нашей анимации панели. + * Ручка `.room-sheet-handle` — теперь чисто визуальная подсказка (сама + * панель ловит жест где угодно), `touch-action: none` на ней оставлен — + * безвредно и снимает системные жесты с этой узкой полоски. */ .room-sheet-overlay { position: fixed; @@ -827,6 +833,7 @@ video[data-lk-source='screen_share'] { object-fit: contain; background: #000; } width: 100%; max-height: 80vh; overflow-y: auto; + overscroll-behavior-y: contain; transition: transform 160ms ease-out; } .room-sheet-handle {