mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 00:03:09 +00:00
Fix active tab scrolling and normalize editor keyboard inset
- Ensure the active session tab is scrolled into view immediately when its layout finishes to prevent it from remaining hidden off-screen. - Round, clamp, and validate WebView visualViewport bottom measurements to avoid rendering inconsistencies from fractional or invalid insets. - Trigger an immediate keyboard inset report on script initialization.
This commit is contained in:
@@ -3933,6 +3933,9 @@ export default function SessionScreen() {
|
||||
onLayout={(e) => {
|
||||
const { x, width } = e.nativeEvent.layout
|
||||
tabLayoutsRef.current.set(t.id, { x, width })
|
||||
if (t.id === activeSessionTabIdRef.current) {
|
||||
scrollActiveTabIntoView(t.id, false)
|
||||
}
|
||||
}}
|
||||
onPress={() => switchSessionTab(t)}
|
||||
onLongPress={() => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from 'lucide-react-native'
|
||||
import WebView, { type WebViewMessageEvent } from 'react-native-webview'
|
||||
import { colors, radii, spacing } from '../theme/mobile-theme'
|
||||
import { normalizeMobileRichMarkdownKeyboardInset } from './mobile-rich-markdown-editor-keyboard-inset-script'
|
||||
import {
|
||||
buildMobileRichMarkdownEditorHtml,
|
||||
escapeInjectedJavaScriptString
|
||||
@@ -198,7 +199,10 @@ function MobileRichMarkdownEditorInner({
|
||||
return
|
||||
}
|
||||
if (editorMessage.type === 'keyboardInset' && typeof editorMessage.bottom === 'number') {
|
||||
onKeyboardInsetChange?.(editorMessage.bottom)
|
||||
const bottom = normalizeMobileRichMarkdownKeyboardInset(editorMessage.bottom)
|
||||
if (bottom !== null) {
|
||||
onKeyboardInsetChange?.(bottom)
|
||||
}
|
||||
}
|
||||
},
|
||||
[applyContent, applyEditable, content, editable, onChange, onKeyboardInsetChange]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeMobileRichMarkdownKeyboardInset } from './mobile-rich-markdown-editor-keyboard-inset-script'
|
||||
|
||||
describe('normalizeMobileRichMarkdownKeyboardInset', () => {
|
||||
it('rounds finite inset measurements for native layout', () => {
|
||||
expect(normalizeMobileRichMarkdownKeyboardInset(42.6)).toBe(43)
|
||||
})
|
||||
|
||||
it('clamps negative inset measurements to zero', () => {
|
||||
expect(normalizeMobileRichMarkdownKeyboardInset(-8)).toBe(0)
|
||||
})
|
||||
|
||||
it('rejects non-finite inset measurements', () => {
|
||||
expect(normalizeMobileRichMarkdownKeyboardInset(Number.NaN)).toBeNull()
|
||||
expect(normalizeMobileRichMarkdownKeyboardInset(Number.POSITIVE_INFINITY)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,13 @@
|
||||
// Native Keyboard events are unreliable while focus lives in the editor
|
||||
// WebView, so measure the covered region directly from visualViewport and let
|
||||
// RN lift its native Save/Discard bar above it.
|
||||
export function normalizeMobileRichMarkdownKeyboardInset(value: number): number | null {
|
||||
if (!Number.isFinite(value)) {
|
||||
return null
|
||||
}
|
||||
return Math.max(0, Math.round(value))
|
||||
}
|
||||
|
||||
export const MOBILE_RICH_MARKDOWN_KEYBOARD_INSET_SCRIPT = `
|
||||
var lastInset = -1;
|
||||
function reportKeyboardInset() {
|
||||
@@ -17,4 +24,5 @@ export const MOBILE_RICH_MARKDOWN_KEYBOARD_INSET_SCRIPT = `
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', reportKeyboardInset);
|
||||
window.visualViewport.addEventListener('scroll', reportKeyboardInset);
|
||||
reportKeyboardInset();
|
||||
}`
|
||||
|
||||
@@ -28,7 +28,6 @@ describe('resolveTabStripScrollOffset', () => {
|
||||
})
|
||||
|
||||
it('scrolls right to reveal a tab off the right edge', () => {
|
||||
// tabEnd = 640 + 128 = 768; needs offset 768 + 12 - 360 = 420.
|
||||
expect(
|
||||
resolveTabStripScrollOffset({
|
||||
tabX: 640,
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
export type TabStripScrollInput = {
|
||||
/** Left edge of the active tab within the scroll content. */
|
||||
tabX: number
|
||||
/** Width of the active tab. */
|
||||
tabWidth: number
|
||||
/** Visible width of the horizontal tab strip. */
|
||||
viewportWidth: number
|
||||
/** Total scrollable content width. */
|
||||
contentWidth: number
|
||||
/** Current horizontal scroll offset. */
|
||||
currentOffset: number
|
||||
/** Margin to keep between the tab and the viewport edge when revealing. */
|
||||
margin?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the horizontal offset that brings the active tab fully into view.
|
||||
*
|
||||
* Returns the current offset unchanged when the tab is already visible so we
|
||||
* never scroll on redundant active-tab updates; otherwise reveals the tab with
|
||||
* the nearest minimal scroll, clamped to the content bounds.
|
||||
* Keep active-tab reveal deterministic across async RN layout events without
|
||||
* nudging the strip when the tab is already visible.
|
||||
*/
|
||||
export function resolveTabStripScrollOffset({
|
||||
tabX,
|
||||
@@ -40,10 +31,8 @@ export function resolveTabStripScrollOffset({
|
||||
|
||||
let nextOffset = currentOffset
|
||||
if (tabStart < visibleStart + margin) {
|
||||
// Off the left edge: align the tab start with the left margin.
|
||||
nextOffset = tabStart - margin
|
||||
} else if (tabEnd > visibleEnd - margin) {
|
||||
// Off the right edge: align the tab end with the right margin.
|
||||
nextOffset = tabEnd + margin - viewportWidth
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user