Skip to content

Commit 6817558

Browse files
omercnetUbuntushantur
authored
fix(mobile): tappable instance/project tab bar while session drawer is open (#459)
## Summary Fixes a mobile UX bug where, with the session list (left drawer) open on phone layout, the still-visible instance/project tab bar at the top was non-interactive. Tapping a tab did nothing — users had to close the drawer first and then switch projects/instances. After this change, tapping a tab in that bar switches the instance/project **and** the drawer auto-closes in a single gesture, matching user expectation. ## Root cause On phone (`max-width: 767px`), `InstanceShell` renders the left session sidebar via MUI's `Drawer variant="temporary"`. The drawer paper is offset down to `floatingTopPx()` so the instance tab bar remains visually visible above it. However, MUI's `Modal` Backdrop is `position: fixed; inset: 0` and covers the entire viewport — including the area over the tab bar. The backdrop is styled `backgroundColor: transparent` (so it's invisible) but it still captures pointer events at `z-index: 60`. Taps over the tab bar hit the transparent backdrop → MUI calls `onClose` (closing the drawer) but the tab's click handler never fires. ## Implementation 1. **`packages/ui/src/components/instance/instance-shell2.tsx`** — Constrain the MUI Drawer Backdrop via `sx` overrides on both the left (session list) and right drawers so the backdrop is bound to the drawer paper's vertical range (`top: floatingTopPx(); height: floatingHeight();`) instead of fullscreen. Taps over the tab bar now reach the tab buttons. 2. **`packages/ui/src/styles/panels/tabs.css`** — Lift `.tab-bar-instance` to `position: relative; z-index: 70` so it stacks deterministically above the drawer (z-index 60) across browsers as defense-in-depth. 3. **`packages/ui/src/components/instance/shell/useDrawerChrome.ts` + `instance-shell2.tsx`** — Expose `closeFloatingDrawersIfAny` from `useDrawerChrome` and call it from an `InstanceShell` effect that fires when `props.isActiveInstance` flips `true → false`. This closes any open floating drawer on the instance the user just switched away from, so its previously-open state doesn't bleed back when the user returns to that tab later. Tablet (>=768px) and desktop (>=1280px) layouts use pinned drawers (no temporary modal), so the backdrop constraint and z-index lift are inert there. The fix applies symmetrically to the right drawer on phone. ## Verification - `tsc --noEmit` clean. - `vite build` clean. - Manual mobile-emulation checklist in the task file (`tasks/done/058-mobile-session-list-blocks-tab-switch.md`) and SUMMARY in `evidences/058-mobile-session-list-blocks-tab-switch/`. ## Reviewer manual check (phone viewport <=767px) - Open session list drawer. Tap a different instance tab → it activates AND drawer closes. - Tap "+" while drawer open → folder picker opens, drawer closes. - Tap Settings / Notifications / Remote while drawer open → action fires, drawer closes. - Same checks against the right drawer. - Tap area below tab bar but outside drawer paper → drawer still closes (existing backdrop dismissal preserved). - Resize to tablet and desktop widths → pinned drawers unaffected. - No visual regression in light or dark theme. --------- Co-authored-by: Ubuntu <omer@Omer.dn3uxh3znnmu5eefnjnut0i1af.tlvx.internal.cloudapp.net> Co-authored-by: Shantur Rathore <i@shantur.com>
1 parent 311b60b commit 6817558

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

packages/ui/src/components/instance/instance-shell2.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
194194
unpinRight: unpinRightDrawer,
195195
closeLeft: closeLeftDrawer,
196196
closeRight: closeRightDrawer,
197+
closeFloatingDrawersIfAny,
197198
leftAppBarButtonLabel,
198199
rightAppBarButtonLabel,
199200
leftAppBarButtonIcon,
@@ -202,6 +203,45 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
202203
handleRightAppBarButtonClick,
203204
} = drawerChrome
204205

