fix: wait for container dimensions before xterm init
- xterm docs require parent to have dimensions when open() is called - Added ResizeObserver to wait for non-zero dimensions before initializing - Added container ResizeObserver to handle resizes (header hide, keyboard) - Fixed cleanup to properly disconnect observers and handle uninitialized ws
This commit is contained in:
@@ -56,6 +56,7 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
});
|
});
|
||||||
const lastPingRef = useRef<number>(0);
|
const lastPingRef = useRef<number>(0);
|
||||||
const heartbeatCheckRef = useRef<number | null>(null);
|
const heartbeatCheckRef = useRef<number | null>(null);
|
||||||
|
const containerResizeObserverRef = useRef<ResizeObserver | null>(null);
|
||||||
|
|
||||||
const calculateFontSize = useCallback(() => {
|
const calculateFontSize = useCallback(() => {
|
||||||
if (!isMobile) return fontSize;
|
if (!isMobile) return fontSize;
|
||||||
@@ -201,17 +202,20 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
term.loadAddon(new WebLinksAddon());
|
term.loadAddon(new WebLinksAddon());
|
||||||
|
|
||||||
term.open(terminalRef.current);
|
// Wait for container to have non-zero dimensions before opening xterm
|
||||||
|
// xterm docs: "parent must be visible (have dimensions) when open is called"
|
||||||
|
const container = terminalRef.current;
|
||||||
|
let ws: WebSocket;
|
||||||
|
|
||||||
// Connect WebSocket
|
const initTerminal = () => {
|
||||||
const ws = connectWebSocket();
|
term.open(container);
|
||||||
|
|
||||||
// Fit after layout settles - use rAF + timeout to ensure DOM is ready
|
// Connect WebSocket
|
||||||
requestAnimationFrame(() => {
|
ws = connectWebSocket();
|
||||||
|
|
||||||
|
// Fit after layout settles
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
fitAddon.fit();
|
requestAnimationFrame(() => {
|
||||||
// Force a second fit after layout fully settles
|
|
||||||
setTimeout(() => {
|
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
const { cols, rows } = term;
|
const { cols, rows } = term;
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
@@ -223,24 +227,58 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// Fit after mobile header auto-hides (3s delay + 0.3s transition)
|
// Force a second fit after layout fully settles
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
const { cols, rows } = term;
|
const { cols, rows } = term;
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "resize",
|
type: "resize",
|
||||||
cols,
|
cols,
|
||||||
rows,
|
rows,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, 4000);
|
}, 500);
|
||||||
|
|
||||||
|
// Fit after mobile header auto-hides (3s delay + 0.3s transition)
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddon.fit();
|
||||||
|
const { cols, rows } = term;
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "resize",
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 4000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Check if container already has dimensions
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
if (rect.width > 0 && rect.height > 0) {
|
||||||
|
initTerminal();
|
||||||
|
} else {
|
||||||
|
// Wait for container to be laid out
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const { width, height } = entry.contentRect;
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
initTerminal();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle terminal input
|
// Handle terminal input
|
||||||
term.onData((data) => {
|
term.onData((data) => {
|
||||||
@@ -281,6 +319,28 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
// Watch container size changes (e.g. mobile header auto-hide, keyboard)
|
||||||
|
const containerResizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const { width, height } = entry.contentRect;
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
fitAddon.fit();
|
||||||
|
const { cols, rows } = term;
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "resize",
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
containerResizeObserver.observe(container);
|
||||||
|
containerResizeObserverRef.current = containerResizeObserver;
|
||||||
|
|
||||||
// Notify parent about terminal readiness
|
// Notify parent about terminal readiness
|
||||||
if (onTerminalReadyRef.current) {
|
if (onTerminalReadyRef.current) {
|
||||||
const sendData = (data: string) => {
|
const sendData = (data: string) => {
|
||||||
@@ -299,7 +359,7 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
|
|
||||||
// Visibility API for reconnection
|
// Visibility API for reconnection
|
||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => {
|
||||||
if (document.visibilityState === "visible" && ws.readyState !== WebSocket.OPEN) {
|
if (document.visibilityState === "visible" && ws && ws.readyState !== WebSocket.OPEN) {
|
||||||
reconnectAttemptsRef.current = 0;
|
reconnectAttemptsRef.current = 0;
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}
|
}
|
||||||
@@ -310,7 +370,13 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
|||||||
clearTimeout(resizeTimeout);
|
clearTimeout(resizeTimeout);
|
||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener("resize", handleResize);
|
||||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
ws.close();
|
if (containerResizeObserverRef.current) {
|
||||||
|
containerResizeObserverRef.current.disconnect();
|
||||||
|
containerResizeObserverRef.current = null;
|
||||||
|
}
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
term.dispose();
|
term.dispose();
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|||||||
Reference in New Issue
Block a user