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
4 changes: 1 addition & 3 deletions src/ui/attachments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ describe("Attachments", () => {
unmount();
});

// React 18 hands a function component no `ref` unless it is wrapped in
// `forwardRef`; React 19, which these tests run on, hands it either way.
test("the list, an attachment and its preview take refs on React 18 too", () => {
test("the list, an attachment and its preview take refs", () => {
for (const part of [Attachments, Attachment, AttachmentPreview]) {
expect(part.$$typeof).toBe(Symbol.for("react.forward_ref"));
}
Expand Down
43 changes: 0 additions & 43 deletions src/ui/conversation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,47 +308,6 @@ describe("ConversationItem registry", () => {
unsubscribe();
mounted.unmount();
});

test("a null ref drops the item, which is how React 18 detaches", async () => {
let api: ConversationContextValue | null = null;
function Probe() {
api = useConversation();
return null;
}
const mounted = render(
<Conversation>
<ConversationContent>
<ConversationItem itemId="a">a</ConversationItem>
</ConversationContent>
<Probe />
</Conversation>,
);
const element =
mounted.container.querySelector<HTMLElement>("[data-item-id='a']");
const fiberKey = Object.keys(element ?? {}).find((key) =>
key.startsWith("__reactFiber$"),
);
const fiber =
element === null || fiberKey === undefined
? undefined
: (element as HTMLElement & Record<string, { ref?: unknown }>)[
fiberKey
];
const ref = fiber?.ref;
if (typeof ref !== "function") throw new Error("expected the item ref");
const items = () => {
if (api === null) throw new Error("expected the conversation API");
return api.items;
};

act(() => {
ref(null);
});
await act(() => Promise.resolve());
expect(items().get("a")).toBeUndefined();
expect(items().ids()).toEqual([]);
mounted.unmount();
});
});

describe("ConversationScrollButton", () => {
Expand Down Expand Up @@ -455,8 +414,6 @@ describe("ConversationContent", () => {
});

test("the log, an item and the scroll button forward a host ref", async () => {
// React 18 drops `ref` from a function component's props: only a
// forwardRef component receives it there.
for (const part of [
ConversationContent,
ConversationItem,
Expand Down
9 changes: 1 addition & 8 deletions src/ui/conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -966,17 +966,10 @@ export const ConversationItem = forwardRef<
ConversationItemProps
>(function ConversationItem({ itemId, ...props }, ref) {
const engine = useEngine("ConversationItem");
// React 19 runs the cleanup `register` returns. React 18 ignores that
// return and detaches by calling the ref with null, so release there too.
const release = useRef<(() => void) | null>(null);
const register = useCallback(
(element: HTMLDivElement | null) => {
release.current?.();
release.current = null;
if (element === null) return;
const cleanup = engine.registry.register(itemId, element);
release.current = cleanup;
return cleanup;
return engine.registry.register(itemId, element);
},
[engine, itemId],
);
Expand Down
2 changes: 0 additions & 2 deletions src/ui/message.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -554,8 +554,6 @@ describe("MessageActions", () => {
});

test("the overflow menu mounts its items only while open", () => {
// React 18 drops `ref` from a function component's props: only a
// forwardRef component receives it there.
expect(MessageAction.$$typeof).toBe(Symbol.for("react.forward_ref"));
const onPin = mock(() => {});
const mounted = render(
Expand Down
8 changes: 1 addition & 7 deletions src/ui/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@ const isProduction =
const DEFAULT_MIN_WIDTH_PX = 320;
const DEFAULT_MAIN_MIN_WIDTH_PX = 440;

// React 18 does not know `inert` and drops a boolean for it, while React 19
// reads it as a boolean. A non-empty string other than "true" or "false"
// reaches the DOM on both without a warning; the record type is wider than
// React 19's boolean-only `inert` to allow it.
const HIDDEN_PANE_PROPS: Record<string, string> = { inert: "inert" };

type PanelContextValue = {
readonly panelId: string;
readonly handleProps: ResizableHandleProps;
Expand Down Expand Up @@ -420,7 +414,7 @@ export function PanelPane({ value, className, ...props }: PanelPaneProps) {
<div
data-slot="panel-pane"
data-state={shown ? "active" : "inactive"}
{...(shown ? null : HIDDEN_PANE_PROPS)}
inert={!shown}
className={cn(
"col-start-1 row-start-1 min-h-0 min-w-0 overflow-y-auto",
!shown && "invisible",
Expand Down
2 changes: 0 additions & 2 deletions src/ui/prompt-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ describe("PromptInput", () => {
});

test("the textarea forwards a host ref", () => {
// React 18 drops `ref` from a function component's props: only a
// forwardRef component receives it there.
expect(PromptInputTextarea.$$typeof).toBe(Symbol.for("react.forward_ref"));
const ref = createRef<HTMLTextAreaElement>();
const container = document.createElement("div");
Expand Down
Loading