Fix composer toolbar collapse flicker on tab switch (#2937)
Retained tabs stay mounted under display:none while inactive, which reports a zero-width onLayout. ControlledAgentControls fed that straight into its density resolver, so a hidden tab's toolbar collapsed to its tightest density and painted one frame collapsed before re-expanding when the tab became visible again. Extract the zero-width guard that useContainerWidthBelow already had into a shared readMeasuredWidth helper, and use it in the composer toolbar's own layout handler so a display:none measurement can never drive its density. Also bump the chat outline rail's MIN_PANEL_WIDTH breakpoint (818 -> 918) so the rail disappears earlier under a narrower panel, and widen the chat-outline e2e spec's viewport so its panel keeps healthy margin over the new threshold.
This commit is contained in:
@@ -12,7 +12,7 @@ import type { ChatOutlineRailProps } from "./rail";
|
||||
// magnifying a slot must not move the box the pointer is resting on. See docs/hover.md.
|
||||
const RAIL_WIDTH = 36;
|
||||
const SLOT_HEIGHT = 8;
|
||||
const MIN_PANEL_WIDTH = 818;
|
||||
const MIN_PANEL_WIDTH = 918;
|
||||
const RESTING_PILL_HEIGHT = 2;
|
||||
const MAGNIFIED_PILL_HEIGHT = 4;
|
||||
const RESTING_PILL_WIDTH = 10;
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
resolveAgentModelSelection,
|
||||
} from "@/composer/agent-controls/utils";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { readMeasuredWidth } from "@/hooks/use-container-width";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { toErrorMessage } from "@/utils/error-messages";
|
||||
import { showProviderNoticeToast } from "@/utils/provider-notice-toast";
|
||||
@@ -520,7 +521,8 @@ function ControlledAgentControls({
|
||||
|
||||
const handleLayout = useCallback(
|
||||
(event: LayoutChangeEvent) => {
|
||||
const availableWidth = event.nativeEvent.layout.width;
|
||||
const availableWidth = readMeasuredWidth(event);
|
||||
if (availableWidth === null) return;
|
||||
availableWidthRef.current = availableWidth;
|
||||
updateDensityForWidth(availableWidth);
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import type { LayoutChangeEvent } from "react-native";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useContainerWidthBelow } from "./use-container-width";
|
||||
import { readMeasuredWidth, useContainerWidthBelow } from "./use-container-width";
|
||||
|
||||
function layoutEvent(width: number): LayoutChangeEvent {
|
||||
return {
|
||||
@@ -18,6 +18,16 @@ function layoutEvent(width: number): LayoutChangeEvent {
|
||||
} as LayoutChangeEvent;
|
||||
}
|
||||
|
||||
describe("readMeasuredWidth", () => {
|
||||
it("returns the width of a rendered container", () => {
|
||||
expect(readMeasuredWidth(layoutEvent(650))).toBe(650);
|
||||
});
|
||||
|
||||
it("returns null for the zero width a hidden retained panel reports", () => {
|
||||
expect(readMeasuredWidth(layoutEvent(0))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useContainerWidthBelow", () => {
|
||||
it("does not re-render for width changes that stay in the same threshold bucket", () => {
|
||||
let renderCount = 0;
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { LayoutChangeEvent } from "react-native";
|
||||
|
||||
/**
|
||||
* Reads a real width out of a layout event. Retained tabs stay mounted under
|
||||
* `display: none`, so an inactive panel emits width 0 — that is "not rendered",
|
||||
* not "very narrow", and must never drive a width-based collapse.
|
||||
*/
|
||||
export function readMeasuredWidth(event: LayoutChangeEvent): number | null {
|
||||
const width = event.nativeEvent.layout.width;
|
||||
return width > 0 ? width : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the width of a container via onLayout.
|
||||
*/
|
||||
@@ -31,8 +41,8 @@ export function useContainerWidthBelow(
|
||||
return {
|
||||
onLayout: useCallback(
|
||||
(e: LayoutChangeEvent) => {
|
||||
const width = e.nativeEvent.layout.width;
|
||||
if (width <= 0) {
|
||||
const width = readMeasuredWidth(e);
|
||||
if (width === null) {
|
||||
return;
|
||||
}
|
||||
const nextIsBelow = width < threshold;
|
||||
|
||||
Reference in New Issue
Block a user