mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
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>
This commit is contained in:
parent
311b60bfb6
commit
681755888f
2 changed files with 58 additions and 0 deletions
|
|
@ -194,6 +194,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
unpinRight: unpinRightDrawer,
|
||||
closeLeft: closeLeftDrawer,
|
||||
closeRight: closeRightDrawer,
|
||||
closeFloatingDrawersIfAny,
|
||||
leftAppBarButtonLabel,
|
||||
rightAppBarButtonLabel,
|
||||
leftAppBarButtonIcon,
|
||||
|
|
@ -202,6 +203,45 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
handleRightAppBarButtonClick,
|
||||
} = drawerChrome
|
||||
|
||||
// When the user switches away from this instance (e.g., taps a different
|
||||
// instance/project tab while a floating drawer is open on phone), close any
|
||||
// open floating drawers so the previous instance's drawer doesn't remain
|
||||
// visually or interactively open when its tab regains focus later.
|
||||
let wasActiveInstance = Boolean(props.isActiveInstance)
|
||||
createEffect(() => {
|
||||
const isActive = Boolean(props.isActiveInstance)
|
||||
if (wasActiveInstance && !isActive) {
|
||||
closeFloatingDrawersIfAny()
|
||||
}
|
||||
wasActiveInstance = isActive
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (typeof document === "undefined") return
|
||||
|
||||
const handleFloatingDrawerPointerDown = (event: PointerEvent) => {
|
||||
if (!props.isActiveInstance) return
|
||||
|
||||
const hasFloatingDrawerOpen = (!leftPinned() && leftOpen()) || (!rightPinned() && rightOpen())
|
||||
if (!hasFloatingDrawerOpen) return
|
||||
|
||||
const target = event.target
|
||||
if (!(target instanceof Node)) return
|
||||
|
||||
const leftContent = leftDrawerContentEl()
|
||||
const rightContent = rightDrawerContentEl()
|
||||
const leftPaper = leftContent?.closest(".MuiDrawer-paper")
|
||||
const rightPaper = rightContent?.closest(".MuiDrawer-paper")
|
||||
if (leftPaper?.contains(target) || rightPaper?.contains(target)) return
|
||||
|
||||
if (!leftPinned() && leftOpen()) setLeftOpen(false)
|
||||
if (!rightPinned() && rightOpen()) setRightOpen(false)
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", handleFloatingDrawerPointerDown, true)
|
||||
onCleanup(() => document.removeEventListener("pointerdown", handleFloatingDrawerPointerDown, true))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const instanceId = props.instance.id
|
||||
loadBackgroundProcesses(instanceId).catch((error) => {
|
||||
|
|
@ -607,7 +647,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
ModalProps={modalProps}
|
||||
sx={{
|
||||
zIndex: 60,
|
||||
// The tab bar sits outside the floating drawer. Let its controls
|
||||
// receive the gesture; click-away handling above still closes the
|
||||
// drawer when the target is not inside the drawer content.
|
||||
pointerEvents: "none",
|
||||
"& .MuiDrawer-paper": {
|
||||
pointerEvents: "auto",
|
||||
width: isPhoneLayout() ? "100vw" : `${sessionSidebarWidth()}px`,
|
||||
boxSizing: "border-box",
|
||||
borderInlineEnd: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
|
||||
|
|
@ -620,8 +665,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
height: floatingHeight(),
|
||||
},
|
||||
|
||||
// Keep backdrop dismissal for the area below the tab bar without
|
||||
// covering the tab bar itself.
|
||||
"& .MuiBackdrop-root": {
|
||||
pointerEvents: "auto",
|
||||
backgroundColor: "transparent",
|
||||
top: floatingTopPx(),
|
||||
height: floatingHeight(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
@ -723,7 +773,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
ModalProps={modalProps}
|
||||
sx={{
|
||||
zIndex: 60,
|
||||
// See the matching override on the left drawer for rationale.
|
||||
pointerEvents: "none",
|
||||
"& .MuiDrawer-paper": {
|
||||
pointerEvents: "auto",
|
||||
width: isPhoneLayout() ? "100vw" : `${rightDrawerWidth()}px`,
|
||||
boxSizing: "border-box",
|
||||
borderInlineStart: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
|
||||
|
|
@ -736,7 +789,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||
height: floatingHeight(),
|
||||
},
|
||||
"& .MuiBackdrop-root": {
|
||||
pointerEvents: "auto",
|
||||
backgroundColor: "transparent",
|
||||
top: floatingTopPx(),
|
||||
height: floatingHeight(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export interface DrawerChromeApi {
|
|||
unpinRight: () => void
|
||||
closeLeft: () => void
|
||||
closeRight: () => void
|
||||
closeFloatingDrawersIfAny: () => boolean
|
||||
leftAppBarButtonLabel: Accessor<string>
|
||||
rightAppBarButtonLabel: Accessor<string>
|
||||
leftAppBarButtonIcon: Accessor<JSX.Element>
|
||||
|
|
@ -250,6 +251,7 @@ export function useDrawerChrome(options: UseDrawerChromeOptions): DrawerChromeAp
|
|||
unpinRight,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeFloatingDrawersIfAny,
|
||||
leftAppBarButtonLabel,
|
||||
rightAppBarButtonLabel,
|
||||
leftAppBarButtonIcon,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue