Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ export default function App() {
isOpen={isSettingsOpen}
onClose={() => setIsSettingsOpen(false)}
/>

</div>
)
}
69 changes: 51 additions & 18 deletions src/client/__tests__/visualViewport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,80 +14,113 @@ function createMockClassList() {
describe('visual viewport helpers', () => {
test('updates keyboard inset and clears it', () => {
const style = {
value: '',
values: new Map<string, string>(),
setProperty: (_key: string, val: string) => {
style.value = val
style.values.set(_key, val)
},
removeProperty: (_key: string) => {
style.value = ''
style.values.delete(_key)
},
}
const classList = createMockClassList()
const doc = {
documentElement: { style, classList },
} as unknown as Document
const win = { innerHeight: 900 } as Window
const viewport = { height: 700 } as VisualViewport
const win = { innerHeight: 900, screen: { height: 900 } } as Window
const viewport = { height: 700, width: 800 } as VisualViewport

const updated = updateKeyboardInset({ viewport, win, doc })
expect(updated).toBe(true)
expect(style.value).toBe('200px')
expect(style.values.get('--keyboard-inset')).toBe('200px')
expect(style.values.get('--viewport-offset-top')).toBe('0px')
expect(style.values.get('--viewport-offset-left')).toBe('0px')
expect(style.values.get('--visual-viewport-height')).toBe('700px')
expect(style.values.get('--visual-viewport-width')).toBe('800px')
expect(classList.has('keyboard-visible')).toBe(true)

clearKeyboardInset(doc)
expect(style.value).toBe('')
expect(style.values.has('--keyboard-inset')).toBe(false)
expect(style.values.has('--viewport-offset-top')).toBe(false)
expect(style.values.has('--viewport-offset-left')).toBe(false)
expect(style.values.has('--visual-viewport-height')).toBe(false)
expect(style.values.has('--visual-viewport-width')).toBe(false)
expect(classList.has('keyboard-visible')).toBe(false)
})

test('returns false when viewport is missing', () => {
const doc = {
documentElement: { style: { setProperty: () => {} } },
} as unknown as Document
const win = { innerHeight: 900 } as Window
const win = { innerHeight: 900, screen: { height: 900 } } as Window

expect(updateKeyboardInset({ viewport: null, win, doc })).toBe(false)
})

test('clamps negative keyboard inset to zero', () => {
const style = {
value: '',
values: new Map<string, string>(),
setProperty: (_key: string, val: string) => {
style.value = val
style.values.set(_key, val)
},
}
const classList = createMockClassList()
const doc = {
documentElement: { style, classList },
} as unknown as Document
const win = { innerHeight: 600 } as Window
const viewport = { height: 800 } as VisualViewport
const win = { innerHeight: 600, screen: { height: 600 } } as Window
const viewport = { height: 800, width: 900 } as VisualViewport

const updated = updateKeyboardInset({ viewport, win, doc })
expect(updated).toBe(true)
expect(style.value).toBe('0px')
expect(style.values.get('--keyboard-inset')).toBe('0px')
expect(style.values.get('--visual-viewport-height')).toBe('800px')
expect(style.values.get('--visual-viewport-width')).toBe('900px')
// Keyboard not visible when height is 0
expect(classList.has('keyboard-visible')).toBe(false)
})

test('toggles keyboard-visible class based on threshold', () => {
const style = {
value: '',
values: new Map<string, string>(),
setProperty: (_key: string, val: string) => {
style.value = val
style.values.set(_key, val)
},
}
const classList = createMockClassList()
const doc = {
documentElement: { style, classList },
} as unknown as Document
const win = { innerHeight: 900 } as Window
const win = { innerHeight: 900, screen: { height: 900 } } as Window

// Below threshold (100px) - not visible
updateKeyboardInset({ viewport: { height: 850 } as VisualViewport, win, doc })
updateKeyboardInset({ viewport: { height: 850, width: 800 } as VisualViewport, win, doc })
expect(classList.has('keyboard-visible')).toBe(false)

// Above threshold - visible
updateKeyboardInset({ viewport: { height: 700 } as VisualViewport, win, doc })
updateKeyboardInset({ viewport: { height: 700, width: 800 } as VisualViewport, win, doc })
expect(classList.has('keyboard-visible')).toBe(true)
})

test('accounts for visual viewport offset in inset calculation', () => {
const style = {
values: new Map<string, string>(),
setProperty: (_key: string, val: string) => {
style.values.set(_key, val)
},
}
const classList = createMockClassList()
const doc = {
documentElement: { style, classList },
} as unknown as Document
const win = { innerHeight: 900, screen: { height: 900 } } as Window
const viewport = { height: 600, width: 700, offsetTop: 100, offsetLeft: 20 } as VisualViewport

const updated = updateKeyboardInset({ viewport, win, doc })
expect(updated).toBe(true)
expect(style.values.get('--keyboard-inset')).toBe('200px')
expect(style.values.get('--viewport-offset-top')).toBe('100px')
expect(style.values.get('--visual-viewport-height')).toBe('600px')
expect(style.values.get('--viewport-offset-left')).toBe('20px')
expect(style.values.get('--visual-viewport-width')).toBe('700px')
})
})
22 changes: 16 additions & 6 deletions src/client/__tests__/visualViewportHook.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,19 @@ describe('useVisualViewport', () => {
const events = new Map<string, EventListener>()
const removed: string[] = []
const style = {
value: '',
values: new Map<string, string>(),
setProperty: (_key: string, val: string) => {
style.value = val
style.values.set(_key, val)
},
removeProperty: (_key: string) => {
style.value = ''
style.values.delete(_key)
},
}
const classList = createMockClassList()

const viewport = {
height: 700,
width: 800,
addEventListener: (event: string, handler: EventListener) => {
events.set(event, handler)
},
Expand All @@ -56,6 +57,7 @@ describe('useVisualViewport', () => {

globalAny.window = {
innerHeight: 900,
screen: { height: 900 },
visualViewport: viewport,
} as unknown as Window & typeof globalThis

Expand All @@ -69,18 +71,26 @@ describe('useVisualViewport', () => {
renderer = TestRenderer.create(<HookHarness />)
})

expect(style.value).toBe('200px')
expect(style.values.get('--keyboard-inset')).toBe('200px')
expect(style.values.get('--viewport-offset-top')).toBe('0px')
expect(style.values.get('--viewport-offset-left')).toBe('0px')
expect(style.values.get('--visual-viewport-height')).toBe('700px')
expect(style.values.get('--visual-viewport-width')).toBe('800px')
expect(events.has('resize')).toBe(true)
expect(events.has('scroll')).toBe(true)

events.get('resize')?.({} as Event)
expect(style.value).toBe('200px')
expect(style.values.get('--keyboard-inset')).toBe('200px')

act(() => {
renderer.unmount()
})

expect(removed).toEqual(['resize', 'scroll'])
expect(style.value).toBe('')
expect(style.values.has('--keyboard-inset')).toBe(false)
expect(style.values.has('--viewport-offset-top')).toBe(false)
expect(style.values.has('--viewport-offset-left')).toBe(false)
expect(style.values.has('--visual-viewport-height')).toBe(false)
expect(style.values.has('--visual-viewport-width')).toBe(false)
})
})
19 changes: 18 additions & 1 deletion src/client/components/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -837,10 +837,27 @@ export default function Terminal({
}, [session, sendMessage, containerRef, inTmuxCopyModeRef, setTmuxCopyMode])

const isKeyboardVisible = useCallback(() => {
if (typeof document === 'undefined') return false
const container = containerRef.current
if (!container) return false
const textarea = container.querySelector('.xterm-helper-textarea') as HTMLTextAreaElement | null
return textarea ? document.activeElement === textarea : false
if (textarea && document.activeElement === textarea) {
return true
}

const activeElement = document.activeElement
const inputActive =
typeof HTMLInputElement !== 'undefined' && activeElement instanceof HTMLInputElement
const textareaActive =
typeof HTMLTextAreaElement !== 'undefined' && activeElement instanceof HTMLTextAreaElement
if (inputActive || textareaActive) {
return false
}
if (activeElement && (activeElement as HTMLElement).isContentEditable) {
return false
}

return !!document.documentElement?.classList?.contains('keyboard-visible')
}, [containerRef])

useEffect(() => {
Expand Down
Loading