Skip to content

Commit 47b155e

Browse files
committed
fix(keyboard): share height state across useKeyboardHeight instances
When the thread drawer is open alongside the main room view, two RoomInput components each mount their own useKeyboardHeight instance. On keyboard open the thread instance had savedHeight=0 (freshly mounted), so its immediate-estimate branch fell back to viewport.height — the wrong mid-animation value — and overwrote the correct estimate already written by the main room instance. This produced a third layout change (wrong height → correct height) visible as jank on every keyboard open. Fix: promote savedHeight, cssVarsSet, and the mount reference counter to module-level variables so all instances share them. - sharedSavedHeight: all instances read and write the same value, so the estimate is always correct even for newly-mounted instances. - cssVarsApplied: the 'only set once while keyboard open' guard now works across instances, preventing double setCSSVars calls. - mountCount: reference-counted so only the last instance to unmount clears the CSS variables — prevents the thread drawer unmounting while the main room keyboard is still open from wiping --sable-visible-height.
1 parent 082f93f commit 47b155e

1 file changed

Lines changed: 31 additions & 14 deletions

File tree

src/app/hooks/ios-keyboard-fix/useKeyboardHeight.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,41 +21,54 @@ import { useEffect, useRef, useState } from 'react';
2121
// visible and skips its document-scroll prediction.
2222
const STABILITY_MS = 80;
2323

24+
// Module-level state shared across all useKeyboardHeight instances.
25+
// The keyboard height is a device property — it's the same regardless of
26+
// which input has focus. Sharing savedHeight prevents the case where two
27+
// simultaneous RoomInput instances (main timeline + open thread drawer) race
28+
// on keyboard open: the thread instance starts with savedHeight=0 and would
29+
// overwrite the main instance's correct estimate with the wrong mid-animation
30+
// viewport.height.
31+
// mountCount is a reference counter so only the last unmounting instance
32+
// clears the CSS vars (prevents the thread drawer unmounting mid-keyboard-open
33+
// from wiping --sable-visible-height while the main room input still uses it).
34+
let sharedSavedHeight = 0;
35+
let mountCount = 0;
36+
// Whether --sable-visible-height is currently applied. Shared so multiple
37+
// instances see the same state and the "only set once while open" guard works
38+
// across instances.
39+
let cssVarsApplied = false;
40+
2441
export function useKeyboardHeight() {
2542
const [keyboardHeight, setKeyboardHeight] = useState(0);
2643
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
2744

2845
// Mirror state in refs so triggerPreLift sees fresh values from
2946
// an onMouseDown handler without re-creating the function each render.
30-
const savedHeight = useRef(0);
3147
const hasOpenedOnce = useRef(false);
3248
const isVisibleRef = useRef(false);
3349

3450
useEffect(() => {
3551
const viewport = window.visualViewport;
3652
if (!viewport) return undefined;
3753

54+
mountCount += 1;
3855
let baselineHeight = window.innerHeight;
3956
let stabilityTimer: ReturnType<typeof setTimeout> | null = null;
4057
let pendingValue = 0;
41-
// Tracks whether --sable-visible-height is currently set so the opening
42-
// path only fires setCSSVars once (avoids double-setting on repeated
43-
// resize events while the keyboard is already open).
44-
let cssVarsSet = false;
4558

4659
const setCSSVars = (viewportHeight: number) => {
4760
document.documentElement.style.setProperty(
4861
'--sable-visible-height',
4962
`${Math.round(viewportHeight)}px`
5063
);
5164
document.documentElement.style.setProperty('--sable-safe-bottom', '0px');
52-
cssVarsSet = true;
65+
cssVarsApplied = true;
5366
};
5467

5568
const clearCSSVars = () => {
5669
document.documentElement.style.removeProperty('--sable-visible-height');
5770
document.documentElement.style.removeProperty('--sable-safe-bottom');
58-
cssVarsSet = false;
71+
cssVarsApplied = false;
5972
};
6073

6174
const handleResize = () => {
@@ -85,9 +98,9 @@ export function useKeyboardHeight() {
8598
// immediate and stability-timer setCSSVars calls land on the same pixel
8699
// value — eliminating the second layout change that causes visible
87100
// timeline stutter during the keyboard animation.
88-
if (!cssVarsSet) {
101+
if (!cssVarsApplied) {
89102
const estimatedViewportHeight =
90-
savedHeight.current > 0 ? baselineHeight - savedHeight.current : viewport.height;
103+
sharedSavedHeight > 0 ? baselineHeight - sharedSavedHeight : viewport.height;
91104
setCSSVars(estimatedViewportHeight);
92105
}
93106

@@ -102,7 +115,7 @@ export function useKeyboardHeight() {
102115
pendingValue = calculatedHeight;
103116
if (stabilityTimer) clearTimeout(stabilityTimer);
104117
stabilityTimer = setTimeout(() => {
105-
savedHeight.current = pendingValue;
118+
sharedSavedHeight = pendingValue;
106119
hasOpenedOnce.current = true;
107120
isVisibleRef.current = true;
108121
setCSSVars(viewport.height); // refine to final settled viewport height
@@ -120,7 +133,7 @@ export function useKeyboardHeight() {
120133
stabilityTimer = null;
121134
}
122135
pendingValue = 0;
123-
savedHeight.current = 0;
136+
sharedSavedHeight = 0;
124137
hasOpenedOnce.current = false;
125138
isVisibleRef.current = false;
126139
clearCSSVars();
@@ -135,10 +148,14 @@ export function useKeyboardHeight() {
135148
viewport.addEventListener('resize', handleResize);
136149
window.addEventListener('orientationchange', handleOrientationChange);
137150
return () => {
151+
mountCount -= 1;
138152
if (stabilityTimer) clearTimeout(stabilityTimer);
139153
viewport.removeEventListener('resize', handleResize);
140154
window.removeEventListener('orientationchange', handleOrientationChange);
141-
clearCSSVars();
155+
// Only clear CSS vars when the last instance unmounts — prevents the thread
156+
// drawer unmounting mid-keyboard-open from wiping the variable while the
157+
// main room's RoomInput still has the keyboard open.
158+
if (mountCount === 0) clearCSSVars();
142159
};
143160
}, []);
144161

@@ -148,8 +165,8 @@ export function useKeyboardHeight() {
148165
// Reads from refs so it always sees the latest state, even when
149166
// captured by an onMouseDown handler that mounted earlier.
150167
const triggerPreLift = () => {
151-
if (hasOpenedOnce.current && savedHeight.current > 0 && isVisibleRef.current) {
152-
setKeyboardHeight(savedHeight.current);
168+
if (hasOpenedOnce.current && sharedSavedHeight > 0 && isVisibleRef.current) {
169+
setKeyboardHeight(sharedSavedHeight);
153170
}
154171
};
155172

0 commit comments

Comments
 (0)