206+
// When the user switches away from this instance (e.g., taps a different
207+
// instance/project tab while a floating drawer is open on phone), close any
208+
// open floating drawers so the previous instance's drawer doesn't remain
209+
// visually or interactively open when its tab regains focus later.
210+
let wasActiveInstance = Boolean(props.isActiveInstance)
211+
createEffect(() => {
212+
const isActive = Boolean(props.isActiveInstance)
213+
if (wasActiveInstance && !isActive) {
214+
closeFloatingDrawersIfAny()
215+
}
216+
wasActiveInstance = isActive
217+
})
218+
219+
onMount(() => {
220+
if (typeof document === "undefined") return
221+
222+
const handleFloatingDrawerPointerDown = (event: PointerEvent) => {
223+
if (!props.isActiveInstance) return
224+
225+
const hasFloatingDrawerOpen = (!leftPinned() && leftOpen()) || (!rightPinned() && rightOpen())
226+
if (!hasFloatingDrawerOpen) return
227+
228+
const target = event.target
229+
if (!(target instanceof Node)) return
230+
231+
const leftContent = leftDrawerContentEl()
232+
const rightContent = rightDrawerContentEl()
233+
const leftPaper = leftContent?.closest(".MuiDrawer-paper")
234+
const rightPaper = rightContent?.closest(".MuiDrawer-paper")
235+
if (leftPaper?.contains(target) || rightPaper?.contains(target)) return
236+
237+
if (!leftPinned() && leftOpen()) setLeftOpen(false)
238+
if (!rightPinned() && rightOpen()) setRightOpen(false)
239+
}
240+
241+
document.addEventListener("pointerdown", handleFloatingDrawerPointerDown, true)
242+
onCleanup(() => document.removeEventListener("pointerdown", handleFloatingDrawerPointerDown, true))
243+
})
244+
205245
createEffect(() => {
206246
const instanceId = props.instance.id
207247
loadBackgroundProcesses(instanceId).catch((error) => {
@@ -607,7 +647,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
607647
ModalProps={modalProps}
608648
sx={{
609649
zIndex: 60,
650+
// The tab bar sits outside the floating drawer. Let its controls
651+
// receive the gesture; click-away handling above still closes the
652+
// drawer when the target is not inside the drawer content.
653+
pointerEvents: "none",
610654
"& .MuiDrawer-paper": {
655+
pointerEvents: "auto",
611656
width: isPhoneLayout() ? "100vw" : `${sessionSidebarWidth()}px`,
612657
boxSizing: "border-box",
613658
borderInlineEnd: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
@@ -620,8 +665,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
620665
height: floatingHeight(),
621666
},
622667

668+
// Keep backdrop dismissal for the area below the tab bar without
669+
// covering the tab bar itself.
623670
"& .MuiBackdrop-root": {
671+
pointerEvents: "auto",
624672
backgroundColor: "transparent",
673+
top: floatingTopPx(),
674+
height: floatingHeight(),
625675
},
626676
}}
627677
>
@@ -723,7 +773,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
723773
ModalProps={modalProps}
724774
sx={{
725775
zIndex: 60,
776+
// See the matching override on the left drawer for rationale.
777+
pointerEvents: "none",
726778
"& .MuiDrawer-paper": {
779+
pointerEvents: "auto",
727780
width: isPhoneLayout() ? "100vw" : `${rightDrawerWidth()}px`,
728781
boxSizing: "border-box",
729782
borderInlineStart: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
@@ -736,7 +789,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
736789
height: floatingHeight(),
737790
},
738791
"& .MuiBackdrop-root": {
792+
pointerEvents: "auto",
739793
backgroundColor: "transparent",
794+
top: floatingTopPx(),
795+
height: floatingHeight(),
740796
},
741797
}}
742798
>

packages/ui/src/components/instance/shell/useDrawerChrome.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export interface DrawerChromeApi {
4444
unpinRight: () => void
4545
closeLeft: () => void
4646
closeRight: () => void
47+
closeFloatingDrawersIfAny: () => boolean
4748
leftAppBarButtonLabel: Accessor<string>
4849
rightAppBarButtonLabel: Accessor<string>
4950
leftAppBarButtonIcon: Accessor<JSX.Element>
@@ -250,6 +251,7 @@ export function useDrawerChrome(options: UseDrawerChromeOptions): DrawerChromeAp
250251
unpinRight,
251252
closeLeft,
252253
closeRight,
254+
closeFloatingDrawersIfAny,
253255
leftAppBarButtonLabel,
254256
rightAppBarButtonLabel,
255257
leftAppBarButtonIcon,

0 commit comments

Comments
 (0)