@assistant-ui/react-native 0.1.42 → 0.1.44
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/context/AssistantContext.d.ts +2 -2
- package/dist/context/AssistantContext.d.ts.map +1 -0
- package/dist/context/providers/AttachmentByIndexProvider.d.ts +2 -2
- package/dist/context/providers/AttachmentByIndexProvider.d.ts.map +1 -0
- package/dist/context/providers/RuntimeAdapterProvider.d.ts +2 -2
- package/dist/context/providers/RuntimeAdapterProvider.d.ts.map +1 -0
- package/dist/context/providers/ThreadListItemRuntimeProvider.d.ts +2 -2
- package/dist/context/providers/ThreadListItemRuntimeProvider.d.ts.map +1 -0
- package/dist/index.d.ts +54 -22
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -4
- package/dist/internal.d.ts +4 -3
- package/dist/internal.d.ts.map +1 -0
- package/dist/primitives/actionBar/ActionBarCopy.d.ts +9 -11
- package/dist/primitives/actionBar/ActionBarCopy.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarCopy.js +23 -19
- package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarEdit.d.ts +7 -7
- package/dist/primitives/actionBar/ActionBarEdit.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarEdit.js +21 -11
- package/dist/primitives/actionBar/ActionBarEdit.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts +8 -10
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +35 -17
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts +8 -10
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +35 -17
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarReload.d.ts +7 -7
- package/dist/primitives/actionBar/ActionBarReload.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarReload.js +21 -11
- package/dist/primitives/actionBar/ActionBarReload.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarSpeak.d.ts +12 -0
- package/dist/primitives/actionBar/ActionBarSpeak.d.ts.map +1 -0
- package/dist/primitives/actionBar/ActionBarSpeak.js +61 -0
- package/dist/primitives/actionBar/ActionBarSpeak.js.map +1 -0
- package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts +12 -0
- package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -0
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js +61 -0
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -0
- package/dist/primitives/actionBar.d.ts +7 -10
- package/dist/primitives/actionBar.d.ts.map +1 -1
- package/dist/primitives/actionBar.js +6 -2
- package/dist/primitives/actionBar.js.map +1 -1
- package/dist/primitives/attachment/AttachmentName.d.ts +4 -7
- package/dist/primitives/attachment/AttachmentName.d.ts.map +1 -1
- package/dist/primitives/attachment/AttachmentRemove.d.ts +4 -7
- package/dist/primitives/attachment/AttachmentRemove.d.ts.map +1 -1
- package/dist/primitives/attachment/AttachmentRoot.d.ts +5 -8
- package/dist/primitives/attachment/AttachmentRoot.d.ts.map +1 -1
- package/dist/primitives/attachment/AttachmentThumb.d.ts +4 -7
- package/dist/primitives/attachment/AttachmentThumb.d.ts.map +1 -1
- package/dist/primitives/attachment.d.ts +4 -9
- package/dist/primitives/attachment.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerCount.d.ts +3 -6
- package/dist/primitives/branchPicker/BranchPickerCount.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerNext.d.ts +7 -7
- package/dist/primitives/branchPicker/BranchPickerNext.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerNext.js +21 -11
- package/dist/primitives/branchPicker/BranchPickerNext.js.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerNumber.d.ts +3 -6
- package/dist/primitives/branchPicker/BranchPickerNumber.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts +7 -7
- package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerPrevious.js +21 -11
- package/dist/primitives/branchPicker/BranchPickerPrevious.js.map +1 -1
- package/dist/primitives/branchPicker.d.ts +4 -9
- package/dist/primitives/branchPicker.d.ts.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts +4 -7
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtRoot.d.ts +5 -8
- package/dist/primitives/chainOfThought/ChainOfThoughtRoot.d.ts.map +1 -1
- package/dist/primitives/chainOfThought.d.ts +3 -8
- package/dist/primitives/chainOfThought.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAddAttachment.d.ts +4 -7
- package/dist/primitives/composer/ComposerAddAttachment.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachments.d.ts +5 -8
- package/dist/primitives/composer/ComposerAttachments.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerCancel.d.ts +4 -7
- package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerIf.d.ts +2 -2
- package/dist/primitives/composer/ComposerIf.d.ts.map +1 -0
- package/dist/primitives/composer/ComposerInput.d.ts +10 -13
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +1 -1
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerQueue.d.ts +2 -2
- package/dist/primitives/composer/ComposerQueue.d.ts.map +1 -0
- package/dist/primitives/composer/ComposerQuote.d.ts +5 -8
- package/dist/primitives/composer/ComposerQuote.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerQuoteDismiss.d.ts +4 -7
- package/dist/primitives/composer/ComposerQuoteDismiss.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerQuoteText.d.ts +5 -8
- package/dist/primitives/composer/ComposerQuoteText.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerRoot.d.ts +5 -8
- package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerSend.d.ts +4 -7
- package/dist/primitives/composer/ComposerSend.d.ts.map +1 -1
- package/dist/primitives/composer.d.ts +11 -16
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/error/ErrorMessage.d.ts +7 -10
- package/dist/primitives/error/ErrorMessage.d.ts.map +1 -1
- package/dist/primitives/error/ErrorRoot.d.ts +7 -10
- package/dist/primitives/error/ErrorRoot.d.ts.map +1 -1
- package/dist/primitives/error/index.d.ts +2 -7
- package/dist/primitives/error/index.d.ts.map +1 -1
- package/dist/primitives/message/MessageAttachments.d.ts +5 -8
- package/dist/primitives/message/MessageAttachments.d.ts.map +1 -1
- package/dist/primitives/message/MessageContent.d.ts +47 -49
- package/dist/primitives/message/MessageContent.d.ts.map +1 -1
- package/dist/primitives/message/MessageContent.js +27 -21
- package/dist/primitives/message/MessageContent.js.map +1 -1
- package/dist/primitives/message/MessageIf.d.ts +8 -11
- package/dist/primitives/message/MessageIf.d.ts.map +1 -1
- package/dist/primitives/message/MessageParts.d.ts +6 -8
- package/dist/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/primitives/message/MessageRoot.d.ts +5 -8
- package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
- package/dist/primitives/message.d.ts +6 -11
- package/dist/primitives/message.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartImage.d.ts +9 -0
- package/dist/primitives/messagePart/MessagePartImage.d.ts.map +1 -0
- package/dist/primitives/messagePart/MessagePartImage.js +39 -0
- package/dist/primitives/messagePart/MessagePartImage.js.map +1 -0
- package/dist/primitives/messagePart/MessagePartInProgress.d.ts +2 -0
- package/dist/primitives/messagePart/MessagePartInProgress.d.ts.map +1 -0
- package/dist/primitives/messagePart/MessagePartInProgress.js +2 -0
- package/dist/primitives/messagePart/MessagePartText.d.ts +9 -0
- package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -0
- package/dist/primitives/messagePart/MessagePartText.js +32 -0
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -0
- package/dist/primitives/messagePart.d.ts +5 -0
- package/dist/primitives/messagePart.d.ts.map +1 -0
- package/dist/primitives/messagePart.js +16 -0
- package/dist/primitives/messagePart.js.map +1 -0
- package/dist/primitives/queueItem/QueueItemRemove.d.ts +4 -7
- package/dist/primitives/queueItem/QueueItemRemove.d.ts.map +1 -1
- package/dist/primitives/queueItem/QueueItemSteer.d.ts +4 -7
- package/dist/primitives/queueItem/QueueItemSteer.d.ts.map +1 -1
- package/dist/primitives/queueItem/QueueItemText.d.ts +5 -8
- package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -1
- package/dist/primitives/queueItem.d.ts +3 -8
- package/dist/primitives/queueItem.d.ts.map +1 -1
- package/dist/primitives/suggestion/SuggestionDescription.d.ts +5 -8
- package/dist/primitives/suggestion/SuggestionDescription.d.ts.map +1 -1
- package/dist/primitives/suggestion/SuggestionTitle.d.ts +5 -8
- package/dist/primitives/suggestion/SuggestionTitle.d.ts.map +1 -1
- package/dist/primitives/suggestion/SuggestionTrigger.d.ts +15 -18
- package/dist/primitives/suggestion/SuggestionTrigger.d.ts.map +1 -1
- package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
- package/dist/primitives/suggestion.d.ts +3 -8
- package/dist/primitives/suggestion.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadEmpty.d.ts +4 -7
- package/dist/primitives/thread/ThreadEmpty.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadIf.d.ts +6 -9
- package/dist/primitives/thread/ThreadIf.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadMessages.d.ts +41 -37
- package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadMessages.js +123 -72
- package/dist/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/primitives/thread/ThreadRoot.d.ts +5 -8
- package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadSuggestion.d.ts +17 -20
- package/dist/primitives/thread/ThreadSuggestion.d.ts.map +1 -1
- package/dist/primitives/thread.d.ts +8 -11
- package/dist/primitives/thread.d.ts.map +1 -1
- package/dist/primitives/threadList/ThreadListItems.d.ts +8 -11
- package/dist/primitives/threadList/ThreadListItems.d.ts.map +1 -1
- package/dist/primitives/threadList/ThreadListItems.js +1 -1
- package/dist/primitives/threadList/ThreadListNew.d.ts +7 -10
- package/dist/primitives/threadList/ThreadListNew.d.ts.map +1 -1
- package/dist/primitives/threadList/ThreadListNew.js +21 -17
- package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
- package/dist/primitives/threadList/ThreadListRoot.d.ts +5 -8
- package/dist/primitives/threadList/ThreadListRoot.d.ts.map +1 -1
- package/dist/primitives/threadList.d.ts +3 -8
- package/dist/primitives/threadList.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts +4 -7
- package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts +4 -7
- package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts +5 -8
- package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts +7 -10
- package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.js +21 -17
- package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts +4 -7
- package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts.map +1 -1
- package/dist/primitives/threadListItem.d.ts +6 -11
- package/dist/primitives/threadListItem.d.ts.map +1 -1
- package/dist/runtimes/useLocalRuntime.d.ts +2 -2
- package/dist/runtimes/useLocalRuntime.d.ts.map +1 -0
- package/dist/runtimes/useRemoteThreadListRuntime.d.ts +2 -2
- package/dist/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -0
- package/package.json +13 -12
- package/src/index.ts +91 -1
- package/src/primitives/accessibilityRole.test.tsx +20 -0
- package/src/primitives/actionBar/ActionBar.test.tsx +375 -0
- package/src/primitives/actionBar/ActionBarCopy.tsx +8 -3
- package/src/primitives/actionBar/ActionBarEdit.tsx +16 -4
- package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +18 -4
- package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +18 -4
- package/src/primitives/actionBar/ActionBarReload.tsx +16 -4
- package/src/primitives/actionBar/ActionBarSpeak.tsx +42 -0
- package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +42 -0
- package/src/primitives/actionBar.ts +8 -0
- package/src/primitives/branchPicker/BranchPickerNext.test.tsx +52 -3
- package/src/primitives/branchPicker/BranchPickerNext.tsx +16 -4
- package/src/primitives/branchPicker/BranchPickerPrevious.test.tsx +52 -6
- package/src/primitives/branchPicker/BranchPickerPrevious.tsx +16 -4
- package/src/primitives/composer/ComposerInput.test.tsx +17 -1
- package/src/primitives/composer/ComposerInput.tsx +6 -1
- package/src/primitives/message/MessageContent.test.tsx +64 -3
- package/src/primitives/message/MessageContent.tsx +5 -3
- package/src/primitives/messagePart/MessagePartImage.test.tsx +91 -0
- package/src/primitives/messagePart/MessagePartImage.tsx +33 -0
- package/src/primitives/messagePart/MessagePartInProgress.ts +1 -0
- package/src/primitives/messagePart/MessagePartText.test.tsx +80 -0
- package/src/primitives/messagePart/MessagePartText.tsx +36 -0
- package/src/primitives/messagePart.ts +10 -0
- package/src/primitives/thread/ThreadMessages.test.tsx +190 -6
- package/src/primitives/thread/ThreadMessages.tsx +68 -1
- package/src/primitives/threadList/ThreadListNew.test.tsx +35 -8
- package/src/primitives/threadList/ThreadListNew.tsx +4 -1
- package/src/primitives/threadListItem/ThreadListItemTrigger.test.tsx +32 -6
- package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +4 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import type { ThreadMessageLike } from "@assistant-ui/core";
|
|
5
|
+
import {
|
|
6
|
+
AssistantRuntimeProvider,
|
|
7
|
+
MessageByIndexProvider,
|
|
8
|
+
PartByIndexProvider,
|
|
9
|
+
useExternalStoreRuntime,
|
|
10
|
+
} from "@assistant-ui/core/react";
|
|
11
|
+
import { MessagePartPrimitiveText } from "./MessagePartText";
|
|
12
|
+
|
|
13
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
|
+
|
|
15
|
+
const messages: ThreadMessageLike[] = [
|
|
16
|
+
{
|
|
17
|
+
role: "assistant",
|
|
18
|
+
content: [
|
|
19
|
+
{ type: "text", text: "Answer" },
|
|
20
|
+
{ type: "reasoning", text: "Thinking" },
|
|
21
|
+
{ type: "image", image: "https://example.com/image.png" },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const App = ({ partIndex }: { partIndex: number }) => {
|
|
27
|
+
const runtime = useExternalStoreRuntime({
|
|
28
|
+
messages,
|
|
29
|
+
convertMessage: (message) => message,
|
|
30
|
+
onNew: async () => {
|
|
31
|
+
throw new Error("This thread is read-only");
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
37
|
+
<MessageByIndexProvider index={0}>
|
|
38
|
+
<PartByIndexProvider index={partIndex}>
|
|
39
|
+
<MessagePartPrimitiveText testID="text" />
|
|
40
|
+
</PartByIndexProvider>
|
|
41
|
+
</MessageByIndexProvider>
|
|
42
|
+
</AssistantRuntimeProvider>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe("MessagePartPrimitiveText", () => {
|
|
47
|
+
let container: HTMLDivElement;
|
|
48
|
+
let root: Root;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
container = document.createElement("div");
|
|
52
|
+
document.body.appendChild(container);
|
|
53
|
+
root = createRoot(container);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(async () => {
|
|
57
|
+
await act(async () => root.unmount());
|
|
58
|
+
container.remove();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const mount = async (partIndex: number) => {
|
|
62
|
+
await act(async () => root.render(<App partIndex={partIndex} />));
|
|
63
|
+
return container.querySelector('[data-testid="text"]') as HTMLElement;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
it("renders text from the current text part", async () => {
|
|
67
|
+
const text = await mount(0);
|
|
68
|
+
expect(text.textContent).toBe("Answer");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("renders text from the current reasoning part", async () => {
|
|
72
|
+
const text = await mount(1);
|
|
73
|
+
expect(text.textContent).toBe("Thinking");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("renders empty text for a non-text part", async () => {
|
|
77
|
+
const text = await mount(2);
|
|
78
|
+
expect(text.textContent).toBe("");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentRef,
|
|
3
|
+
type ForwardRefExoticComponent,
|
|
4
|
+
forwardRef,
|
|
5
|
+
type RefAttributes,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { Text, type TextProps } from "react-native";
|
|
8
|
+
import { useAuiState } from "@assistant-ui/store";
|
|
9
|
+
|
|
10
|
+
export namespace MessagePartPrimitiveText {
|
|
11
|
+
export type Element = ComponentRef<typeof Text>;
|
|
12
|
+
export type Props = Omit<TextProps, "children">;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const MessagePartPrimitiveText: ForwardRefExoticComponent<
|
|
16
|
+
MessagePartPrimitiveText.Props &
|
|
17
|
+
RefAttributes<MessagePartPrimitiveText.Element>
|
|
18
|
+
> = forwardRef<
|
|
19
|
+
MessagePartPrimitiveText.Element,
|
|
20
|
+
MessagePartPrimitiveText.Props
|
|
21
|
+
>((props, forwardedRef) => {
|
|
22
|
+
const text = useAuiState((s) => {
|
|
23
|
+
const part = s.part;
|
|
24
|
+
return part.type === "text" || part.type === "reasoning" ? part.text : "";
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Text {...props} ref={forwardedRef}>
|
|
29
|
+
{text}
|
|
30
|
+
</Text>
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
MessagePartPrimitiveText.displayName = "MessagePartPrimitive.Text";
|
|
35
|
+
|
|
36
|
+
export type MessagePartTextProps = MessagePartPrimitiveText.Props;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export {
|
|
2
|
+
MessagePartPrimitiveText as Text,
|
|
3
|
+
type MessagePartTextProps as TextProps,
|
|
4
|
+
} from "./messagePart/MessagePartText";
|
|
5
|
+
export {
|
|
6
|
+
MessagePartPrimitiveImage as Image,
|
|
7
|
+
type MessagePartImageProps as ImageProps,
|
|
8
|
+
} from "./messagePart/MessagePartImage";
|
|
9
|
+
export { MessagePartPrimitiveInProgress as InProgress } from "./messagePart/MessagePartInProgress";
|
|
10
|
+
export { PartPrimitiveMessages as Messages } from "@assistant-ui/core/react";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { act, createRef } from "react";
|
|
2
2
|
import { createRoot, type Root } from "react-dom/client";
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import type { FlatList } from "react-native";
|
|
4
|
+
import type { FlatList, FlatListProps } from "react-native";
|
|
5
5
|
import type { ThreadMessage } from "@assistant-ui/core";
|
|
6
|
+
import type { MessageState } from "@assistant-ui/core/store";
|
|
6
7
|
import { ThreadMessages, ThreadMessagesFlatList } from "./ThreadMessages";
|
|
7
8
|
|
|
8
9
|
type Msg = { id: string; role: string };
|
|
@@ -12,7 +13,7 @@ const h = vi.hoisted(() => ({
|
|
|
12
13
|
thread: { messages: [] as Msg[] },
|
|
13
14
|
message: { role: "user" as string, composer: { isEditing: false } },
|
|
14
15
|
},
|
|
15
|
-
itemState: { role: "user" } as { role: string },
|
|
16
|
+
itemState: { role: "user" } as { role: string; parts?: unknown[] },
|
|
16
17
|
events: {} as Record<string, Set<() => void>>,
|
|
17
18
|
flatListProps: null as Record<string, unknown> | null,
|
|
18
19
|
scrollToOffset: vi.fn(),
|
|
@@ -154,6 +155,10 @@ describe("ThreadMessages", () => {
|
|
|
154
155
|
onContentSizeChange?: (width: number, height: number) => void;
|
|
155
156
|
onLayout?: (event: unknown) => void;
|
|
156
157
|
onScroll?: (event: unknown) => void;
|
|
158
|
+
onStartReached?: NonNullable<
|
|
159
|
+
FlatListProps<ThreadMessage>["onStartReached"]
|
|
160
|
+
>;
|
|
161
|
+
onStartReachedThreshold?: number;
|
|
157
162
|
scrollEventThrottle?: number;
|
|
158
163
|
} | null;
|
|
159
164
|
if (!props) throw new Error("FlatList was not rendered");
|
|
@@ -299,13 +304,15 @@ describe("ThreadMessages", () => {
|
|
|
299
304
|
describe("children mode", () => {
|
|
300
305
|
it("renders via the children render prop", async () => {
|
|
301
306
|
h.state.thread.messages = [{ id: "1", role: "user" }];
|
|
302
|
-
h.itemState = { role: "user" };
|
|
303
|
-
const children = vi.fn(({ message }: { message:
|
|
304
|
-
<span data-testid="child">
|
|
307
|
+
h.itemState = { role: "user", parts: [] };
|
|
308
|
+
const children = vi.fn(({ message }: { message: MessageState }) => (
|
|
309
|
+
<span data-testid="child">
|
|
310
|
+
child:{message.role}:{message.parts.length}
|
|
311
|
+
</span>
|
|
305
312
|
));
|
|
306
313
|
await mount({ children });
|
|
307
314
|
const el = container.querySelector('[data-testid="child"]');
|
|
308
|
-
expect(el?.textContent).toBe("child:user");
|
|
315
|
+
expect(el?.textContent).toBe("child:user:0");
|
|
309
316
|
expect(children).toHaveBeenCalled();
|
|
310
317
|
});
|
|
311
318
|
});
|
|
@@ -392,6 +399,183 @@ describe("ThreadMessages", () => {
|
|
|
392
399
|
expect(h.scrollToOffset).not.toHaveBeenCalled();
|
|
393
400
|
});
|
|
394
401
|
|
|
402
|
+
describe("MessagesFlatList history", () => {
|
|
403
|
+
it("wires loadMore only while history can load more", async () => {
|
|
404
|
+
const loadMore = vi.fn();
|
|
405
|
+
|
|
406
|
+
await mountFlatList({
|
|
407
|
+
components: messageComponents,
|
|
408
|
+
history: { hasMore: true, isLoadingMore: false, loadMore },
|
|
409
|
+
});
|
|
410
|
+
getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
|
|
411
|
+
expect(loadMore).toHaveBeenCalledOnce();
|
|
412
|
+
|
|
413
|
+
await mountFlatList({
|
|
414
|
+
components: messageComponents,
|
|
415
|
+
history: { hasMore: false, isLoadingMore: false, loadMore },
|
|
416
|
+
});
|
|
417
|
+
expect(getFlatListProps().onStartReached).toBeUndefined();
|
|
418
|
+
|
|
419
|
+
await mountFlatList({
|
|
420
|
+
components: messageComponents,
|
|
421
|
+
history: { hasMore: true, isLoadingMore: true, loadMore },
|
|
422
|
+
});
|
|
423
|
+
expect(getFlatListProps().onStartReached).toBeUndefined();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("loads one page when start reached fires again before rerender", async () => {
|
|
427
|
+
const loadMore = vi.fn();
|
|
428
|
+
const callerOnStartReached = vi.fn();
|
|
429
|
+
|
|
430
|
+
await mountFlatList({
|
|
431
|
+
components: messageComponents,
|
|
432
|
+
history: { hasMore: true, isLoadingMore: false, loadMore },
|
|
433
|
+
onStartReached: callerOnStartReached,
|
|
434
|
+
});
|
|
435
|
+
const installedOnStartReached = getFlatListProps().onStartReached;
|
|
436
|
+
|
|
437
|
+
installedOnStartReached?.({ distanceFromStart: 0 });
|
|
438
|
+
installedOnStartReached?.({ distanceFromStart: 0 });
|
|
439
|
+
|
|
440
|
+
expect(loadMore).toHaveBeenCalledOnce();
|
|
441
|
+
expect(callerOnStartReached).toHaveBeenCalledTimes(2);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("loads again after a commit without a loading transition", async () => {
|
|
445
|
+
const loadMore = vi.fn();
|
|
446
|
+
const history = { hasMore: true, isLoadingMore: false, loadMore };
|
|
447
|
+
|
|
448
|
+
await mountFlatList({ components: messageComponents, history });
|
|
449
|
+
getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
|
|
450
|
+
|
|
451
|
+
await mountFlatList({ components: messageComponents, history });
|
|
452
|
+
getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
|
|
453
|
+
|
|
454
|
+
expect(loadMore).toHaveBeenCalledTimes(2);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("does not carry a request latch to another history source", async () => {
|
|
458
|
+
const firstLoadMore = vi.fn();
|
|
459
|
+
const secondLoadMore = vi.fn();
|
|
460
|
+
|
|
461
|
+
await mountFlatList({
|
|
462
|
+
components: messageComponents,
|
|
463
|
+
history: {
|
|
464
|
+
hasMore: true,
|
|
465
|
+
isLoadingMore: false,
|
|
466
|
+
loadMore: firstLoadMore,
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
|
|
470
|
+
|
|
471
|
+
await mountFlatList({
|
|
472
|
+
components: messageComponents,
|
|
473
|
+
history: {
|
|
474
|
+
hasMore: true,
|
|
475
|
+
isLoadingMore: false,
|
|
476
|
+
loadMore: secondLoadMore,
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
|
|
480
|
+
|
|
481
|
+
expect(firstLoadMore).toHaveBeenCalledOnce();
|
|
482
|
+
expect(secondLoadMore).toHaveBeenCalledOnce();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("loads again after the previous history request settles", async () => {
|
|
486
|
+
const loadMore = vi.fn();
|
|
487
|
+
|
|
488
|
+
await mountFlatList({
|
|
489
|
+
components: messageComponents,
|
|
490
|
+
history: { hasMore: true, isLoadingMore: false, loadMore },
|
|
491
|
+
});
|
|
492
|
+
getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
|
|
493
|
+
|
|
494
|
+
await mountFlatList({
|
|
495
|
+
components: messageComponents,
|
|
496
|
+
history: { hasMore: true, isLoadingMore: true, loadMore },
|
|
497
|
+
});
|
|
498
|
+
await mountFlatList({
|
|
499
|
+
components: messageComponents,
|
|
500
|
+
history: { hasMore: true, isLoadingMore: false, loadMore },
|
|
501
|
+
});
|
|
502
|
+
getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
|
|
503
|
+
|
|
504
|
+
expect(loadMore).toHaveBeenCalledTimes(2);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("allows retrying when loadMore throws synchronously", async () => {
|
|
508
|
+
const loadError = new Error("load failed");
|
|
509
|
+
const loadMore = vi
|
|
510
|
+
.fn()
|
|
511
|
+
.mockImplementationOnce(() => {
|
|
512
|
+
throw loadError;
|
|
513
|
+
})
|
|
514
|
+
.mockImplementationOnce(() => undefined);
|
|
515
|
+
|
|
516
|
+
await mountFlatList({
|
|
517
|
+
components: messageComponents,
|
|
518
|
+
history: { hasMore: true, isLoadingMore: false, loadMore },
|
|
519
|
+
});
|
|
520
|
+
const onStartReached = getFlatListProps().onStartReached;
|
|
521
|
+
|
|
522
|
+
expect(() => onStartReached?.({ distanceFromStart: 0 })).toThrow(
|
|
523
|
+
loadError,
|
|
524
|
+
);
|
|
525
|
+
expect(() => onStartReached?.({ distanceFromStart: 0 })).not.toThrow();
|
|
526
|
+
expect(loadMore).toHaveBeenCalledTimes(2);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("defaults the history threshold and preserves a caller override", async () => {
|
|
530
|
+
const history = {
|
|
531
|
+
hasMore: true,
|
|
532
|
+
isLoadingMore: false,
|
|
533
|
+
loadMore: vi.fn(),
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
await mountFlatList({ components: messageComponents, history });
|
|
537
|
+
expect(getFlatListProps().onStartReachedThreshold).toBe(1);
|
|
538
|
+
|
|
539
|
+
await mountFlatList({
|
|
540
|
+
components: messageComponents,
|
|
541
|
+
history,
|
|
542
|
+
onStartReachedThreshold: 0.5,
|
|
543
|
+
});
|
|
544
|
+
expect(getFlatListProps().onStartReachedThreshold).toBe(0.5);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("runs a caller onStartReached before loading more history", async () => {
|
|
548
|
+
const calls: string[] = [];
|
|
549
|
+
const info = { distanceFromStart: 42 };
|
|
550
|
+
const onStartReached = vi.fn(() => calls.push("onStartReached"));
|
|
551
|
+
|
|
552
|
+
await mountFlatList({
|
|
553
|
+
components: messageComponents,
|
|
554
|
+
history: {
|
|
555
|
+
hasMore: true,
|
|
556
|
+
isLoadingMore: false,
|
|
557
|
+
loadMore: () => calls.push("loadMore"),
|
|
558
|
+
},
|
|
559
|
+
onStartReached,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
getFlatListProps().onStartReached?.(info);
|
|
563
|
+
|
|
564
|
+
expect(onStartReached).toHaveBeenCalledWith(info);
|
|
565
|
+
expect(calls).toEqual(["onStartReached", "loadMore"]);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("leaves start-reached props untouched without history", async () => {
|
|
569
|
+
const onStartReached = vi.fn();
|
|
570
|
+
|
|
571
|
+
await mountFlatList({ components: messageComponents, onStartReached });
|
|
572
|
+
|
|
573
|
+
const props = getFlatListProps();
|
|
574
|
+
expect(props.onStartReached).toBe(onStartReached);
|
|
575
|
+
expect(props).not.toHaveProperty("onStartReachedThreshold");
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
395
579
|
describe("MessagesFlatList auto-scroll", () => {
|
|
396
580
|
it("scrolls to the bottom when messages first appear", async () => {
|
|
397
581
|
h.state.thread.messages = [{ id: "1", role: "user" }];
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
type NativeScrollEvent,
|
|
18
18
|
type NativeSyntheticEvent,
|
|
19
19
|
} from "react-native";
|
|
20
|
-
import type {
|
|
20
|
+
import type { ThreadMessage } from "@assistant-ui/core";
|
|
21
|
+
import type { MessageState } from "@assistant-ui/core/store";
|
|
21
22
|
import {
|
|
22
23
|
RenderChildrenWithAccessor,
|
|
23
24
|
useAuiEvent,
|
|
@@ -67,6 +68,13 @@ export type ThreadMessagesFlatListProps = Omit<
|
|
|
67
68
|
scrollToBottomOnRunStart?: boolean | undefined;
|
|
68
69
|
scrollToBottomOnInitialize?: boolean | undefined;
|
|
69
70
|
scrollToBottomOnThreadSwitch?: boolean | undefined;
|
|
71
|
+
history?:
|
|
72
|
+
| {
|
|
73
|
+
hasMore: boolean;
|
|
74
|
+
isLoadingMore: boolean;
|
|
75
|
+
loadMore: () => void;
|
|
76
|
+
}
|
|
77
|
+
| undefined;
|
|
70
78
|
};
|
|
71
79
|
|
|
72
80
|
/** @deprecated Use ThreadMessagesFlatListProps instead. */
|
|
@@ -75,6 +83,42 @@ export type ThreadMessagesProps = ThreadMessagesFlatListProps;
|
|
|
75
83
|
const DEFAULT_SYSTEM_MESSAGE = () => null;
|
|
76
84
|
const AT_BOTTOM_THRESHOLD = 4;
|
|
77
85
|
|
|
86
|
+
const useHistoryLoad = (
|
|
87
|
+
history: ThreadMessagesFlatListProps["history"],
|
|
88
|
+
onStartReached: FlatListProps<ThreadMessage>["onStartReached"],
|
|
89
|
+
) => {
|
|
90
|
+
const loadRequestedRef = useRef(false);
|
|
91
|
+
const hasMore = history?.hasMore ?? false;
|
|
92
|
+
const isLoadingMore = history?.isLoadingMore ?? false;
|
|
93
|
+
|
|
94
|
+
// The latch is scoped to one commit so a no-op load cannot disable paging.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
loadRequestedRef.current = isLoadingMore;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const handleStartReached = useCallback<
|
|
100
|
+
NonNullable<FlatListProps<ThreadMessage>["onStartReached"]>
|
|
101
|
+
>(
|
|
102
|
+
(info) => {
|
|
103
|
+
onStartReached?.(info);
|
|
104
|
+
if (loadRequestedRef.current) return;
|
|
105
|
+
loadRequestedRef.current = true;
|
|
106
|
+
try {
|
|
107
|
+
history?.loadMore();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
loadRequestedRef.current = false;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
[history, onStartReached],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
canLoadMore: hasMore && !isLoadingMore,
|
|
118
|
+
handleStartReached,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
78
122
|
const getComponent = (
|
|
79
123
|
components: MessageComponents,
|
|
80
124
|
role: ThreadMessage["role"],
|
|
@@ -415,10 +459,13 @@ export const ThreadMessagesFlatList = forwardRef<
|
|
|
415
459
|
onContentSizeChange,
|
|
416
460
|
onLayout,
|
|
417
461
|
onScroll,
|
|
462
|
+
onStartReached,
|
|
463
|
+
onStartReachedThreshold,
|
|
418
464
|
scrollEventThrottle,
|
|
419
465
|
scrollToBottomOnInitialize,
|
|
420
466
|
scrollToBottomOnRunStart,
|
|
421
467
|
scrollToBottomOnThreadSwitch,
|
|
468
|
+
history,
|
|
422
469
|
...flatListProps
|
|
423
470
|
},
|
|
424
471
|
forwardedRef,
|
|
@@ -485,6 +532,11 @@ export const ThreadMessagesFlatList = forwardRef<
|
|
|
485
532
|
[handleAutoScrollContentSizeChange, onContentSizeChange],
|
|
486
533
|
);
|
|
487
534
|
|
|
535
|
+
const { canLoadMore, handleStartReached } = useHistoryLoad(
|
|
536
|
+
history,
|
|
537
|
+
onStartReached,
|
|
538
|
+
);
|
|
539
|
+
|
|
488
540
|
return (
|
|
489
541
|
<FlatList
|
|
490
542
|
ref={setFlatListRef}
|
|
@@ -505,6 +557,21 @@ export const ThreadMessagesFlatList = forwardRef<
|
|
|
505
557
|
...(onScroll && { onScroll }),
|
|
506
558
|
...(scrollEventThrottle !== undefined && { scrollEventThrottle }),
|
|
507
559
|
})}
|
|
560
|
+
{...(history
|
|
561
|
+
? {
|
|
562
|
+
...(canLoadMore
|
|
563
|
+
? { onStartReached: handleStartReached }
|
|
564
|
+
: onStartReached
|
|
565
|
+
? { onStartReached }
|
|
566
|
+
: {}),
|
|
567
|
+
onStartReachedThreshold: onStartReachedThreshold ?? 1,
|
|
568
|
+
}
|
|
569
|
+
: {
|
|
570
|
+
...(onStartReached && { onStartReached }),
|
|
571
|
+
...(onStartReachedThreshold !== undefined && {
|
|
572
|
+
onStartReachedThreshold,
|
|
573
|
+
}),
|
|
574
|
+
})}
|
|
508
575
|
{...flatListProps}
|
|
509
576
|
/>
|
|
510
577
|
);
|
|
@@ -6,6 +6,7 @@ import { ThreadListNew } from "./ThreadListNew";
|
|
|
6
6
|
const h = vi.hoisted(() => ({
|
|
7
7
|
switchToNewThread: vi.fn<() => void>(),
|
|
8
8
|
state: { threads: { newThreadId: "new", mainThreadId: "other" } },
|
|
9
|
+
platform: { os: "web" },
|
|
9
10
|
pressableProps: null as Record<string, unknown> | null,
|
|
10
11
|
}));
|
|
11
12
|
|
|
@@ -41,7 +42,16 @@ vi.mock("react-native", async (importOriginal) => {
|
|
|
41
42
|
);
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
...actual,
|
|
47
|
+
Platform: {
|
|
48
|
+
...actual.Platform,
|
|
49
|
+
get OS() {
|
|
50
|
+
return h.platform.os;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
Pressable: PressableMock,
|
|
54
|
+
};
|
|
45
55
|
});
|
|
46
56
|
|
|
47
57
|
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -58,6 +68,7 @@ describe("ThreadListNew", () => {
|
|
|
58
68
|
beforeEach(() => {
|
|
59
69
|
h.switchToNewThread.mockReset();
|
|
60
70
|
h.state.threads = { newThreadId: "new", mainThreadId: "other" };
|
|
71
|
+
h.platform.os = "web";
|
|
61
72
|
h.pressableProps = null;
|
|
62
73
|
|
|
63
74
|
container = document.createElement("div");
|
|
@@ -110,39 +121,55 @@ describe("ThreadListNew", () => {
|
|
|
110
121
|
expect(h.switchToNewThread).not.toHaveBeenCalled();
|
|
111
122
|
});
|
|
112
123
|
|
|
113
|
-
it("marks itself
|
|
124
|
+
it("marks itself current on the web while the new thread is current", async () => {
|
|
114
125
|
h.state.threads = { newThreadId: "new", mainThreadId: "new" };
|
|
115
126
|
|
|
116
|
-
await mount();
|
|
127
|
+
const el = await mount();
|
|
117
128
|
|
|
129
|
+
expect(el.getAttribute("aria-current")).toBe("true");
|
|
130
|
+
expect(el.hasAttribute("aria-selected")).toBe(false);
|
|
118
131
|
expect(accessibilityState()).toMatchObject({ selected: true });
|
|
119
132
|
});
|
|
120
133
|
|
|
121
|
-
it("is not
|
|
122
|
-
await mount();
|
|
134
|
+
it("is not current while another thread is current", async () => {
|
|
135
|
+
const el = await mount();
|
|
123
136
|
|
|
137
|
+
expect(el.hasAttribute("aria-current")).toBe(false);
|
|
124
138
|
expect(accessibilityState()).toMatchObject({ selected: false });
|
|
125
139
|
});
|
|
126
140
|
|
|
141
|
+
it("marks itself selected without aria-current on native", async () => {
|
|
142
|
+
h.platform.os = "ios";
|
|
143
|
+
h.state.threads = { newThreadId: "new", mainThreadId: "new" };
|
|
144
|
+
|
|
145
|
+
const el = await mount();
|
|
146
|
+
|
|
147
|
+
expect(el.hasAttribute("aria-current")).toBe(false);
|
|
148
|
+
expect(accessibilityState()).toMatchObject({ selected: true });
|
|
149
|
+
});
|
|
150
|
+
|
|
127
151
|
it("keeps other accessibility state the caller passes", async () => {
|
|
128
152
|
h.state.threads = { newThreadId: "new", mainThreadId: "new" };
|
|
129
153
|
|
|
130
|
-
await mount({ accessibilityState: { busy: true } });
|
|
154
|
+
const el = await mount({ accessibilityState: { busy: true } });
|
|
131
155
|
|
|
156
|
+
expect(el.getAttribute("aria-current")).toBe("true");
|
|
132
157
|
expect(accessibilityState()).toMatchObject({ selected: true, busy: true });
|
|
133
158
|
});
|
|
134
159
|
|
|
135
160
|
it("lets the caller override the selected state", async () => {
|
|
136
161
|
h.state.threads = { newThreadId: "new", mainThreadId: "new" };
|
|
137
162
|
|
|
138
|
-
await mount({ accessibilityState: { selected: false } });
|
|
163
|
+
const el = await mount({ accessibilityState: { selected: false } });
|
|
139
164
|
|
|
165
|
+
expect(el.hasAttribute("aria-current")).toBe(false);
|
|
140
166
|
expect(accessibilityState()).toMatchObject({ selected: false });
|
|
141
167
|
});
|
|
142
168
|
|
|
143
169
|
it("lets the caller select a control the state reports inactive", async () => {
|
|
144
|
-
await mount({ accessibilityState: { selected: true } });
|
|
170
|
+
const el = await mount({ accessibilityState: { selected: true } });
|
|
145
171
|
|
|
172
|
+
expect(el.getAttribute("aria-current")).toBe("true");
|
|
146
173
|
expect(accessibilityState()).toMatchObject({ selected: true });
|
|
147
174
|
});
|
|
148
175
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Platform,
|
|
3
4
|
Pressable,
|
|
4
5
|
type PressableProps,
|
|
5
6
|
type PressableStateCallbackType,
|
|
@@ -27,12 +28,14 @@ export const ThreadListNew = ({
|
|
|
27
28
|
(s) => s.threads.newThreadId === s.threads.mainThreadId,
|
|
28
29
|
);
|
|
29
30
|
const { switchToNewThread } = useThreadListNew();
|
|
31
|
+
const selected = accessibilityState?.selected ?? isActive;
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
34
|
<Pressable
|
|
33
35
|
onPress={switchToNewThread}
|
|
34
36
|
accessibilityRole="button"
|
|
35
|
-
accessibilityState={{
|
|
37
|
+
accessibilityState={{ ...accessibilityState, selected }}
|
|
38
|
+
aria-current={Platform.OS === "web" && selected ? "true" : undefined}
|
|
36
39
|
{...pressableProps}
|
|
37
40
|
>
|
|
38
41
|
{typeof children === "function"
|
|
@@ -9,6 +9,7 @@ const h = vi.hoisted(() => ({
|
|
|
9
9
|
threads: { mainThreadId: "thread-1" },
|
|
10
10
|
threadListItem: { id: "thread-1" },
|
|
11
11
|
},
|
|
12
|
+
platform: { os: "web" },
|
|
12
13
|
pressableProps: null as Record<string, unknown> | null,
|
|
13
14
|
}));
|
|
14
15
|
|
|
@@ -37,7 +38,16 @@ vi.mock("react-native", async (importOriginal) => {
|
|
|
37
38
|
{ ...props, ref },
|
|
38
39
|
);
|
|
39
40
|
});
|
|
40
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
...actual,
|
|
43
|
+
Platform: {
|
|
44
|
+
...actual.Platform,
|
|
45
|
+
get OS() {
|
|
46
|
+
return h.platform.os;
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
Pressable: PressableMock,
|
|
50
|
+
};
|
|
41
51
|
});
|
|
42
52
|
|
|
43
53
|
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -55,6 +65,7 @@ describe("ThreadListItemTrigger", () => {
|
|
|
55
65
|
h.switchTo.mockReset();
|
|
56
66
|
h.state.threads.mainThreadId = "thread-1";
|
|
57
67
|
h.state.threadListItem.id = "thread-1";
|
|
68
|
+
h.platform.os = "web";
|
|
58
69
|
h.pressableProps = null;
|
|
59
70
|
|
|
60
71
|
container = document.createElement("div");
|
|
@@ -107,23 +118,38 @@ describe("ThreadListItemTrigger", () => {
|
|
|
107
118
|
expect(h.switchTo).not.toHaveBeenCalled();
|
|
108
119
|
});
|
|
109
120
|
|
|
110
|
-
it("marks the current thread
|
|
111
|
-
await mount();
|
|
121
|
+
it("marks the current thread current on the web", async () => {
|
|
122
|
+
const el = await mount();
|
|
112
123
|
|
|
124
|
+
expect(el.getAttribute("aria-current")).toBe("true");
|
|
125
|
+
expect(el.hasAttribute("aria-selected")).toBe(false);
|
|
113
126
|
expect(accessibilityState()).toMatchObject({ selected: true });
|
|
114
127
|
});
|
|
115
128
|
|
|
116
|
-
it("does not mark another thread
|
|
129
|
+
it("does not mark another thread current", async () => {
|
|
117
130
|
h.state.threads.mainThreadId = "thread-2";
|
|
118
131
|
|
|
119
|
-
await mount();
|
|
132
|
+
const el = await mount();
|
|
120
133
|
|
|
134
|
+
expect(el.hasAttribute("aria-current")).toBe(false);
|
|
121
135
|
expect(accessibilityState()).toMatchObject({ selected: false });
|
|
122
136
|
});
|
|
123
137
|
|
|
138
|
+
it("marks the current thread selected without aria-current on native", async () => {
|
|
139
|
+
h.platform.os = "ios";
|
|
140
|
+
|
|
141
|
+
const el = await mount();
|
|
142
|
+
|
|
143
|
+
expect(el.hasAttribute("aria-current")).toBe(false);
|
|
144
|
+
expect(accessibilityState()).toMatchObject({ selected: true });
|
|
145
|
+
});
|
|
146
|
+
|
|
124
147
|
it("keeps caller accessibility state overrides", async () => {
|
|
125
|
-
await mount({
|
|
148
|
+
const el = await mount({
|
|
149
|
+
accessibilityState: { busy: true, selected: false },
|
|
150
|
+
});
|
|
126
151
|
|
|
152
|
+
expect(el.hasAttribute("aria-current")).toBe(false);
|
|
127
153
|
expect(accessibilityState()).toMatchObject({ busy: true, selected: false });
|
|
128
154
|
});
|
|
129
155
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Platform,
|
|
3
4
|
Pressable,
|
|
4
5
|
type PressableProps,
|
|
5
6
|
type PressableStateCallbackType,
|
|
@@ -27,12 +28,14 @@ export const ThreadListItemTrigger = ({
|
|
|
27
28
|
(s) => s.threads.mainThreadId === s.threadListItem.id,
|
|
28
29
|
);
|
|
29
30
|
const { switchTo } = useThreadListItemTrigger();
|
|
31
|
+
const selected = accessibilityState?.selected ?? isActive;
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
34
|
<Pressable
|
|
33
35
|
onPress={switchTo}
|
|
34
36
|
accessibilityRole="button"
|
|
35
|
-
accessibilityState={{
|
|
37
|
+
accessibilityState={{ ...accessibilityState, selected }}
|
|
38
|
+
aria-current={Platform.OS === "web" && selected ? "true" : undefined}
|
|
36
39
|
{...pressableProps}
|
|
37
40
|
>
|
|
38
41
|
{typeof children === "function"
|