mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 11:37:25 +00:00
Keep inactive tabs clickable in narrow docked strips (#34881)
* Keep inactive tabs clickable when active dock crowds the strip * Fix narrow tab dock state after drag cancellation --------- Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
@@ -838,6 +838,11 @@ body[data-modal-overlay-open]
|
||||
mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 1.5rem), transparent);
|
||||
}
|
||||
|
||||
.terminal-tab-strip[data-active-tab-dock-disabled] > [data-active-tab-dock] {
|
||||
position: relative;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
/* Why: tabs only draw a right border, so a tab docked at the end has no seam against the tab it covers. */
|
||||
.terminal-tab-strip[data-active-tab-docked='end'] > [data-active-tab-dock]::before {
|
||||
content: '';
|
||||
|
||||
@@ -4,9 +4,16 @@ import { vi } from 'vitest'
|
||||
// Headless React: hooks are stubbed so TabBar can be invoked as a plain function.
|
||||
export async function stubHeadlessReact(): Promise<Record<string, unknown>> {
|
||||
const actual = await vi.importActual<typeof ReactModule>('react')
|
||||
const contextDefaults = new WeakMap<object, unknown>()
|
||||
return {
|
||||
...actual,
|
||||
memo: <T>(component: T) => component,
|
||||
createContext: <T>(defaultValue: T) => {
|
||||
const context = actual.createContext(defaultValue)
|
||||
contextDefaults.set(context, defaultValue)
|
||||
return context
|
||||
},
|
||||
useContext: (context: ReactModule.Context<unknown>) => contextDefaults.get(context),
|
||||
useEffect: () => {},
|
||||
useLayoutEffect: () => {},
|
||||
useCallback: <T extends (...args: never[]) => unknown>(callback: T) => callback,
|
||||
|
||||
@@ -18,9 +18,11 @@ import {
|
||||
getActiveTabDockSide,
|
||||
readTabStripSlotIds,
|
||||
revealTabStripSlot,
|
||||
updateTabStripDockAvailability,
|
||||
type ActiveTabDockSide
|
||||
} from './tab-strip-slot-geometry'
|
||||
import { motionSafeScrollBehavior } from '@/hooks/usePrefersReducedMotion'
|
||||
import { useTabDragActive } from '../tab-group/tab-drag-context'
|
||||
|
||||
const TAB_STRIP_SCROLL_FRACTION = 0.75
|
||||
const TAB_STRIP_MIN_SCROLL_STEP_PX = 120
|
||||
@@ -69,6 +71,7 @@ export function useTabStripOverflowNavigation({
|
||||
scrollTabStrip: (direction: 'start' | 'end', behavior?: ScrollBehavior) => void
|
||||
subscribeToStripResize: (listener: () => void) => () => void
|
||||
} {
|
||||
const isTabDragActive = useTabDragActive()
|
||||
const tabStripRef = useRef<HTMLDivElement>(null)
|
||||
const stripResizeListenersRef = useRef<Set<() => void>>(new Set())
|
||||
const prevStripRef = useRef<{ worktreeId: string; tabIds: ReadonlySet<string> } | null>(null)
|
||||
@@ -94,6 +97,7 @@ export function useTabStripOverflowNavigation({
|
||||
setTabStripOverflowState((previous) =>
|
||||
sameTabStripOverflowState(previous, next) ? previous : next
|
||||
)
|
||||
updateTabStripDockAvailability(el)
|
||||
setActiveTabDockSide(getActiveTabDockSide(el))
|
||||
}, [])
|
||||
const scrollTabStrip = useCallback(
|
||||
@@ -291,10 +295,10 @@ export function useTabStripOverflowNavigation({
|
||||
recordScrollAnchor()
|
||||
}, [activeVisibleTabId, recordScrollAnchor, updateTabStripOverflowState])
|
||||
|
||||
// Why: moving the dock between slots shifts which edge it is drawn at with no scroll or resize.
|
||||
// Dock ownership and drag transitions change sticky layout without scrolling or resizing.
|
||||
useLayoutEffect(() => {
|
||||
updateTabStripOverflowState()
|
||||
}, [activeDockSlotId, updateTabStripOverflowState])
|
||||
}, [activeDockSlotId, isTabDragActive, updateTabStripOverflowState])
|
||||
|
||||
// Why every render: the close flag belongs to the commit that set it, not a later tab switch.
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const SLOT_SELECTOR = '[data-tab-strip-slot]'
|
||||
/** Marks the slot of the tab that renders active; it is sticky, so it docks to the edge it would scroll past. */
|
||||
const DOCK_SELECTOR = '[data-active-tab-dock]'
|
||||
const DOCK_DISABLED_ATTRIBUTE = 'data-active-tab-dock-disabled'
|
||||
|
||||
export type ActiveTabDockSide = 'start' | 'end'
|
||||
|
||||
@@ -14,7 +15,18 @@ function getSlotId(slot: HTMLElement): string {
|
||||
}
|
||||
|
||||
function getActiveTabDock(strip: HTMLElement): HTMLElement | null {
|
||||
return strip.querySelector<HTMLElement>(`:scope > ${DOCK_SELECTOR}`)
|
||||
return strip.hasAttribute(DOCK_DISABLED_ATTRIBUTE)
|
||||
? null
|
||||
: strip.querySelector<HTMLElement>(`:scope > ${DOCK_SELECTOR}`)
|
||||
}
|
||||
|
||||
export function updateTabStripDockAvailability(strip: HTMLElement): void {
|
||||
const dock = strip.querySelector<HTMLElement>(`:scope > ${DOCK_SELECTOR}`)
|
||||
// Leave room for another tab beside the dock, or inactive tabs become unreachable.
|
||||
strip.toggleAttribute(
|
||||
DOCK_DISABLED_ATTRIBUTE,
|
||||
dock !== null && strip.clientWidth < dock.getBoundingClientRect().width * 2
|
||||
)
|
||||
}
|
||||
|
||||
export function findTabStripSlot(strip: HTMLElement, tabId: string): HTMLElement | undefined {
|
||||
|
||||
Reference in New Issue
Block a user