@assistant-ui/react 0.15.31 → 0.15.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/_rolldown/runtime.js +1 -2
- package/dist/adapters/OpenAIRealtimeVoiceAdapter.js +1 -2
- package/dist/assistant-transport.js +1 -2
- package/dist/cloud-renderer/CloudRendererHost.js +1 -2
- package/dist/context/ReadonlyStore.js +1 -2
- package/dist/context/providers/MessageProvider.js +1 -2
- package/dist/context/providers/ThreadViewportProvider.js +1 -2
- package/dist/context/react/ThreadViewportContext.js +1 -2
- package/dist/context/react/utils/createContextHook.js +1 -2
- package/dist/context/react/utils/createContextStoreHook.js +1 -2
- package/dist/context/stores/ThreadViewport.js +1 -2
- package/dist/devtools/DevToolsHooks.js +1 -2
- package/dist/hooks/useMessageQuote.js +1 -2
- package/dist/hooks/useMessageTiming.js +1 -2
- package/dist/hooks/useToolCallElapsed.js +1 -2
- package/dist/internal.js +1 -2
- package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -2
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +3 -3
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.js +1 -2
- package/dist/mcp-apps/app-frame.js +1 -2
- package/dist/mcp-apps/bridge.js +1 -2
- package/dist/mcp-apps/csp.js +1 -2
- package/dist/mcp-apps/types.js +1 -2
- package/dist/mcp-apps/utils.js +1 -2
- package/dist/model-context/frame/useAssistantFrameHost.js +1 -2
- package/dist/model-context/makeAssistantVisible.js +1 -2
- package/dist/primitives/actionBar/ActionBarCopy.js +1 -2
- package/dist/primitives/actionBar/ActionBarEdit.js +1 -2
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js +1 -2
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -2
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -2
- package/dist/primitives/actionBar/ActionBarInteractionContext.js +1 -2
- package/dist/primitives/actionBar/ActionBarReload.js +1 -2
- package/dist/primitives/actionBar/ActionBarRoot.js +1 -2
- package/dist/primitives/actionBar/ActionBarSpeak.js +1 -2
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js +1 -2
- package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -2
- package/dist/primitives/actionBar.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreContent.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreItem.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js +1 -2
- package/dist/primitives/actionBarMore/scope.js +1 -2
- package/dist/primitives/actionBarMore.js +1 -2
- package/dist/primitives/assistantModal/AssistantModalAnchor.js +1 -2
- package/dist/primitives/assistantModal/AssistantModalContent.js +1 -2
- package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -2
- package/dist/primitives/assistantModal/AssistantModalTrigger.js +1 -2
- package/dist/primitives/assistantModal/popoverRenderPrimitives.js +1 -2
- package/dist/primitives/assistantModal/scope.js +1 -2
- package/dist/primitives/assistantModal.js +1 -2
- package/dist/primitives/attachment/AttachmentName.js +1 -2
- package/dist/primitives/attachment/AttachmentRemove.js +1 -2
- package/dist/primitives/attachment/AttachmentRoot.js +1 -2
- package/dist/primitives/attachment/AttachmentThumb.js +1 -2
- package/dist/primitives/attachment.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerCount.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerNext.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerNumber.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerPrevious.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerRoot.js +1 -2
- package/dist/primitives/branchPicker.js +1 -2
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts +5 -4
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +57 -20
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtRoot.js +1 -2
- package/dist/primitives/chainOfThought.js +1 -2
- package/dist/primitives/composer/ComposerAddAttachment.js +1 -2
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -2
- package/dist/primitives/composer/ComposerCancel.js +1 -2
- package/dist/primitives/composer/ComposerCompactContext.js +1 -2
- package/dist/primitives/composer/ComposerDictate.js +1 -2
- package/dist/primitives/composer/ComposerDictationTranscript.js +1 -2
- package/dist/primitives/composer/ComposerInput.js +1 -2
- package/dist/primitives/composer/ComposerInputPluginContext.js +1 -2
- package/dist/primitives/composer/ComposerQuote.js +1 -2
- package/dist/primitives/composer/ComposerResume.js +1 -2
- package/dist/primitives/composer/ComposerRoot.js +1 -2
- package/dist/primitives/composer/ComposerSend.js +1 -2
- package/dist/primitives/composer/ComposerStopDictation.js +1 -2
- package/dist/primitives/composer/composerInputSelector.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopover.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverBack.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +1 -2
- package/dist/primitives/composer/trigger/detectTrigger.js +1 -2
- package/dist/primitives/composer/trigger/index.js +1 -2
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +1 -2
- package/dist/primitives/composer/trigger/triggerDetectionResource.js +1 -2
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js +1 -2
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +1 -2
- package/dist/primitives/composer/trigger/triggerSelectionResource.js +1 -2
- package/dist/primitives/composer/useComposerCancelWithFocus.js +1 -2
- package/dist/primitives/composer/useComposerInputState.js +1 -2
- package/dist/primitives/composer.js +1 -2
- package/dist/primitives/dropdownMenuRenderPrimitives.js +1 -2
- package/dist/primitives/error/ErrorMessage.js +1 -2
- package/dist/primitives/error/ErrorRoot.js +1 -2
- package/dist/primitives/error.js +1 -2
- package/dist/primitives/message/MessageError.js +1 -2
- package/dist/primitives/message/MessageIf.js +1 -2
- package/dist/primitives/message/MessageParts.js +1 -2
- package/dist/primitives/message/MessagePartsGrouped.js +1 -2
- package/dist/primitives/message/MessageRoot.js +1 -2
- package/dist/primitives/message.js +1 -2
- package/dist/primitives/messagePart/MessagePartImage.js +1 -2
- package/dist/primitives/messagePart/MessagePartText.js +1 -2
- package/dist/primitives/messagePart/useMessagePartData.js +1 -2
- package/dist/primitives/messagePart/useMessagePartFile.js +1 -2
- package/dist/primitives/messagePart/useMessagePartImage.js +1 -2
- package/dist/primitives/messagePart/useMessagePartReasoning.js +1 -2
- package/dist/primitives/messagePart/useMessagePartSource.js +1 -2
- package/dist/primitives/messagePart/useMessagePartText.js +1 -2
- package/dist/primitives/messagePart.js +1 -2
- package/dist/primitives/queueItem/QueueItemRemove.js +1 -2
- package/dist/primitives/queueItem/QueueItemSteer.js +1 -2
- package/dist/primitives/queueItem/QueueItemText.js +1 -2
- package/dist/primitives/queueItem.js +1 -2
- package/dist/primitives/reasoning/useScrollLock.d.ts.map +1 -1
- package/dist/primitives/reasoning/useScrollLock.js +41 -25
- package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -2
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +1 -2
- package/dist/primitives/selectionToolbar.js +1 -2
- package/dist/primitives/suggestion/SuggestionDescription.js +1 -2
- package/dist/primitives/suggestion/SuggestionTitle.js +1 -2
- package/dist/primitives/suggestion/SuggestionTrigger.js +1 -2
- package/dist/primitives/suggestion.js +1 -2
- package/dist/primitives/thread/ThreadEmpty.js +1 -2
- package/dist/primitives/thread/ThreadIf.js +1 -2
- package/dist/primitives/thread/ThreadLoadEarlier.js +1 -2
- package/dist/primitives/thread/ThreadRoot.js +1 -2
- package/dist/primitives/thread/ThreadRootElementContext.js +1 -2
- package/dist/primitives/thread/ThreadScrollToBottom.js +1 -2
- package/dist/primitives/thread/ThreadSuggestion.js +1 -2
- package/dist/primitives/thread/ThreadViewport.js +1 -2
- package/dist/primitives/thread/ThreadViewportFooter.js +1 -2
- package/dist/primitives/thread/topAnchor/computeTopAnchorSlack.js +1 -2
- package/dist/primitives/thread/topAnchor/createReserveObservers.js +1 -2
- package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js +1 -2
- package/dist/primitives/thread/topAnchor/topAnchorTurn.js +1 -2
- package/dist/primitives/thread/topAnchor/topAnchorUtils.js +1 -2
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +1 -2
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +1 -2
- package/dist/primitives/thread.js +1 -2
- package/dist/primitives/threadList/ThreadListLoadMore.js +1 -2
- package/dist/primitives/threadList/ThreadListNew.js +1 -2
- package/dist/primitives/threadList/ThreadListRoot.js +1 -2
- package/dist/primitives/threadList.js +1 -2
- package/dist/primitives/threadListFocusGroup.js +1 -2
- package/dist/primitives/threadListItem/ThreadListItemArchive.js +1 -2
- package/dist/primitives/threadListItem/ThreadListItemDelete.js +1 -2
- package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.js +2 -3
- package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.js +1 -2
- package/dist/primitives/threadListItem/ThreadListItemUnarchive.js +1 -2
- package/dist/primitives/threadListItem.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -2
- package/dist/primitives/threadListItemMore/scope.js +1 -2
- package/dist/primitives/threadListItemMore.js +1 -2
- package/dist/sandbox-host/SandboxHost.js +1 -2
- package/dist/unstable/useComposerInput.js +1 -2
- package/dist/unstable/useComposerInputHistory.js +1 -2
- package/dist/unstable/useLiveCompletionAdapter.js +1 -2
- package/dist/unstable/useMentionAdapter.js +1 -2
- package/dist/unstable/useMessageStallDetection.js +1 -2
- package/dist/unstable/useModelContextSnapshot.js +1 -2
- package/dist/unstable/useSlashCommandAdapter.js +1 -2
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js +1 -2
- package/dist/unstable/webmcp/convertTools.js +1 -2
- package/dist/unstable/webmcp/useWebMcpProvider.js +1 -2
- package/dist/unstable/webmcp/webmcp-host.js +1 -2
- package/dist/utils/Primitive.js +1 -2
- package/dist/utils/createActionButton.js +1 -2
- package/dist/utils/getSelectionMessageId.js +1 -2
- package/dist/utils/hooks/useManagedRef.js +1 -2
- package/dist/utils/hooks/useMediaQuery.js +1 -2
- package/dist/utils/hooks/useOnScrollToBottom.js +1 -2
- package/dist/utils/hooks/useSizeHandle.js +1 -2
- package/dist/utils/isCompositionKey.js +1 -2
- package/dist/utils/smooth/SmoothContext.js +1 -2
- package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +11 -4
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/dist/utils/useIsomorphicLayoutEffect.js +0 -1
- package/package.json +6 -6
- package/src/mcp-apps/McpAppRenderer.composer.test.tsx +111 -0
- package/src/mcp-apps/McpAppRenderer.test.tsx +24 -23
- package/src/mcp-apps/McpAppRenderer.tsx +9 -1
- package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.test.tsx +91 -0
- package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.tsx +54 -0
- package/src/primitives/reasoning/useScrollLock.test.tsx +71 -2
- package/src/primitives/reasoning/useScrollLock.ts +78 -51
- package/src/primitives/threadListItem/ThreadListItemRoot.tsx +4 -1
- package/src/primitives/threadListItem/ThreadListKeyboardNav.test.tsx +25 -0
- package/src/utils/smooth/useSmooth.test.tsx +113 -4
- package/src/utils/smooth/useSmooth.ts +16 -2
- package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.ts +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.32",
|
|
4
4
|
"description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -55,16 +55,16 @@
|
|
|
55
55
|
],
|
|
56
56
|
"sideEffects": false,
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@assistant-ui/core": "^0.3.
|
|
58
|
+
"@assistant-ui/core": "^0.3.32",
|
|
59
59
|
"@assistant-ui/store": "^0.3.22",
|
|
60
60
|
"@assistant-ui/tap": "^0.9.24",
|
|
61
|
-
"assistant-cloud": "^0.2.
|
|
62
|
-
"assistant-stream": "^0.3.
|
|
61
|
+
"assistant-cloud": "^0.2.9",
|
|
62
|
+
"assistant-stream": "^0.3.55",
|
|
63
63
|
"radix-ui": "^1.8.1",
|
|
64
64
|
"react-textarea-autosize": "^8.5.9",
|
|
65
65
|
"safe-content-frame": "^0.0.34",
|
|
66
66
|
"zod": "^4.6.7",
|
|
67
|
-
"zustand": "^5.0.
|
|
67
|
+
"zustand": "^5.0.16"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@types/react": "*",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@assistant-ui/vite": "0.0.21",
|
|
85
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
85
|
+
"@assistant-ui/x-buildutils": "0.0.32",
|
|
86
86
|
"@assistant-ui/x-react18": "0.0.0",
|
|
87
87
|
"@testing-library/react": "^16.3.3",
|
|
88
88
|
"@types/json-schema": "^7.0.15",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, render, waitFor } from "@testing-library/react";
|
|
3
|
+
import { resource, useResource } from "@assistant-ui/tap";
|
|
4
|
+
import {
|
|
5
|
+
AssistantRuntimeProvider,
|
|
6
|
+
MessageByIndexProvider,
|
|
7
|
+
useExternalStoreRuntime,
|
|
8
|
+
type ToolCallMessagePartProps,
|
|
9
|
+
} from "@assistant-ui/core/react";
|
|
10
|
+
import type { ThreadMessageLike } from "@assistant-ui/core";
|
|
11
|
+
import { expect, it, vi } from "vitest";
|
|
12
|
+
import type { AssistantRuntime } from "../index";
|
|
13
|
+
import type { McpAppBridgeHandlers, McpAppsHost } from "./types";
|
|
14
|
+
|
|
15
|
+
const { framePropsMock } = vi.hoisted(() => ({ framePropsMock: vi.fn() }));
|
|
16
|
+
|
|
17
|
+
vi.mock("./app-frame", () => ({
|
|
18
|
+
McpAppFrame: (props: unknown) => {
|
|
19
|
+
framePropsMock(props);
|
|
20
|
+
return null;
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { McpAppRenderer } from "./McpAppRenderer";
|
|
25
|
+
|
|
26
|
+
const Host = resource(({ host }: { host: McpAppsHost }) => host);
|
|
27
|
+
|
|
28
|
+
const host: McpAppsHost = {
|
|
29
|
+
loadResource: vi.fn(async ({ uri }) => ({
|
|
30
|
+
uri,
|
|
31
|
+
mimeType: "text/html;profile=mcp-app" as const,
|
|
32
|
+
html: "",
|
|
33
|
+
})),
|
|
34
|
+
callTool: vi.fn(),
|
|
35
|
+
readResource: vi.fn(),
|
|
36
|
+
listResources: vi.fn(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const part: ToolCallMessagePartProps = {
|
|
40
|
+
type: "tool-call",
|
|
41
|
+
toolCallId: "call-1",
|
|
42
|
+
toolName: "search",
|
|
43
|
+
args: {},
|
|
44
|
+
argsText: "{}",
|
|
45
|
+
status: { type: "complete" },
|
|
46
|
+
mcp: { app: { resourceUri: "ui://example/search" } },
|
|
47
|
+
addResult: vi.fn(),
|
|
48
|
+
resume: vi.fn(),
|
|
49
|
+
respondToApproval: vi.fn(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const messages: ThreadMessageLike[] = [
|
|
53
|
+
{ role: "assistant", content: "Here is the app" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const AppPart = () => {
|
|
57
|
+
const renderer = useResource(McpAppRenderer({ host: Host({ host }) }));
|
|
58
|
+
const Renderer = renderer.render;
|
|
59
|
+
return <Renderer {...part} />;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let runtime!: AssistantRuntime;
|
|
63
|
+
|
|
64
|
+
const App = () => {
|
|
65
|
+
runtime = useExternalStoreRuntime({
|
|
66
|
+
messages,
|
|
67
|
+
convertMessage: (message: ThreadMessageLike) => message,
|
|
68
|
+
onNew: async () => {},
|
|
69
|
+
});
|
|
70
|
+
return (
|
|
71
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
72
|
+
<MessageByIndexProvider index={0}>
|
|
73
|
+
<AppPart />
|
|
74
|
+
</MessageByIndexProvider>
|
|
75
|
+
</AssistantRuntimeProvider>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const renderApp = async () => {
|
|
80
|
+
framePropsMock.mockClear();
|
|
81
|
+
render(<App />);
|
|
82
|
+
await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
|
|
83
|
+
const handlers = framePropsMock.mock.lastCall?.[0]
|
|
84
|
+
.handlers as McpAppBridgeHandlers;
|
|
85
|
+
return handlers;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
it("fills the thread composer from an MCP App message inside a message", async () => {
|
|
89
|
+
const handlers = await renderApp();
|
|
90
|
+
|
|
91
|
+
let result: unknown;
|
|
92
|
+
act(() => {
|
|
93
|
+
result = handlers.sendMessage?.({ text: "Open Grade 5 Math" });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result).toEqual({ ok: true });
|
|
97
|
+
expect(runtime.thread.composer.getState().text).toBe("Open Grade 5 Math");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("keeps the user's draft when an MCP App message arrives", async () => {
|
|
101
|
+
const handlers = await renderApp();
|
|
102
|
+
act(() => runtime.thread.composer.setText("Show my classes"));
|
|
103
|
+
|
|
104
|
+
act(() => {
|
|
105
|
+
handlers.sendMessage?.({ text: "Open Grade 5 Math" });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(runtime.thread.composer.getState().text).toBe(
|
|
109
|
+
"Show my classes Open Grade 5 Math",
|
|
110
|
+
);
|
|
111
|
+
});
|
|
@@ -16,15 +16,22 @@ import type {
|
|
|
16
16
|
McpAppsRemoteHostOptions,
|
|
17
17
|
} from "./types";
|
|
18
18
|
|
|
19
|
-
const { appendMock, framePropsMock } = vi.hoisted(() => ({
|
|
19
|
+
const { appendMock, setTextMock, framePropsMock } = vi.hoisted(() => ({
|
|
20
20
|
appendMock: vi.fn(),
|
|
21
|
+
setTextMock: vi.fn(),
|
|
21
22
|
framePropsMock: vi.fn(),
|
|
22
23
|
}));
|
|
23
24
|
|
|
24
25
|
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
25
26
|
...(await importOriginal()),
|
|
26
27
|
useAui: () => ({
|
|
27
|
-
thread: {
|
|
28
|
+
thread: {
|
|
29
|
+
append: appendMock,
|
|
30
|
+
composer: () => ({
|
|
31
|
+
setText: setTextMock,
|
|
32
|
+
getState: () => ({ text: "" }),
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
28
35
|
}),
|
|
29
36
|
}));
|
|
30
37
|
|
|
@@ -190,6 +197,7 @@ function RemoteHarness({
|
|
|
190
197
|
describe("McpAppRenderer", () => {
|
|
191
198
|
beforeEach(() => {
|
|
192
199
|
appendMock.mockReset();
|
|
200
|
+
setTextMock.mockReset();
|
|
193
201
|
framePropsMock.mockReset();
|
|
194
202
|
});
|
|
195
203
|
|
|
@@ -676,7 +684,7 @@ describe("McpAppRenderer", () => {
|
|
|
676
684
|
expect(listResources).toHaveBeenCalledWith(params);
|
|
677
685
|
});
|
|
678
686
|
|
|
679
|
-
it("
|
|
687
|
+
it("puts MCP Apps text content in the composer without appending", async () => {
|
|
680
688
|
render(<Harness host={loadingHost()} />);
|
|
681
689
|
await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
|
|
682
690
|
const handlers = framePropsMock.mock.lastCall?.[0]
|
|
@@ -692,19 +700,13 @@ describe("McpAppRenderer", () => {
|
|
|
692
700
|
],
|
|
693
701
|
}),
|
|
694
702
|
).toEqual({ ok: true });
|
|
695
|
-
expect(
|
|
696
|
-
|
|
697
|
-
{ type: "text", text: "Open Grade 5 Math" },
|
|
698
|
-
{ type: "text", text: "Period 4" },
|
|
699
|
-
],
|
|
700
|
-
});
|
|
703
|
+
expect(setTextMock).toHaveBeenCalledWith("Open Grade 5 Math\nPeriod 4");
|
|
704
|
+
expect(appendMock).not.toHaveBeenCalled();
|
|
701
705
|
|
|
702
706
|
expect(handlers.sendMessage?.({ text: "typed instead" })).toEqual({
|
|
703
707
|
ok: true,
|
|
704
708
|
});
|
|
705
|
-
expect(
|
|
706
|
-
content: [{ type: "text", text: "typed instead" }],
|
|
707
|
-
});
|
|
709
|
+
expect(setTextMock).toHaveBeenLastCalledWith("typed instead");
|
|
708
710
|
|
|
709
711
|
expect(
|
|
710
712
|
handlers.sendMessage?.({
|
|
@@ -722,7 +724,8 @@ describe("McpAppRenderer", () => {
|
|
|
722
724
|
ok: false,
|
|
723
725
|
reason: "unrecognised params shape",
|
|
724
726
|
});
|
|
725
|
-
expect(
|
|
727
|
+
expect(setTextMock).toHaveBeenCalledTimes(2);
|
|
728
|
+
expect(appendMock).not.toHaveBeenCalled();
|
|
726
729
|
});
|
|
727
730
|
|
|
728
731
|
it("extracts sparse message content without traversing holes", async () => {
|
|
@@ -755,9 +758,8 @@ describe("McpAppRenderer", () => {
|
|
|
755
758
|
}
|
|
756
759
|
|
|
757
760
|
expect(visitedHole).not.toHaveBeenCalled();
|
|
758
|
-
expect(
|
|
759
|
-
|
|
760
|
-
});
|
|
761
|
+
expect(setTextMock).toHaveBeenCalledExactlyOnceWith("sparse prompt");
|
|
762
|
+
expect(appendMock).not.toHaveBeenCalled();
|
|
761
763
|
});
|
|
762
764
|
|
|
763
765
|
it("ignores non-index message content properties after structured clone", async () => {
|
|
@@ -781,13 +783,10 @@ describe("McpAppRenderer", () => {
|
|
|
781
783
|
).toEqual({ ok: true });
|
|
782
784
|
}
|
|
783
785
|
|
|
784
|
-
expect(
|
|
785
|
-
expect(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
expect(appendMock).toHaveBeenNthCalledWith(2, {
|
|
789
|
-
content: [{ type: "text", text: "message text" }],
|
|
790
|
-
});
|
|
786
|
+
expect(setTextMock).toHaveBeenCalledTimes(2);
|
|
787
|
+
expect(setTextMock).toHaveBeenNthCalledWith(1, "message text");
|
|
788
|
+
expect(setTextMock).toHaveBeenNthCalledWith(2, "message text");
|
|
789
|
+
expect(appendMock).not.toHaveBeenCalled();
|
|
791
790
|
});
|
|
792
791
|
|
|
793
792
|
it("uses caller UI handlers and keeps data-plane handlers on the host", async () => {
|
|
@@ -838,6 +837,8 @@ describe("McpAppRenderer", () => {
|
|
|
838
837
|
expect(updateModelContext).toHaveBeenCalledWith({ text: "context" });
|
|
839
838
|
expect(openLink).toHaveBeenCalledWith({ url: "https://example.com" });
|
|
840
839
|
expect(sendMessage).toHaveBeenCalledWith({ text: "hello" });
|
|
840
|
+
expect(setTextMock).not.toHaveBeenCalled();
|
|
841
|
+
expect(appendMock).not.toHaveBeenCalled();
|
|
841
842
|
expect(onInitialized).toHaveBeenCalledOnce();
|
|
842
843
|
expect(host.callTool).toHaveBeenCalledWith({ name: "search" });
|
|
843
844
|
expect(host.readResource).toHaveBeenCalledWith({ uri: "ui://resource" });
|
|
@@ -277,7 +277,15 @@ function InlineRenderer({
|
|
|
277
277
|
if ("reason" in result) {
|
|
278
278
|
return { isError: true, ok: false, reason: result.reason };
|
|
279
279
|
}
|
|
280
|
-
aui.thread.
|
|
280
|
+
const composer = aui.thread.composer();
|
|
281
|
+
composer.setText(
|
|
282
|
+
[
|
|
283
|
+
composer.getState().text,
|
|
284
|
+
result.content.map((part) => part.text).join("\n"),
|
|
285
|
+
]
|
|
286
|
+
.filter((part) => part.trim())
|
|
287
|
+
.join(" "),
|
|
288
|
+
);
|
|
281
289
|
return { ok: true };
|
|
282
290
|
}),
|
|
283
291
|
callTool: (params) =>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createRef, type MouseEvent, type Ref } from "react";
|
|
3
|
+
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { AuiProvider, useAui, useAuiState } from "@assistant-ui/store";
|
|
6
|
+
import { ChainOfThoughtClient } from "@assistant-ui/core/store";
|
|
7
|
+
import { ChainOfThoughtPrimitiveAccordionTrigger } from "./ChainOfThoughtAccordionTrigger";
|
|
8
|
+
|
|
9
|
+
afterEach(cleanup);
|
|
10
|
+
|
|
11
|
+
const Content = () => {
|
|
12
|
+
const collapsed = useAuiState((s) => s.chainOfThought.collapsed);
|
|
13
|
+
return (
|
|
14
|
+
<div data-testid="reasoning" hidden={collapsed}>
|
|
15
|
+
Reasoning details
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const App = ({
|
|
21
|
+
triggerRef,
|
|
22
|
+
...props
|
|
23
|
+
}: ChainOfThoughtPrimitiveAccordionTrigger.Props & {
|
|
24
|
+
triggerRef?: Ref<HTMLButtonElement>;
|
|
25
|
+
}) => {
|
|
26
|
+
const aui = useAui({
|
|
27
|
+
chainOfThought: ChainOfThoughtClient({
|
|
28
|
+
parts: [
|
|
29
|
+
{
|
|
30
|
+
type: "reasoning",
|
|
31
|
+
text: "Reasoning details",
|
|
32
|
+
status: { type: "complete" },
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
getMessagePart: () => {
|
|
36
|
+
throw new Error("Part lookup is not used by the toggle");
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
return (
|
|
41
|
+
<AuiProvider value={aui}>
|
|
42
|
+
<ChainOfThoughtPrimitiveAccordionTrigger {...props} ref={triggerRef} />
|
|
43
|
+
<Content />
|
|
44
|
+
</AuiProvider>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe("ChainOfThoughtPrimitiveAccordionTrigger", () => {
|
|
49
|
+
it.each(["button", "render", "asChild"] as const)(
|
|
50
|
+
"exposes expansion and forwards the ref with %s",
|
|
51
|
+
async (mode) => {
|
|
52
|
+
const ref = createRef<HTMLButtonElement>();
|
|
53
|
+
const { getByRole, getByTestId } = render(
|
|
54
|
+
<App
|
|
55
|
+
triggerRef={ref}
|
|
56
|
+
{...(mode === "render" ? { render: <button /> } : {})}
|
|
57
|
+
{...(mode === "asChild" ? { asChild: true } : {})}
|
|
58
|
+
>
|
|
59
|
+
{mode === "asChild" ? <button>Thinking</button> : "Thinking"}
|
|
60
|
+
</App>,
|
|
61
|
+
);
|
|
62
|
+
const button = getByRole("button", { name: "Thinking" });
|
|
63
|
+
const content = getByTestId("reasoning");
|
|
64
|
+
expect(ref.current).toBe(button);
|
|
65
|
+
expect(content.hidden).toBe(true);
|
|
66
|
+
expect(button.getAttribute("aria-expanded")).toBe("false");
|
|
67
|
+
fireEvent.click(button);
|
|
68
|
+
await waitFor(() => expect(content.hidden).toBe(false));
|
|
69
|
+
expect(button.getAttribute("aria-expanded")).toBe("true");
|
|
70
|
+
fireEvent.click(button);
|
|
71
|
+
await waitFor(() => expect(content.hidden).toBe(true));
|
|
72
|
+
expect(button.getAttribute("aria-expanded")).toBe("false");
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
it("lets the caller veto expansion", () => {
|
|
77
|
+
const onClick = vi.fn((event: MouseEvent) => event.preventDefault());
|
|
78
|
+
const { getByRole, getByTestId } = render(
|
|
79
|
+
<App onClick={onClick}>Thinking</App>,
|
|
80
|
+
);
|
|
81
|
+
fireEvent.click(getByRole("button"));
|
|
82
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
83
|
+
expect(getByTestId("reasoning").hidden).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not expand a disabled trigger", () => {
|
|
87
|
+
const { getByRole, getByTestId } = render(<App disabled>Thinking</App>);
|
|
88
|
+
fireEvent.click(getByRole("button"));
|
|
89
|
+
expect(getByTestId("reasoning").hidden).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ActionButtonElement } from "../../utils/createActionButton";
|
|
4
|
+
import { type ComponentPropsWithoutRef, forwardRef, useCallback } from "react";
|
|
5
|
+
import { useAuiState, useAui } from "@assistant-ui/store";
|
|
6
|
+
import { composeEventHandlers } from "radix-ui/internal";
|
|
7
|
+
import { Primitive } from "../../utils/Primitive";
|
|
8
|
+
|
|
9
|
+
export namespace ChainOfThoughtPrimitiveAccordionTrigger {
|
|
10
|
+
export type Element = ActionButtonElement;
|
|
11
|
+
/**
|
|
12
|
+
* Props for the ChainOfThoughtPrimitive.AccordionTrigger component.
|
|
13
|
+
* Inherits all button element props and action button functionality.
|
|
14
|
+
*/
|
|
15
|
+
export type Props = ComponentPropsWithoutRef<typeof Primitive.button>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A button component that toggles the collapsed state of the chain of thought accordion.
|
|
20
|
+
*
|
|
21
|
+
* This component automatically handles the toggle functionality, expanding or collapsing
|
|
22
|
+
* the chain of thought parts when clicked.
|
|
23
|
+
* Exposes the current disclosure state through `aria-expanded`.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <ChainOfThoughtPrimitive.AccordionTrigger>
|
|
28
|
+
* Toggle Reasoning
|
|
29
|
+
* </ChainOfThoughtPrimitive.AccordionTrigger>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const ChainOfThoughtPrimitiveAccordionTrigger = forwardRef<
|
|
33
|
+
ChainOfThoughtPrimitiveAccordionTrigger.Element,
|
|
34
|
+
ChainOfThoughtPrimitiveAccordionTrigger.Props
|
|
35
|
+
>(({ onClick, ...props }, ref) => {
|
|
36
|
+
const aui = useAui();
|
|
37
|
+
const collapsed = useAuiState((s) => s.chainOfThought.collapsed);
|
|
38
|
+
const toggle = useCallback(() => {
|
|
39
|
+
aui.chainOfThought.setCollapsed(!collapsed);
|
|
40
|
+
}, [aui, collapsed]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Primitive.button
|
|
44
|
+
type="button"
|
|
45
|
+
{...props}
|
|
46
|
+
ref={ref}
|
|
47
|
+
aria-expanded={!collapsed}
|
|
48
|
+
onClick={composeEventHandlers(onClick, toggle)}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
ChainOfThoughtPrimitiveAccordionTrigger.displayName =
|
|
54
|
+
"ChainOfThoughtPrimitive.AccordionTrigger";
|
|
@@ -31,9 +31,29 @@ function stubWidths(
|
|
|
31
31
|
function lockWithin(container: HTMLElement) {
|
|
32
32
|
const animated = document.createElement("div");
|
|
33
33
|
container.appendChild(animated);
|
|
34
|
+
return createLock(animated).lock;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createLock(animated: HTMLElement) {
|
|
34
38
|
const ref = { current: animated };
|
|
35
|
-
const { result } = renderHook(() => useScrollLock(ref, 200));
|
|
36
|
-
return result.current;
|
|
39
|
+
const { result, unmount } = renderHook(() => useScrollLock(ref, 200));
|
|
40
|
+
return { lock: result.current, dispose: unmount };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createSharedScroller() {
|
|
44
|
+
const scroller = document.createElement("div");
|
|
45
|
+
scroller.style.overflowY = "auto";
|
|
46
|
+
scroller.style.borderWidth = "0px";
|
|
47
|
+
scroller.style.paddingRight = "4px";
|
|
48
|
+
document.body.appendChild(scroller);
|
|
49
|
+
Object.defineProperty(scroller, "offsetWidth", { value: 306 });
|
|
50
|
+
Object.defineProperty(scroller, "clientWidth", {
|
|
51
|
+
get: () => (scroller.style.scrollbarWidth === "none" ? 306 : 300),
|
|
52
|
+
});
|
|
53
|
+
const first = document.createElement("div");
|
|
54
|
+
const second = document.createElement("div");
|
|
55
|
+
scroller.append(first, second);
|
|
56
|
+
return { scroller, first: createLock(first), second: createLock(second) };
|
|
37
57
|
}
|
|
38
58
|
|
|
39
59
|
describe("useScrollLock", () => {
|
|
@@ -115,6 +135,55 @@ describe("useScrollLock", () => {
|
|
|
115
135
|
expect(scroller.style.scrollbarWidth).toBe("");
|
|
116
136
|
});
|
|
117
137
|
|
|
138
|
+
it("keeps overlapping animations locked until the last one finishes", () => {
|
|
139
|
+
vi.useFakeTimers();
|
|
140
|
+
const { scroller, first, second } = createSharedScroller();
|
|
141
|
+
scroller.scrollTop = 120;
|
|
142
|
+
|
|
143
|
+
first.lock();
|
|
144
|
+
vi.advanceTimersByTime(100);
|
|
145
|
+
second.lock();
|
|
146
|
+
expect(scroller.style.paddingRight).toBe("10px");
|
|
147
|
+
|
|
148
|
+
vi.advanceTimersByTime(100);
|
|
149
|
+
expect(scroller.style.scrollbarWidth).toBe("none");
|
|
150
|
+
expect(scroller.style.paddingRight).toBe("10px");
|
|
151
|
+
scroller.scrollTop = 90;
|
|
152
|
+
scroller.dispatchEvent(new Event("scroll"));
|
|
153
|
+
expect(scroller.scrollTop).toBe(120);
|
|
154
|
+
|
|
155
|
+
vi.advanceTimersByTime(100);
|
|
156
|
+
expect(scroller.style.scrollbarWidth).toBe("");
|
|
157
|
+
expect(scroller.style.paddingRight).toBe("4px");
|
|
158
|
+
scroller.scrollTop = 90;
|
|
159
|
+
scroller.dispatchEvent(new Event("scroll"));
|
|
160
|
+
expect(scroller.scrollTop).toBe(90);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it.each(["first", "second"] as const)(
|
|
164
|
+
"preserves a restarted shared lock when the %s owner is disposed first",
|
|
165
|
+
(disposedFirst) => {
|
|
166
|
+
vi.useFakeTimers();
|
|
167
|
+
const shared = createSharedScroller();
|
|
168
|
+
const { scroller, first, second } = shared;
|
|
169
|
+
first.lock();
|
|
170
|
+
second.lock();
|
|
171
|
+
vi.advanceTimersByTime(100);
|
|
172
|
+
first.lock();
|
|
173
|
+
|
|
174
|
+
shared[disposedFirst].dispose();
|
|
175
|
+
expect(scroller.style.scrollbarWidth).toBe("none");
|
|
176
|
+
expect(scroller.style.paddingRight).toBe("10px");
|
|
177
|
+
|
|
178
|
+
shared[disposedFirst === "first" ? "second" : "first"].dispose();
|
|
179
|
+
expect(scroller.style.scrollbarWidth).toBe("");
|
|
180
|
+
expect(scroller.style.paddingRight).toBe("4px");
|
|
181
|
+
scroller.style.scrollbarWidth = "thin";
|
|
182
|
+
vi.advanceTimersByTime(200);
|
|
183
|
+
expect(scroller.style.scrollbarWidth).toBe("thin");
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
118
187
|
it("leaves the padding alone when no scrollbar takes space", () => {
|
|
119
188
|
const scroller = document.createElement("div");
|
|
120
189
|
scroller.style.overflowY = "auto";
|
|
@@ -12,6 +12,74 @@ const findScrollableAncestor = (element: HTMLElement | null) => {
|
|
|
12
12
|
return null;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
const scrollLocks = new WeakMap<
|
|
16
|
+
HTMLElement,
|
|
17
|
+
{ count: number; restore: () => void }
|
|
18
|
+
>();
|
|
19
|
+
|
|
20
|
+
const acquireScrollLock = (scrollContainer: HTMLElement) => {
|
|
21
|
+
const existing = scrollLocks.get(scrollContainer);
|
|
22
|
+
if (existing) {
|
|
23
|
+
existing.count++;
|
|
24
|
+
return existing;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const scrollPosition = scrollContainer.scrollTop;
|
|
28
|
+
const scrollbarWidth = scrollContainer.style.scrollbarWidth;
|
|
29
|
+
|
|
30
|
+
// Hiding the scrollbar collapses its gutter on classic scrollbars, which
|
|
31
|
+
// shifts centered content horizontally; compensate with padding on the
|
|
32
|
+
// side the scrollbar occupies (the left side in RTL).
|
|
33
|
+
const computed = getComputedStyle(scrollContainer);
|
|
34
|
+
const paddingSide =
|
|
35
|
+
computed.direction === "rtl" ? "paddingLeft" : "paddingRight";
|
|
36
|
+
const previousPadding = scrollContainer.style[paddingSide];
|
|
37
|
+
const elementScrollbarSize =
|
|
38
|
+
scrollContainer.offsetWidth -
|
|
39
|
+
scrollContainer.clientWidth -
|
|
40
|
+
parseFloat(computed.borderLeftWidth) -
|
|
41
|
+
parseFloat(computed.borderRightWidth);
|
|
42
|
+
// A root element's offsetWidth already excludes the viewport scrollbar, so
|
|
43
|
+
// the element formula reports zero once the scroll propagates to the
|
|
44
|
+
// viewport, and only then does the viewport measure apply. A body that
|
|
45
|
+
// scrolls in its own right is measured by the element formula like any
|
|
46
|
+
// other scroller, whether or not the viewport is scrolling too.
|
|
47
|
+
const ownerDocument = scrollContainer.ownerDocument;
|
|
48
|
+
const isRootScroller =
|
|
49
|
+
scrollContainer === ownerDocument.documentElement ||
|
|
50
|
+
scrollContainer === ownerDocument.body;
|
|
51
|
+
const scrollbarSize =
|
|
52
|
+
isRootScroller && elementScrollbarSize <= 0
|
|
53
|
+
? (ownerDocument.defaultView?.innerWidth ?? 0) -
|
|
54
|
+
ownerDocument.documentElement.clientWidth
|
|
55
|
+
: elementScrollbarSize;
|
|
56
|
+
|
|
57
|
+
scrollContainer.style.scrollbarWidth = "none";
|
|
58
|
+
if (scrollbarSize > 0) {
|
|
59
|
+
scrollContainer.style[paddingSide] = `${
|
|
60
|
+
parseFloat(computed[paddingSide]) + scrollbarSize
|
|
61
|
+
}px`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const restoreStyles = () => {
|
|
65
|
+
scrollContainer.style.scrollbarWidth = scrollbarWidth;
|
|
66
|
+
scrollContainer.style[paddingSide] = previousPadding;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const resetPosition = () => (scrollContainer.scrollTop = scrollPosition);
|
|
70
|
+
scrollContainer.addEventListener("scroll", resetPosition);
|
|
71
|
+
|
|
72
|
+
const lock = {
|
|
73
|
+
count: 1,
|
|
74
|
+
restore: () => {
|
|
75
|
+
scrollContainer.removeEventListener("scroll", resetPosition);
|
|
76
|
+
restoreStyles();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
scrollLocks.set(scrollContainer, lock);
|
|
80
|
+
return lock;
|
|
81
|
+
};
|
|
82
|
+
|
|
15
83
|
/**
|
|
16
84
|
* Locks scroll position during collapsible/height animations and hides scrollbar.
|
|
17
85
|
*
|
|
@@ -59,61 +127,20 @@ export const useScrollLock = <T extends HTMLElement = HTMLElement>(
|
|
|
59
127
|
return;
|
|
60
128
|
}
|
|
61
129
|
|
|
62
|
-
const
|
|
63
|
-
const scrollbarWidth = scrollContainer.style.scrollbarWidth;
|
|
64
|
-
|
|
65
|
-
// Hiding the scrollbar collapses its gutter on classic scrollbars, which
|
|
66
|
-
// shifts centered content horizontally; compensate with padding on the
|
|
67
|
-
// side the scrollbar occupies (the left side in RTL).
|
|
68
|
-
const computed = getComputedStyle(scrollContainer);
|
|
69
|
-
const paddingSide =
|
|
70
|
-
computed.direction === "rtl" ? "paddingLeft" : "paddingRight";
|
|
71
|
-
const previousPadding = scrollContainer.style[paddingSide];
|
|
72
|
-
const elementScrollbarSize =
|
|
73
|
-
scrollContainer.offsetWidth -
|
|
74
|
-
scrollContainer.clientWidth -
|
|
75
|
-
parseFloat(computed.borderLeftWidth) -
|
|
76
|
-
parseFloat(computed.borderRightWidth);
|
|
77
|
-
// A root element's offsetWidth already excludes the viewport scrollbar, so
|
|
78
|
-
// the element formula reports zero once the scroll propagates to the
|
|
79
|
-
// viewport, and only then does the viewport measure apply. A body that
|
|
80
|
-
// scrolls in its own right is measured by the element formula like any
|
|
81
|
-
// other scroller, whether or not the viewport is scrolling too.
|
|
82
|
-
const ownerDocument = scrollContainer.ownerDocument;
|
|
83
|
-
const isRootScroller =
|
|
84
|
-
scrollContainer === ownerDocument.documentElement ||
|
|
85
|
-
scrollContainer === ownerDocument.body;
|
|
86
|
-
const scrollbarSize =
|
|
87
|
-
isRootScroller && elementScrollbarSize <= 0
|
|
88
|
-
? (ownerDocument.defaultView?.innerWidth ?? 0) -
|
|
89
|
-
ownerDocument.documentElement.clientWidth
|
|
90
|
-
: elementScrollbarSize;
|
|
91
|
-
|
|
92
|
-
scrollContainer.style.scrollbarWidth = "none";
|
|
93
|
-
if (scrollbarSize > 0) {
|
|
94
|
-
scrollContainer.style[paddingSide] = `${
|
|
95
|
-
parseFloat(computed[paddingSide]) + scrollbarSize
|
|
96
|
-
}px`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const restoreStyles = () => {
|
|
100
|
-
scrollContainer.style.scrollbarWidth = scrollbarWidth;
|
|
101
|
-
scrollContainer.style[paddingSide] = previousPadding;
|
|
102
|
-
};
|
|
130
|
+
const lock = acquireScrollLock(scrollContainer);
|
|
103
131
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
scrollContainer.removeEventListener("scroll", resetPosition);
|
|
109
|
-
restoreStyles();
|
|
110
|
-
cleanupRef.current = null;
|
|
111
|
-
}, animationDuration);
|
|
132
|
+
const timeoutId = setTimeout(
|
|
133
|
+
() => cleanupRef.current?.(),
|
|
134
|
+
animationDuration,
|
|
135
|
+
);
|
|
112
136
|
|
|
113
137
|
cleanupRef.current = () => {
|
|
114
138
|
clearTimeout(timeoutId);
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
if (--lock.count === 0) {
|
|
140
|
+
lock.restore();
|
|
141
|
+
scrollLocks.delete(scrollContainer);
|
|
142
|
+
}
|
|
143
|
+
cleanupRef.current = null;
|
|
117
144
|
};
|
|
118
145
|
}, [animationDuration, animatedElementRef]);
|
|
119
146
|
|
|
@@ -51,7 +51,10 @@ export const ThreadListItemPrimitiveRoot = forwardRef<
|
|
|
51
51
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
52
52
|
const triggers = getItems()
|
|
53
53
|
.map((item) => item.ref.current)
|
|
54
|
-
.filter(
|
|
54
|
+
.filter(
|
|
55
|
+
(node): node is HTMLButtonElement =>
|
|
56
|
+
node !== null && !node.disabled,
|
|
57
|
+
);
|
|
55
58
|
const next =
|
|
56
59
|
triggers[
|
|
57
60
|
triggers.indexOf(trigger) + (event.key === "ArrowDown" ? 1 : -1)
|